はじめに
このシリーズでは、Java8で導入されたラムダ式・Stream APIを利用した、デザインパターンの実装方法を検討していきます。
サンプルコードはGitHubにアップしています。
今回のパターン
前回の記事でNull Object
パターンに言及しましたが、説明は割愛したので今回はこれを取り上げます。
Null Object
パターンとは、null参照の代わりに特定のインターフェースを実装するが、実際には何もしない(副作用を生じない)
オブジェクトを参照することで、nullチェック不要で安全なコードを記述するテクニックです。
従来の実装
以下のインターフェースに対して処理を委譲するクラスを想定します。
public interface Discount {
Integer calcDiscount(Integer totalAmount);
}
Dicsount
の実装クラスのインスタンスへの参照はコンストラクタやsetterで与えられるとして、それを利用する際にはnullの場合を考慮して以下のようなnullチェックが必要となるでしょう。
private Discount discount;
public Integer getPrice(Integer amount) {
if (discount != null) {
amount -= discount.calcDiscount(amount);
}
return amount;
}
もし複数の箇所でdiscount
変数を使用する場合、いちいちnullチェックを行うのは面倒です。
そこで、以下のように何もしない(正確には、利用側の処理に影響を及ぼさないように処理をする)インスタンス、つまりNull Object
を設定します。
Null Object
は内部クラスとして定義したり、以下のように匿名内部クラスとすることも多いです。
private Discount discount2 = new Discount() {
@Override
public Integer calcDiscount(Integer totalAmount) {
return 0;
}
};
public Integer getPrice2(Integer amount) {
amount -= discount2.calcDiscount(amount);
return amount;
}
ところでDiscount
インターフェースには@FunctionalInterface
は付けていませんが、メソッドがひとつのみのインターフェースなので関数型インターフェースとして扱うことができます。
つまり、以下のようにラムダ式を用いてNull Object
を定義することもできます。
private Discount discount3 = amount -> 0;
public Integer getPrice3(Integer amount) {
amount -= discount3.calcDiscount(amount);
return amount;
}
関数型のNull Object
以下のサンプルは、日付を操作するユーティリティの例です。
public class DateTimeBuilder {
private LocalDateTime base;
private Function<Temporal, Temporal> func;
private DateTimeBuilder(LocalDateTime base) {
this.base = base;
func = t -> t;
}
private void compose(TemporalAdjuster next) {
func = func.andThen(t -> next.adjustInto(t));
}
public DateTimeBuilder nextMonth() {
compose(t -> t.plus(1, ChronoUnit.MONTHS));
return this;
}
public DateTimeBuilder lastDay() {
compose(TemporalAdjusters.lastDayOfMonth());
return this;
}
public LocalDateTime get() {
return (LocalDateTime) func.apply(base);
}
public static DateTimeBuilder of(LocalDateTime base) {
DateTimeBuilder builder = new DateTimeBuilder(base);
return builder;
}
}
使用例は以下のようになります。以下のコードでは、引数で与えた日付(2017/1/17
)の翌月末日を求めています。
LocalDateTime base = LocalDateTime.of(2017, 1, 17, 0, 0);
LocalDateTime dt = DateTimeBuilder.of(base)
.nextMonth()
.lastDay()
.get();
assertEquals(LocalDateTime.of(2017, 2, 28, 0, 0), dt);
DateTimeBuilder
では、nextMonth()
やlastDay()
が呼ばれる度に該当する日付の変換処理を行う関数をインスタンス変数func
へ合成していき、最終的にget()
が呼び出されたタイミングで合成関数を適用しています。
合成を行っているのは以下のコードですが、この時、func
がnullだった場合にNullPointerException
が発生しないようにする…という考慮を不要にしたいというのがNull Object
パターン導入の動機となります。
private void compose(TemporalAdjuster next) {
func = func.andThen(t -> next.adjustInto(t));
}
というわけで、コンストラクタにおいてfunc
に何もしない関数
を設定します。
Function<Temporal, Temporal>
型が何もしないということの意味は、引数で受け取ったTemporal
型のオブジェクトをそのまま返す(右から左へ受け流す)ということです。
それはラムダ式で書くとt -> t
ということですね。
private Function<Temporal, Temporal> func;
private DateTimeBuilder(LocalDateTime base) {
this.base = base;
func = t -> t;
}
Function#identity
は単位元
前述のラムダ式t -> t
は、java.util.Function
インターフェースに定義されているstaticメソッドidentity()
を使用した方がよいでしょう。
private DateTimeBuilder(LocalDateTime base) {
this.base = base;
//func = t -> t;
func = Function.identity();
}
Function#identity()
は何もしない関数、つまり入力引数をそのまま返却する関数を返します。この関数は、別の変換関数(g
とします)に対して左から合成しても、右から合成してもg
と等しい結果を返す合成関数を得ることができます。
これは数学的に考えると、Function<Temporal, Temporal>
という系における単位元
であると言えますね。
実際に単位元
の英語訳は、identity element
だそうです。
まとめ
関数の合成を行う際は、Function#identity()
を単位元として初期値に使うと、煩雑なnullチェックコードを削減しコードの見通しをよくすることができそうです。