範圍

表示值範圍

2004 年 3 月 7 日

這是 進階企業應用程式架構開發 文章的一部分,我在 2000 年代中期撰寫。遺憾的是,從那之後有太多其他事情吸引了我的注意力,所以我沒有時間進一步處理它們,而且在可預見的未來也沒有太多時間。因此,這份資料很草稿,直到我有時間再次處理它之前,我不會進行任何更正或更新。

在將值與值範圍進行比較時,看到比較是很常見的。範圍通常由一對值處理,而且您會針對它們進行檢查。範圍 改用單一物件來表示整個範圍,然後提供相關的運算來測試值是否落在範圍內,並比較範圍。

運作方式

基本類別非常簡單。您有一個類別,其中包含兩個欄位,表示範圍的開始和結束。您還提供一個 includes 方法,用於測試提供的值是否落在範圍內。

您可以將 範圍 與支援比較運算的任何類型搭配使用,也就是 <、>、=、<= 和 >= 的道德等價物。根據語言和類型,您可能無法取得這些確切的運算子,但您需要類型上的等效關係,也就是您需要具備一些預設排序準則,讓您可以對值進行排名。

圖 1:使用參數化類型符號在 UML 中顯示範圍

如果您的語言支援,範圍是參數化類別的明顯選擇 圖 1。在 UML 術語中,您可以使用具有類型 Range<number> 和 Range<date> 的類別來顯示不同類型的範圍。這實際上只是數字範圍和日期範圍的建模簡寫。因此,包括我在內的許多人,都比較喜歡避免使用奇怪的名稱,而只使用數字範圍和資料範圍等術語。

可以透過排序條件設定更精密的 範圍。一般來說,這可以是任何有能力對範圍中所使用的類型實例進行排序的函數。排序條件基本上是一個函數,或是一個包裝函數的物件。

你可能會有一些開放式的範圍(例如大於 6)。你可以透過幾種方式來處理這件事。一種方法是將空值視為無限大。你的範圍檢查程式碼將會變得有點複雜,但你幾乎可以對你的使用者隱藏這件事。另一種替代方法是針對極端值(例如正無限大)建立一個 特殊案例。無論你做出什麼選擇,你都可以透過建立 Range.greaterThan(6). 形式的建立方法來對類別使用者隱藏。

如果你所設定的範圍類型是連續的(例如實數),而不是離散的(例如整數或日期),你將需要其他資訊來判斷上限或下限是否在範圍內。對於整數,你可以透過將下限設定為 7 來選擇大於 6 的範圍。然而,對於實數,你不想使用 6.0000000000001 的下限。改用幾個布林標記。

除了用於測試值是否在範圍內的運算外,你還可以包含用於比較範圍的運算:這些運算可以判斷一個範圍是否與另一個範圍重疊、是否與另一個範圍相切,或一個範圍是否包含另一個範圍。當你需要執行某些動作(例如檢查範圍子集是否包含另一個範圍中的所有值)時,這些運算會非常有用。

如果你的語言使用參數化類別,範圍類別顯然適合參數化類別。如果不是,你將面臨根據抽象類別建立它們或為特定案例建立特殊子類型的問題。如果你很可能會為了其他目的取得上限和下限值,那麼向下轉型的痛苦將足以讓你最好建立一個具體的範圍類別。

在思考範圍時,我發現最常見的方法是有一個開始和一個結束。然而,開始和長度,甚至結束和長度也同樣有用。你也可以同時擁有這三個:開始、結束和長度,以及值之間明顯的約束。

何時使用

範圍是我一直使用的模式。編寫適當的範圍類別很容易,一旦你完成後,使用範圍比使用成對值更容易。在建模時,使用範圍比使用成對值更明確,而且同樣直觀。

範例:日期範圍(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;
  }