範囲

値の範囲を表す

これは、私が2000年代半ばに執筆していた「エンタープライズアプリケーションアーキテクチャの更なる開発」の一部です。その後、多くの他の事柄に注意を奪われてしまい、これ以上取り組む時間がありませんでしたし、近い将来、時間を見つけることも難しいと考えています。そのため、この資料は非常に草案段階のものであり、再度作業に取り組めるようになるまで、修正や更新は行いません。

値を値の範囲と比較する場面がよく見られます。範囲は通常、値のペアで処理され、両方の値に対してチェックが行われます。Rangeは、代わりに単一のオブジェクトを使用して範囲全体を表し、値が範囲内にあるかどうかをテストしたり、範囲を比較したりするための関連する操作を提供します。

動作方法

基本クラスは非常にシンプルです。範囲の開始と終了を表す2つのフィールドを持つクラスがあります。また、提供された値が範囲内にあるかどうかをテストするincludesメソッドも提供します。

比較演算をサポートする任意の型でRangeを使用できます。つまり、<、>、=、<=、>=と同等のものです。言語や型によっては、これらの演算子が正確に得られる場合と得られない場合があります。しかし、型に対して同等の関係が必要になります。つまり、値のランク付けを可能にするデフォルトのソート基準が必要です。

図1:パラメーター化された型表記を使用したUMLでの範囲の表示

言語がサポートしている場合、Rangeはパラメーター化されたクラス図1の明白な選択肢です。UMLでは、Range<number>やRange<date>などのクラスを使用して、さまざまな種類の範囲を示すことができます。これは、数値範囲と日付範囲の単なるモデリングの略記にすぎません。したがって、私自身を含め多くの人が、奇妙な名前を避けて、数値範囲や日付範囲のような用語を使用することを好みます。

より洗練されたRangeは、ソート基準を使用して設定できます。一般的に、これは範囲で使用されている型のインスタンスをランク付けできる任意の関数です。ソート基準は基本的に関数、または関数をラップするオブジェクトです。

おそらく、上限のない範囲(例:6より大きい)を持つでしょう。これにはいくつかの方法で対処できます。1つは、null値を無制限を意味するものとして扱うことです。範囲チェックコードは少し複雑になりますが、ユーザーからこれを隠すことができます。もう1つの方法は、極値(例:正の無限大)に対して特別なケースを作成することです。どちらの方法を選択しても、`Range.greaterThan(6)。`という形式の作成メソッドを使用して、クラスのユーザーから隠すことができます。

範囲を指定する型が連続的(例:実数)である場合、離散的(例:整数または日付)とは異なり、上限または下限が範囲内にあるかどうかを知るために他の情報が必要です。整数では、下限を7に設定することで、6より大きい範囲を選択できます。しかし、実数では、6.0000000000001の下限を使用したくありません。代わりに、ブール値のマーカーをいくつか使用します。

値が範囲内にあるかどうかをテストする操作に加えて、範囲を比較する操作を含めることもできます。これらは、ある範囲が別の範囲と重なっているかどうか、ある範囲が別の範囲に接しているかどうか、またはある範囲が別の範囲を含んでいるかどうかを判断できます。これらは、範囲のサブセットが別の範囲のすべての値を含むかどうかをチェックする必要がある場合などに非常に役立ちます。

Rangeクラスは、言語が使用している場合、明らかにパラメーター化されたクラスに適しています。そうでない場合は、抽象クラスに基づいて構築するか、特定のケースに特化したサブタイプを作成する必要があります。他の目的で上限と下限の値を取得する可能性が高い場合、ダウンキャストの困難は、具体的なRangeクラスを作成する方が良いほどになります。

範囲について考えるとき、最も一般的なアプローチは開始と終了を持つことです。しかし、開始と長さ、または終了と長さを持つことも同様に役立ちます。開始、終了、長さの3つすべてを持つこともできます。値間の明らかな制約があります。

いつ使用するのか

Rangeは、私が常に使用するパターンです。適切なRangeクラスのコーディングは簡単で、一度作成すれば、値のペアを使用するよりも範囲を使用する方が簡単です。モデリングする際には、ペアを使用するよりも範囲を使用する方が明確で、直感的です。

例:日付範囲(Java)

例として、日付範囲を使用します。これは必要な一般的な範囲であり、連続範囲の追加の複雑さをうまく回避できます。Javaの標準日付を使用する代わりに、日付の精度のみを持つ独自のDateクラスを使用しています(Time Pointの議論を参照)。

基本的なコンストラクターとアクセサーは非常にシンプルです。

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);
  }

ほとんどのアプリケーションでは、これですべてです。しかし、特定の状況では、他の便利な動作が示唆されます。1つは、2つの範囲間にどのようなギャップが存在するかを調べることです。

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);
  }

もう1つは、2つの日付範囲が互いに隣接しているかどうかを検出することです。

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;
  }