范围

表示一系列值

2004 年 3 月 7 日

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

在比较中,经常看到将值与一系列值进行比较。范围通常由一对值处理,您会针对这两个值进行检查。相反,Range 使用单个对象来表示整个范围,然后提供相关的操作来测试值是否落在范围内以及比较范围。

工作原理

基本类非常简单。您有一个类,其中包含两个字段,分别表示范围的开始和结束。您还提供了一个 includes 方法,用于测试提供的值是否在范围内。

您可以将 Range 与任何支持比较操作的类型一起使用,这与 <、>、=、<= 和 >= 的道德等效性相同。根据语言和类型,您可能无法获得这些确切的操作符,但您需要类型上的等效关系,也就是说,您需要具有一些默认排序标准,允许您对值进行排名。

图 1:使用参数化类型表示法在 UML 中显示范围

如果您的语言支持,Range 是参数化类的明显选择 图 1。在 UML 术语中,您可以通过使用具有类型 Range<number> 和 Range<date> 的类来显示不同类型的范围。这实际上只是对数字范围和日期范围的建模速记。因此,包括我在内的许多人更喜欢避免奇怪的名称,而只使用数字范围和数据范围之类的术语。

更复杂的 Range 可以使用排序标准进行设置。通常,这可以是任何能够对用于范围的类型的实例进行排名的函数。排序标准本质上是一个函数,或者是一个包装函数的对象。

您可能有一些开放式范围(例如大于六)。您可以通过几种方式处理此问题。一种是将空值视为无限。您的范围检查代码会变得更加复杂,但实际上您可以向用户隐藏这一点。另一种选择是为极值(例如正无穷大)创建一个 特殊情况。无论您做出哪种选择,您都可以通过使用 Range.greaterThan(6) 形式的创建方法来向类用户隐藏。

如果您正在进行范围的类型是连续的(例如实数),而不是离散的(例如整数或日期),那么您将需要其他信息来判断上限或下限是否在范围内。对于整数,您可以通过将下限设置为 7 来选择大于六的范围。但是,对于实数,您不希望使用 6.0000000000001 的下限。相反,请使用几个布尔标记。

除了测试值是否在范围内的操作之外,您还可以包含比较范围的操作:这些操作可以判断一个范围是否与另一个范围重叠,一个范围是否与另一个范围相切,或者一个范围是否包含另一个范围。当您需要执行诸如检查范围的某个子集是否包含另一个范围中的所有值之类的操作时,这些操作非常有用。

如果您的语言使用参数化类,则范围类显然适合参数化类。如果不是,您将面临根据抽象类构建它们或为特定情况创建专门的子类型。如果您可能出于其他目的获取上限和下限值,那么向下转换的痛苦将足以让您最好创建一个具体的范围类。

在考虑范围时,我发现最常见的方法是具有开始和结束。但是,拥有开始和长度,甚至结束和长度同样有用。您还可以拥有所有三个:开始、结束和长度,它们之间存在明显的约束。

何时使用它

Range 是我一直使用的模式。编码适当的范围类很容易,并且一旦完成,使用范围比使用值对更容易。在建模时,使用范围比使用对更明确,而且同样直观。

示例:日期范围(Java)

举个例子,我将使用日期范围。这是一个需要经常使用的范围,它使我能够巧妙地避开连续范围的额外复杂性。我使用的是自己的日期类,而不是 Java 的标准日期,该类只有日期精度(参见 时间点 中的讨论)。

基本构造函数和访问器非常简单。

class DateRange...

  public DateRange (Date start, Date end) {
    this (new MfDate(start), new MfDate(end));
  }
  public DateRange (MfDate start, MfDate end) {
    this.start = start;
    this.end = end;
  }

class DateRange...

  public MfDate end(){
    return end;
  }
  public MfDate start() {
    return start;
  }
  public String toString() {
    if (isEmpty()) return "Empty Date Range";
    return start.toString() + " - " + end.toString();
  }
  public boolean isEmpty() {
    return start.after(end);
  }

在任何使用 Range 的情况下,要提供的关键方法是 includes 方法。

class DateRange...

  public boolean includes (MfDate arg) {
    return !arg.before(start) && !arg.after(end);
  }

我喜欢为开放式范围和空范围提供额外的构造函数。

class DateRange...

  public static DateRange upTo(MfDate end) {
    return new DateRange(MfDate.PAST, end);
  }
  public static DateRange startingOn(MfDate start) {
    return new DateRange(start, MfDate.FUTURE);
  }
  public static DateRange EMPTY = new DateRange(new MfDate(2000,4,1), new MfDate(2000,1,1));

提供允许您比较范围的操作非常有用。

class DateRange...

  public boolean equals (Object arg) {
    if (! (arg instanceof DateRange)) return false;
    DateRange other = (DateRange) arg;
    return start.equals(other.start) && end.equals(other.end);
  }
  public int hashCode() {
    return start.hashCode();
  }
  public boolean overlaps(DateRange arg) {
     return arg.includes(start) || arg.includes(end) || this.includes(arg);
   }
  public boolean includes(DateRange arg) {
    return this.includes(arg.start) && this.includes(arg.end);
  }

对于大多数应用程序来说,这已经足够了。但某些情况表明其他有用的行为。一个是找出两个范围之间存在什么差距。

class DateRange...

  public DateRange gap(DateRange arg){
    if (this.overlaps(arg)) return DateRange.EMPTY;
    DateRange lower, higher;
    if (this.compareTo(arg) < 0) {
      lower = this;
      higher = arg;
    }
    else {
      lower = arg;
      higher = this;
    }
    return new DateRange(lower.end.addDays(1), higher.start.addDays(-1));
  }
  public int compareTo(Object arg) {
    DateRange other = (DateRange) arg;
    if (!start.equals(other.start)) return start.compareTo(other.start);
    return end.compareTo(other.end);
  }

另一个是检测两个日期范围是否彼此相接。

class DateRange...

  public boolean abuts(DateRange arg) {
    return !this.overlaps(arg) && this.gap(arg).isEmpty();
  }

以及查看一组范围是否完全对另一个范围进行分区。

class DateRange...

  public boolean partitionedBy(DateRange[] args) {
    if (!isContiguous(args)) return false;
    return this.equals(DateRange.combination(args));
  }
  public static DateRange combination(DateRange[] args) {
    Arrays.sort(args);
    if (!isContiguous(args)) throw new IllegalArgumentException("Unable to combine date ranges");
    return new DateRange(args[0].start, args[args.length -1].end);
  }
  public static boolean isContiguous(DateRange[] args) {
    Arrays.sort(args);
    for (int i=0; i<args.length - 1; i++) {
        if (!args[i].abuts(args[i+1])) return false;
    }
    return true;
  }