RAKUS Developers Blog | ラクス エンジニアブログ

株式会社ラクスのITエンジニアによる技術ブログです。

GoFの次に覚えたいデザインパターン ~Null Objectパターン~

楽楽精算開発部の id:smdr9p です。主に Java を使ったサーバーサイドを担当しています。

前置き

GoFデザインパターンはご存知でしょうか。

ご存知の方も多いかと思いますが簡単に説明すると、GoFデザインパターンとは Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides の4人、通称 Gang of Four、略称 GoF によって書かれた書籍、Design Patterns: Elements of Reusable Object-Oriented Software(邦題:オブジェクト指向における再利用のためのデザインパターン)に掲載されている23のデザインパターンのことです。GoF パターンや単に GoF と呼ばれることもあります。(この記事では以降は GoF パターンと呼びます。)

これらは本のタイトルにも示されているとおりオブジェクト指向プログラミングの領域におけるデザインパターンをまとめたものですが、この本、そしてこれに掲載されたデザインパターンがあまりにも有名であるため、単に「デザインパターン」と言った場合にこの GoF パターンを思い浮かべる人も数多くいるほどです。
GoF パターンのうち、特に有用性の高いものは言語仕様自体に取り込まれたり、メジャーなミドルウェアでサポートされたりしているものもあります。

このように GoF パターンは多くの人々によって長年活用され続けてきているものの、デザインパターンはこれだけ知っていれば十分、というわけにはいきません。なにしろ GoF パターンは書籍が発行されたのが 1994 年、JavaPHP もリリースされる前であり、それからオブジェクト指向プログラミング界隈にも様々な知見が蓄積され、新たな常識やベストプラクティスが作られてきました。
デザインパターンについても新たなパターンの発見や既存のパターンの再評価、ブラッシュアップがなされています。
そこで、それらの中から個人的によく見る、または使いやすいパターンを何回かにわけていくつか紹介したいと思います。

Null Objectパターン

今回は Null Objectパターンです。

このパターンを語弊を恐れずに簡単に言うと

null をオブジェクトとして扱う

です。
これにより null をポリモーフィズムに組み込むことができ、null に関するさまざまな面倒や障害を避けることができます。

このパターンでは、実際のオブジェクトと同じインターフェースを持つ null オブジェクトを作成します。このオブジェクトは null 値の代わりに使用され、null 値のときに期待される振る舞いをします。これにより、今まで null チェックを行って null の場合はスキップ、null でない場合はオブジェクトのメソッドを実行、のような処理をしていた場面で、null チェックなしにオブジェクトのメソッドを実行することができるようになります。

パターン適用前

以下のありがちなコードを Null Object パターンを使って改善してみます。

/** インターフェース */
public interface Shape {
    void draw();
    int sumOfInteriorAngles();
}

/** 実装クラス1 */
public class Triangle implements Shape {
    public void draw() {
        System.out.println("△");
    }
    public int sumOfInteriorAngles() {
        return 180;
    }
}

/** 実装クラス2 */
public class Square implements Shape {
    public void draw() {
        System.out.println("□");
    }
    public int sumOfInteriorAngles() {
        return 360;
    }
}

/** Factoryクラス */
public class ShapeFactory {
    public static Shape createPolygon(int numOfCorners) {
        return switch (numOfCorners) {
            case 3 -> new Triangle();
            case 4 -> new Square();
            default -> null;
        }
    }
}

図形を表す Shapeインターフェースの実装として、Circleクラスと Squareクラスがあります。
図形を描画する draw()メソッドに加え、図形の内角の和を取得する sumOfInteriorAngles()メソッドも持っています。
またそれらの実装クラスを生成する ShapeFactoryクラスもあります。角の数を指定するとそれに応じた図形クラスのインスタンスを返してくれますが、対応する実装がない場合は null を返します。

そして以下がそれらのクラスを利用する利用側のクラスです。

/** 利用側クラス */
public class Main {
    public void drawAll(List<Shape> shapes) {
        for (Shape shape : shapes) {
            if (shape == null) {  // nullチェック
                continue;
            }
            shape.draw();
        }
    }

    public int totalInteriorAngles(List<Shape> shapes) {
        return shapes.stream()
            .mapToInt(Shape::sumOfInteriorAngles)
            .sum();
    }
}

図形リストの shapes を渡してリストの図形を描画したり、リスト全体の内角の総和を取得できたりします。
ここで渡されるリストの生成方法については記載していませんが、リストの要素はすべて ShapeFactoryクラスによって生成されたものが入っていると考えてください。
上記のとおり ShapeFactoryクラスは null を返すことがあるのでリストには null が含まれている可能性があります。リストから取り出した Shape の実装クラスのオブジェクトはしっかり null チェックして、「null の場合は何もしない」ようにしています。

…のつもりだったのですが、実は一箇所 null チェックを忘れている箇所がありました。

    public int totalInteriorAngles(List<Shape> shapes) {
        return shapes.stream()
            .mapToInt(Shape::sumOfInteriorAngles)
            .sum();
    }

このメソッドに渡されるリストにも null が含まれる可能性がありますので、ここでも null チェックが必要でした。

    public int totalInteriorAngles(List<Shape> shapes) {
        return shapes.stream()
            .filter(Objects::nonNull)  // ここも要nullチェックでした
            .mapToInt(Shape::sumOfInteriorAngles)
            .sum();
    }

このケースではここだけの修正で済みましたが、他の Shape を参照している箇所で同様のチェック漏れがないか確認する必要がありそうです。また、Shape に新たなふるまいが追加された場合にも継続して注意していく必要がありますし、Shape 以外のクラスでも類似の例がないか確認する必要もあるかもしれません。

このように、null を null のまま使い続ける限り、常に null を意識してコードを書いていく必要があります。

しかも null チェックを忘れていたとしてもコンパイラはエラーを吐いてはくれず、実行時に条件が合った場合に初めて NullPointerException が発生するのも頭の痛いところです。テストコードでチェックするとしてもテストケースから抜けていたら同じことです。

Null Objectパターンを適用してみる

このような場合に Null Objectパターンが役立ちます。このパターンを適用することで、利用側で null チェックが不要になり、コードがシンプルで読みやすくなるだけでなく、今後の機能追加や変更に対しても安全性が向上します。

それでは、上記の実装に Null Objectパターンを適用してみましょう。
Null Objectパターンでは、null の場合に適用されるクラスを作成し、いままで各利用箇所に書かれていた「null の場合のふるまい」をあらかじめ持たせておきます。
Shape であれば draw() のときは文字どおり「何もしない」ふるまいをします。sumOfInterirorAngles() であれば「何もない」数値である0を返すふるまいをします。

/** Null Object実装クラス */
public class NullShape implements Shape {
    public void draw() {
        // 何もしない
    }
    public int sumOfInteriorAngles() {
        return 0;  // 何もない
    }
}

そして、Factoryクラスは適切な Shape の実装クラスのインスタンスを生成できない場合、Null Object である NullShapeインスタンスを返すようにします。

/** Null Object対応 Factoryクラス */
public class ShapeFactory {
    public static Shape createPolygon(int numOfCorners) {
        return switch (numOfCorners) {
            case 3 -> new Triangle();
            case 4 -> new Square();
            default -> new NullShape();  // ここが変わった
        }
    }
}

ここでは簡易的に都度 new していますが、可能であれば Singletonパターンで NullShapeインスタンスは1つのみ生成されるようにしておくのが望ましいです。

これによりリストには null の代わりにこの NullShapeオブジェクトが入るようになりますので、リストから取り出した Shape の null チェックは不要になります。

/** Null Objectの恩恵を受けた利用側クラス */
public class Main {
    public void drawAll(List<Shape> shapes) {
        for (Shape shape : shapes) {
            // if (shape == null) {  // nullチェック不要!
            //     continue;
            // }
            shape.draw();
        }
    }

    public int countCorners(List<Shape> shapes) {
        return shapes.stream()
            // .filter(Objects::nonNull)  // ここもnullチェック不要!
            .mapToInt(Shape::numOfCorners)
            .sum();
    }
}

もちろんこの変更によってエラーは出ませんし、出力結果も変わりません。

いままでは利用側のコードで「オブジェクトが null であれば何もしないようにする、null でなければオブジェクトに命令する」判断が必要でした。
しかし Null Objectパターンを適用したコードでは、利用側は「オブジェクトに命令する」のみ でOKです。オブジェクトが Null Object でなければ今までと同じふるまいをしてくれますし、Null Object であれば「何もしない」をしてくれます

このように null を NullShape として Shape の派生とすることで、null を特別扱いすることなく他のクラスと同等の扱いができるようになります。将来的に Shape を利用するコードが追加されたとしても null に関する問題が発生する可能性を軽減できるでしょう。

まとめ

このパターンは、null チェックが頻繁に行われるプログラムや、null が意図しないエラーやバグの原因となりやすい場面で特に有用です。
Null Objectパターンは null すらオブジェクトにすることによってポリモーフィズムの活用範囲を広げる、非常にオブジェクト指向らしいパターンだと思います。

あなたの既存のコードで Null Objectパターンが活用できる場所がないか、ぜひ探してみてください。

関連するデザインパターン

Null Objectパターンに関連、あるいは活用できる他のデザインパターンをいくつか紹介します。

Strategyパターン

Strategyパターンはアルゴリズムや実装を実行時に柔軟に切り替えることができるようにするパターンです。Strategy として何もしたくないケースがある場合に Null Object で Strategy を実装する事が可能です。開発やテストのときにログが出力されないようにするために NullLogger のようなクラス使ったことがあるかもしれませんが、それがまさに Null Object です。

Stateパターン

Stateパターンはオブジェクトの内部状態にもとづいてオブジェクトのふるまいを変更するパターンです。State が何もない状態のときのふるまいを Null Object に実装することが可能です。初期化前などの状態に null を使用しているのであればそれを Null Object にしてみるといいかもしれません。

Compositeパターン

Compositeパターンは階層構造を表現するパターンで、容器と中身を統一的に扱えるようにします。中身が何もないケースを Null Object で実装することで、容器の中身が何もない場合も変わらず統一的な操作を行うことが可能になります。

参考文献

Copyright © RAKUS Co., Ltd. All rights reserved.