生效日期

向对象添加时间段以显示其生效时间。

2004 年 3 月 7 日

这是我在 2000 年代中期进行的 企业应用程序架构进一步开发 编写的一部分。不幸的是,此后太多其他事情吸引了我的注意力,因此我没有时间进一步处理它们,而且在可预见的未来我也没有看到太多时间。因此,这些材料非常草稿形式,在我能够抽出时间再次处理之前,我不会进行任何更正或更新。

工作原理

许多事实只在特定时间段内为真。因此,描述这些事实的一种明显方法是用时间段标记它们。对于许多人来说,该时间段是一对日期,但是 范围 可以在这里使用,使该日期范围成为一个对象。

定义后,生效范围将用于查询以返回在特定日期生效的适当对象。

生效范围通常直接更新,但是通常有意义的是提供更适合类需求的接口。创建方法可以获取生效期的开始日期,然后使用开放式 范围 来指示没有结束日期:这适合在特定日期创建并一直有效直到另行通知的情况。当收到进一步通知时,您可以使用一种方法来指示该对象不再有效以及发生的日期。

使用草图中建议的就业示例,我们有一个叫 Wellington 的人。他在 1999 年 12 月 12 日开始在印度公司工作,我们通过创建一个就业对象来表示这一点 ( 图 1)。随着时间的推移,他于 4 月 1 日开始在 Peninsula 公司工作,并于 5 月 1 日结束在印度公司的工作。 图 2 显示了这些事件发生后对象的状况。请注意,这意味着他在 4 月份同时被两家公司雇用。

图 1:只有一个就业

图 2:一个就业结束,另一个开始

此更新机制处理增量更新,即按时间顺序发生的更新。在许多情况下,增量更新是您所需要的。但是,有时您需要追溯更新,有效地纠正时间线上的错误。假设我们后来发现 Wellington 实际上在 5 月份为 Dublin 公司工作,直到 6 月 1 日才开始在 Peninsula 公司工作。我们必须更改 Peninsula 公司的就业生效日期,并添加 Dublin 公司的新就业,以产生 图 3 中描述的状态。

图 3:添加 Dublin 公司就业后

这通常需要更原始的接口,允许就业直接更改其生效范围。

支持追溯更改通常很重要,因为人们会犯错误。我们可以通过直接访问生效日期范围来添加它。

从某种意义上说,添加双时态支持非常简单 - 您只需添加另一个日期范围。当然,复杂性会传递给类的用户,他们现在需要在所有查询和更新中始终使用这两个日期。

何时使用它

生效日期是建模中指示时间性的最常见方法。它简单易懂,易于理解其用法。它主要的缺点是它要求客户端了解这些时间方面并在处理时考虑它们。因此,任何想要查看当前信息的查询都需要在其逻辑中添加一个子句来测试生效范围。虽然这不是一项非常繁重的要求,但它确实使事情变得更加棘手,尤其是在时间责任域不明确的情况下。

可以构建结构来消除大部分这种责任,从而使时间问题更加透明 - 这样您只需要在特定需要时才需要担心它们。要针对一次属性执行此操作,可以使用 时间属性。要针对整个对象执行此操作,可以使用 时间对象。这两种更复杂的模式都处理了大部分时间逻辑,减少了这些对象客户端的负担。

因此,当您遇到时间行为的简单情况时,请使用 生效日期,并且在域中,这些对象应该是时间性的。在对象实现中,您应该确保实际使用 范围 作为生效范围,因为这比使用日期对要容易得多。

示例:就业(Java)

这些代码示例与我在工作原理部分中谈到的案例相平行。我将从简单命名的对象类(人员和公司)开始。

class NamedObject...

  protected String _name = "no name";
  public NamedObject ()  {}
  public NamedObject (String name)  {_name = name;}
  public String name ()  {return _name;}
  public String toString() {return _name;}

class Company...

  class Company extends NamedObject{
    Company (String name) {
      super(name);
    }

class Person...

  class Person extends NamedObject{
    private List employments = new ArrayList();
    public Person (String name) {
      super(name);
    }
    Employment[] employments() {
      return (Employment[]) employments.toArray(new Employment[0]);
    }

就业类是具有生效范围的类。它的基本数据非常简单

class Employment...

  private DateRange effective;
  private Company company;
  Company company() {return company;}
  boolean isEffectiveOn(MfDate arg){
    return effective.includes(arg);
  }

现在让我们添加增量行为。我通过人员上的方法添加新的就业,该方法使用开始日期创建一个新的就业。

class Person...

  void addEmployment(Company company, MfDate startDate) {
    employments.add(new Employment(company, startDate));
  }

class Employment...

  Employment (Company company, MfDate startDate) {
    this.company = company;
    effective = DateRange.startingOn(startDate);
  }

我通过就业类上的方法结束就业。

class Employment...

  void end (MfDate endDate) {
    effective = new DateRange(effective.start(), endDate);
  }

有了这些,我们现在可以向人员添加就业,并查询它们以找到特定日期的适当就业。

class Tester...

  public void setUp() {
    duke.addEmployment(india, new mf.MfDate(1999,12,1));
    duke.addEmployment(peninsular, new MfDate(2000,4,1));
    duke.employments()[0].end(new MfDate (2000,5,1));
  }
  public void testAdditive() {
    assertEquals(2, duke.employments().length);
    Employment actual = null;
    for (int i = 0; i < duke.employments().length; i++) {
      if (duke.employments()[i].isEffectiveOn(new MfDate(2000,6,1))) {
        actual = duke.employments()[i];
        break;
      }
    }
    assertNotNull(actual);
    assertEquals(peninsular, actual.company());
  }

一个好的对象设计者可能会想知道该 for 循环是否应该真正移动到人员类上的方法中。确实应该,这就是 时间属性 模式的驱动因素。我们将看到这种移动的后果,因此我将继续在此模式的片段中使用 for 循环。

现在让我们看一下追溯更改。对于这些更改,我们需要在就业和人员上进行一些更原始的行为。

class Person...

  void addEmployment(Employment arg) {
   employments.add(arg);
 }

class Employment...

  void setEffectivity(DateRange arg) {
    effective = arg;
  }
  Employment (Company company, DateRange effective) {
    this.company = company;
    this.effective = effective;
  }

然后我们可以进行这样的追溯更改

class Tester...

  public void testRetro() {
    duke.employments()[1].setEffectivity(DateRange.startingOn(new MfDate(2000,6,1)));
    duke.addEmployment(new Employment(dublin, new DateRange(new MfDate(2000,5,1), new MfDate(2000,5,31))));
    Employment april = null;
    for (int i = 0; i < duke.employments().length; i++) {
      if (duke.employments()[i].isEffectiveOn(new MfDate(2000,4,10))) {
        april = duke.employments()[i];
        break;
      }
    }
    assertNotNull(april);
    assertEquals(india, april.company());
    Employment may = null;
    for (int i = 0; i < duke.employments().length; i++) {
      if (duke.employments()[i].isEffectiveOn(new MfDate(2000,5,10))) {
        may = duke.employments()[i];
        break;
      }
    }
    assertNotNull("null may", may);
    assertEquals(dublin, may.company());
  }

追溯更改并不总是需要,但通常是需要的 - 毕竟人们以犯错而闻名。进行追溯更改通常需要这种比增量更改更原始的接口,因此有人认为您不需要为增量更改提供单独的接口。但是,我更喜欢包含它,因为它使执行最常见的增量更改变得更容易。毕竟,我们不是在寻找最小的接口,而是在寻找最易于使用的接口。小有助于易用性(因为学习的内容更少),但它只是一个因素,而不是主要因素。