LoginSignup
2
5

More than 5 years have passed since last update.

Java8 ラムダ式&Streamによるデザインパターン再考 - Null Object パターン -

Posted at

はじめに

このシリーズでは、Java8で導入されたラムダ式・Stream APIを利用した、デザインパターンの実装方法を検討していきます。
サンプルコードはGitHubにアップしています。

今回のパターン

前回の記事Null Objectパターンに言及しましたが、説明は割愛したので今回はこれを取り上げます。
Null Objectパターンとは、null参照の代わりに特定のインターフェースを実装するが、実際には何もしない(副作用を生じない)オブジェクトを参照することで、nullチェック不要で安全なコードを記述するテクニックです。

従来の実装

以下のインターフェースに対して処理を委譲するクラスを想定します。

Discount.java
public interface Discount {
    Integer calcDiscount(Integer totalAmount);
}

Dicsountの実装クラスのインスタンスへの参照はコンストラクタやsetterで与えられるとして、それを利用する際にはnullの場合を考慮して以下のようなnullチェックが必要となるでしょう。

OrderService.java(1)
    private Discount discount;

    public Integer getPrice(Integer amount) {
        if (discount != null) {
            amount -= discount.calcDiscount(amount);
        }
        return amount;
    }

もし複数の箇所でdiscount変数を使用する場合、いちいちnullチェックを行うのは面倒です。
そこで、以下のように何もしない(正確には、利用側の処理に影響を及ぼさないように処理をする)インスタンス、つまりNull Objectを設定します。
Null Objectは内部クラスとして定義したり、以下のように匿名内部クラスとすることも多いです。

OrderService.java(2)
    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を定義することもできます。

OrderService.java(3)
    private Discount discount3 = amount -> 0;

    public Integer getPrice3(Integer amount) {
        amount -= discount3.calcDiscount(amount);
        return amount;
    }

関数型のNull Object

以下のサンプルは、日付を操作するユーティリティの例です。

DateTimeBuilder.java
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)の翌月末日を求めています。

Usage
        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パターン導入の動機となります。

#composeメソッド
    private void compose(TemporalAdjuster next) {
        func = func.andThen(t -> next.adjustInto(t));
    }

というわけで、コンストラクタにおいてfunc何もしない関数を設定します。
Function<Temporal, Temporal>型が何もしないということの意味は、引数で受け取ったTemporal型のオブジェクトをそのまま返す(右から左へ受け流す)ということです。
それはラムダ式で書くとt -> tということですね。

NullObjectにあたる関数
    private Function<Temporal, Temporal> func;

    private DateTimeBuilder(LocalDateTime base) {
        this.base = base;
        func = t -> t;
    }

Function#identityは単位元

前述のラムダ式t -> tは、java.util.Functionインターフェースに定義されているstaticメソッドidentity()を使用した方がよいでしょう。

Function#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チェックコードを削減しコードの見通しをよくすることができそうです。

2
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
5