torutkのブログ

ソフトウェア・エンジニアのブログ

範囲を扱う

プログラミングにおいて、台数、個数、チャンネル数、金額、速度、年齢、身長といった何らかの数量を表現することはよくあることです。
こうした数量を、それぞれ個別のクラスにすることは面倒なので、大抵はint、doubleなどの基本型で表現します。

表現する数量には、プログラミング対象世界によって決まる値の有効範囲があります。例えば、遠足のお菓子は金額で300円まで(0円以上300円以下の範囲)、ある車の速度計は0kmから180kmまで、などです。会計上では金額はマイナスの値を取ることもありますし、所持金ではマイナスの値はとらないでしょう。

プログラミングでは、この有効範囲の中にあるかどうかを判定する、という処理があちらこちらに出てきます。これを、そのたびに、

public void setSpeed(int speed) {
    if (speed < 0 || 180 < speed) {
        throw new IllegalArgumentException("out of range");
    }
    ...

と範囲チェックコードを書くのは、似たコードがあちこちに散在するので品質が劣化します。*1

手続き的な発想では、範囲チェックをメソッド化することで散在を防ぎます。

public class Validator {
    public static boolean includes(int lower, int upper, int value) {
        return lower <= value && value <= upper;
    }
}

public class SpeedMeter {
    ...
    public void setSpeed(int speed) {
        if (!Validator.includes(0, 180, speed)) {
            throw new IllegalArgumentException("out of range");
        }
    ...

うん、あまりコードが変わらないです。範囲の下限、上限の指定はチェックする箇所に散在してしまいます。

ここで、例外スローまでをValidator.includesに記載すれば、上で挙げたコード例はシンプルになります。しかし、例外にしたくないときは別なメソッドを用意し呼び出し側で使い分けることになり、うれしくはありません。

オブジェクト化

範囲の下限、上限を状態として持つオブジェクトにすれば、改善ができます。

public class IntValidator {
    private int lowerBound;
    private int upperBound;

    public IntValidator(int lowerBount, int upperBound) {
        this.lowerBound = lowerBound;
        this.upperBound = upperBound;
    }

    public boolean includes(int value) {
        return  lowerBound <= value && value <= upperBound;
    }
}

public class SpeedMeter {
    private static final IntValidator speedValidator = new IntValidator(0, 180);
    ...
    public void setSpeed(int speed) {
        if (!speedValidator.includes(speed)) {
            throw new IllegalArgumentException("out of range");
        }
      ...

このような範囲を扱うクラスを導入するのは、マーチンファウラー氏の書籍「アナリシスパターン」でも取り上げられています。*2

範囲を扱うライブラリ

範囲を扱うライブラリを探してみると、コード例を紹介したWeb記事やライブラリをいくつか見い出すことができました。

大半は、

public class Range<T extends Comparable<T>>

ジェネリックスクラスで定義しており、一部

public interface Range<T extends Comparable<T>>

あるいは

public interface Range<T>

となっていました。

Rangeの実装イメージ

それぞれの実装を参考にしつつ、エラー処理や範囲チェックに直接関与しないものを除いて、Rangeの実装の骨子を簡潔に示してみます。

public class Range<T extends Comparable> {
    private T lowerBound;
    private T upperBound;

    public Range(T lowerBound, T upperBound) {
        this.lowerBound = lowerBound;
        this.upperBound = upperBound;
    }

    public boolean includes(T value) {
        return lowerBound.compareTo(value) <= 0 && value.compareTo(upperBound) <= 0;
    }
}

利用イメージは

public class SpeedMeter {
    private static final Range<Integer> speedRange = new Range<Integer>(0, 180);
    ...
    public void setSpeed(int speed) {
        if (!speedRange.includes(speed)) {
            throw new IllegalArgumentException("out of range");
         ...

範囲は、以上以下だけでなく、より大きい、未満がある

範囲は、以上、以下だけでなく、より大きい、未満、もあります。しかし、上述の実装イメージだと、以上、以下の決め打ちで範囲チェックをしているため、より大きい、未満、が扱えません。

整数の場合は、0より大きく10未満、を、1以上9以下、と読み替えることで、以上、以下の範囲チェックだけでも実現できます。
しかし、小数の場合は、0.0より大きく10.0未満、を読み替えるのは困難です。したがって、汎用の範囲チェッククラスを作るには、より大きい、未満を扱う必要があります。

ファウラー氏の「アナリシスパターン」では、範囲の上限と下限の値についてそれぞれ範囲に含めるか否かをbooleanで持つような記述をしています。

boolean で持つとしたら、Rangeの実装イメージは次のようになります。

public class Range<T exends Comparable> {
    private T lowerBound;
    private boolean withLowerBound;
    private T upperBound;
    private boolean withUpperBound;
    public Range(T lowerBound, boolean withLowerBound, T upperBound, boolean withUpperBound) {
        this.lowerBound = lowerBound;
        this.withLowerBound = withLowerBound;
        this.upperBound = upperBound;
        this.withUpperBound = withUpperBound;
    }
    public boolean includes(T value) {
        return (withLowerBound ? lowerBound.compareTo(value) <= 0 : lowerBound.compareTo(value) < 0) &&
               (withUpperBound ? value.compareTo(upperBound) <= 0 : value.compareTo(upperBound) < 0);
    }
}

利用イメージは次のようになります。

public class SpeedMeter {
    private static final Range<Integer> speedRange =
        new Range<Integer>(0, true, 180, true);
    ...

先に紹介したライブラリの中で、この問題をどう扱っているのか調べてみると、guavaでは対応していました。*3

数学的な区間の表現を用いると、以上、以下の範囲を閉区間といい、より大きい、未満の範囲を開区間といいます。guavaではRangeクラスのインスタンスを生成するときに、下限、上限がそれぞれ閉区間か開区間かで異なる名前のファクトリメソッドを用意しています。

Range<Integer> speedRange = Ranges.closed(0, 180);
Range<Integer> speedRange = Ranges.open(0, 180);
Range<Integer> speedRange = Ranges.closedOpen(0, 180);
Range<Integer> speedRange = Ranges.openClosed(0, 180);

また、範囲を下限または上限の片方だけ指定する、例えば100以上、というRangeインスタンスも生成可能でした。

Range<Integer> speedRange = Ranges.atLeast(100);
guavaのRangeクラスのソースコード調査

どうやって実現しているのか、guavaのコードを追ってみました。以下に、エラー処理ほかを省略したguavaのRangeクラス骨子を示します。

public final class Range<C extends Comparable> implements ... {
  final Cut<C> lowerBound;
  final Cut<C> upperBound;

  Range(Cut<C> lowerBound, Cut<C> upperBound) {
    this.lowerBound = lowerBound;
    this.upperBound = upperBound;
  }
  
  public boolean contains(C value) {
    return lowerBound.isLessThan(value) && !upperBound.isLessThan(value);
  }

  static int compareOrThrow(Comparable left, Comparable right) {
    return left.compareTo(right);
  }
}

下限値、上限値をCutクラスでラップしています。以下に、エラー処理ほかを省略したguavaのCutクラスの骨子を示します。

abstract class Cut<C extends Comparable> ... {
  final C endpoint;
  :
  abstract boolean isLessThan(C value);
  :
  static <C extends Comparable> Cut<C> belowValue(C endpoint) {
    return new BelowValue<C>(endpoint);
  }
  private static final class BelowValue<C extends Comparable> extends Cut<C> {
    BelowValue(C endpoint) {
      super(endpoint);
    }
    boolean isLessThan(C value) {
      return Range.compareOrThrow(endpoint, value) <= 0;
    }
    :
  }
  private static final class AboveValue<C extends Comparable> extends Cut<C> {
    AboveValue(C endpoint) {
      super(endpoint);
    }
    boolean isLessThan(C value) {
      return Range.compareOrThrow(endpoint, value) < 0;
    }
    :
  }
  :
}

Rangeのファクトリメソッドを持つguavaのRangesクラスの骨子は以下です。

public final class Ranges {
  ...
  public static <C extends Comparable<?>> Range<C> closedOpen(C lower, C upper) {
    return create(Cut.belowValue(lower), Cut.aboveValue(upper));
  }
  ...
}

なるほどー、ちょっと複雑ですが、うまく切り替えをしていますね。

*1:マジックナンバーを定数化するべきですが、今回は分かりやすさのためサボっています。

*2:第4章 4.3 範囲。また、ファウラー氏のWebサイト http://martinfowler.com/eaaDev/Range.html にも記述があります。

*3:2011年9月リリース予定のRelease 10で新規追加される機能です。com.google.common.collect.Rangeクラス他