朝日ネット 技術者ブログ

朝日ネットのエンジニアによるリレーブログ。今、自分が一番気になるテーマで書きます。

オブジェクト指向を5年間理解できなかった人間がオブジェクト指向を説明する(第6回)

こんにちは。朝日ネット社員のjiweenです。

前回までで重要なパターンは説明し終わりました。

最終回となる今回では、初学者にとって必要性が低いと判断し後回しにしていたパターンに触れます。 GoFパターンのマニアでなければ、読み飛ばしていただいても問題ありません。

最後では連載のまとめを行います。

  • 第1回 はじめに, 概要
  • 第2回 結論
  • 第3回 Adapter, State, Strategy, Abstract Factory
  • 第4回 Template Method, Factory Method, Bridge, Proxy, Composite, Interpreter, Decorator
  • 第5回 Visitor, Observer, Iterator, Facade, Mediator
  • 第6回 Builder, Singleton, Prototype, Flyweight, Chain of Responsibility, Command, Memento, まとめ

目次(第6回)

残りのパターン

これまでに扱っていなかった、残りのパターンです。

Builder

Builder (建築者) パターンはオブジェクトの生成手順を別のクラスとして切り出すパターンです。このパターンのメリットは主に 読み書きしやすい構文になる ことであり、また後述するようにパターンの制限があるので、それほど積極的に使わないと思います。

※冗長なインターフェイスを省略しています

通常のコンストラクタによるオブジェクト生成では引数を一度に受け入れ、オブジェクトを構築します。

    object = new SomeObject(arg1, arg2, arg3);

Builderパターンでは構築に必要な指示を少しずつ受け入れ、オブジェクトの生成を要求される (build()) と構築物を返却します。

// HTTPリクエストの送信をBuilderパターンで実装するサンプルコード
// (実際にHTTPリクエストを送るためのコードは目指していません。あくまで題材です)

import java.util.stream.Collectors;
import java.util.ArrayList;

class Client {
    public static void main(String[] args) {
        HttpRequestBuilder builder =
          new HttpRequestBuilder("example.com", Method.POST);
        Json json = new Json("{ \"value\": 3 }");
        builder.setJsonBody(json);
        builder.addQueryString("foo", "bar");
        builder.addQueryString("hoge", "fuga");
        HttpRequest req = builder.build();
        HttpResponse res = req.sendSync();
        // ...
    }
}

class HttpRequestBuilder {
    private String host;
    private int port;
    private byte[] body;
    private ArrayList<QueryParam> queryParams;
    private Method method;

    public HttpRequestBuilder(String host, Method method) {
        this.host = host;
        this.method = method;
        port = 80;
        body = new byte[] {};
        queryParams = new ArrayList<>();
    }
    public void setPort(int port) {
        this.port = port;
    }
    public void setJsonBody(Json json) {
        body = json.decode();
    }
    public void addQueryString(String key, String value) {
        queryParams.add(new QueryParam(key, value));
    }
    public HttpRequest build() {
        String queryString = this.queryParams.stream()
          .map(param -> param.key + "=" + param.value)
          .collect(Collectors.joining("&"));
        String url = "http://"
          + this.host + ":" + this.port
          + "?" + queryString;
        return new HttpRequest(url, method, body);
    }
}

class HttpRequest {
    private String url;
    private Method method;
    private byte[] body;
    // ...
    public HttpRequest(String url, Method method, byte[] body) {
        this.url = url;
        this.method = method;
        this.body = body;
    }
    public HttpResponse sendSync() {
        System.out.println("Sending request...: " + method + " " + url);
        return new HttpResponse(); // TODO
    }
}

class Json {
    public Json(String str) {
        // TODO
    }
    public byte[] decode() {
        return new byte[] {}; // TODO
    }
}


class QueryParam {
    public String key;
    public String value;
    public QueryParam(String key, String value) {
        this.key = key;
        this.value = value;
    }
}

class HttpResponse {
    //TODO
}

enum Method {
    GET,
    POST,
    PUT,
    DELETE,
    // ...
}

ちょっとしたテクニックとして、BuilderのメソッドにBuilder自身を返却させることでメソッドチェインスタイルの書き方ができます。(Builderパターンに限らずたまに使われるテクニックです)

    // void の代わりに Builder 自身を返す
    public HttpRequestBuilder setPort(int port) {
        this.port = port;
        return this;
    }
        // メソッドチェインで書けてちょっと楽
        builder
            .setMethod(Method.POST)
            .setJsonBody(json)
            .addQueryString("foo", "bar")
            .addQueryString("hoge", "fuga");

Builderパターンには以下のようなメリットがあります。

  • (オプション引数の無い言語では) オプション引数の多いオブジェクト生成が簡単に書ける。
  • (名前付き引数の無い言語では) コンストラクタで値を設定する場合と比べ、名前の付いたメソッドで値を設定できるので分かりやすくなる。

つまり、Builderパターンは構文的な改善が主なメリットです。 特にオプション引数を便利に渡せるようにするためによく使われます。

パターンの制限

デメリット...というか、このパターンの限界は、必須の値設定や生成手順は結局コンストラクタに書くしかなく (型保証を捨てたくなければ)、パターンに切り出せないということです。

    // 例えば、必須の引数であるmethodをbuilderのメソッドで渡すようにすると...
    
    public HttpRequestBuilder(String host) {
        this.host = host;
        this.port = 80;
        this.method = null;
        body = new byte[] {};
        queryParams = new ArrayList<>();
    }

    public HttpRequestBuilder setMethod(Method method) {
        this.method = method;
        return this;
    }
        // methodを設定し忘れてnullになっても、コンパイルエラーにならない!

        builder
            .setJsonBody(json);
            .addQueryString("foo", "bar");
            .addQueryString("hoge", "fuga");
        // (methodのデフォルト値を決めてオプション引数にすれば問題ありませんが、
        //  一般にデザインパターンのために要件を変更したくありません)

要するに、コンパイラの支援を捨てずにBuilderパターンが使えるのはオプショナルな生成手順に対してだけです。 method のような必須引数の設定をbuilderに切り出すことはできません。

これは、Builderパターンが名前付き引数の代替にならないことを意味します。

名前付き引数を実現する方針としては、引数用のオブジェクトを定義し、key-valueペアで初期化することが考えられます (言語にそのような構文があれば) 。Javaで似たことをするには、匿名クラスを使った方法があります。

class HttpRequestBuilder {
    
    // ...

    // static nested class
    static class Arg {
        public String host;
        public Method method;
    }

    public HttpRequestBuilder(Arg arg) {
        host = arg.host;
        port = 80;
        method = arg.method;
        body = new byte[] {};
        queryParams = new ArrayList<>();
    }

    // ...
}

class Client {
    public static void main(String[] args) {
        HttpRequestBuilder builder = new HttpRequestBuilder(
            // 少しトリッキーな書き方ですが、匿名クラスと初期化ブロック
            // という機能を使っています。
            new HttpRequestBuilder.Arg() {{
                host = "example.com";
                method = Method.POST;
            }
        });
        // ...
    }
}

あるいは、もしかしたらあなたがやりたいのは DI (依存性の注入) ツールを使ってコンストラクタをスッキリさせることかもしれません。DIについてはSingletonパターンの章で少し解説します。

余談: 型レベルの保証

JavaプログラマがBuilderパターンに本当に望むのは、必須の値設定などもBuilderのメソッドとして書けて、設定し忘れた値があったらコンパイルエラーにできることです。 しかし、これはJavaではできません。

全てのエラーケースに対して型を対応させられれば実現できるのですが、これは「何の値が設定済みか」の組み合わせに対して一つ一つ手作業でクラスを定義することを意味するので非現実的です (もしかしたら何か高度なhackがあるかもしれませんが)。

ちなみに、Rustくらい強い型システムになると解決策があるようです 1 (パターンとして使うには少し複雑ですが)。

余談: HaskellにおけるBuilderパターン?

値の構築は関数型言語でも共通する話題です。

と言ってもHaskellではJavaとは色々と前提が異なるので比較するのが難しいのですが、値の構築手順を別の型に切り出す、という意味でBuilderパターンと似た設計が出てくることがあります。

Text.Builder

ただ、このBuilderはBuilderパターンのように構文を改善するものではなくパフォーマンス (処理効率) のためのものです。 完成形の値と構築途中の値を違う型にすることで、実装を分け、それぞれ処理を最適化しています。

また、オプショナルな設定を後から行うという意味でBuilderパターンに似た設計もあります。

Network.HTTP.Client #setQueryString

これはHTTPリクエストに関するモジュールです。

このモジュールでは、parseRequest 関数などでHTTPリクエストデータ Request を簡単に初期化した後、ヘッダ、ボディ、リクエストタイムアウトなどのオプションを後から設定できます。 例えば、 setQueryString 関数はクエリパラメータを追加します。

Singleton

Singleton (一枚札) パターンはあるクラスのオブジェクト (インスタンス) が多くとも一つしか存在しないことを保証するパターンです。ただし、Singletonパターンは現代では推奨されません。問題点と代替手法については後述します。

Singletonパターンは元々設計のパターンというより実装テクニックのようなものです。

class Singleton {
    static private Singleton instance;
    static public Singleton Instance() {
        if (instance == null) instance = new Singleton();
        return instance;
    }
    
    private String value;
    public Singleton() {
        value = "foo";
    }
    public void say() {
        System.out.println(value);
    }
}

class Client {
    static public void main(String[] args) {
        for(int i = 0; i < 10; i++) {
            someMethod();
        }
    }
    static public void someMethod() {
        // ...
        
        // 何回実行されても、Singletoのインスタンスは常に同じ
        Singleton.Instance().say();
        
        // ...
    }
}

上図のSingletonクラスのオブジェクトは常に1つしか存在しません。コンストラクタは非公開にされており、Singletonクラスのオブジェクトを取得する場合は必ず静的メソッドInstanceを呼び出す必要があります。常に1つしか存在しないオブジェクトは静的フィールドinstanceとして保持されており、Instanceは既にinstanceが生成されていればそれを返却し、まだ生成されていなかったらinstanceを初期化して返却します。

Singletonパターンの問題点は以下です。

  • 分散・並列処理の文脈ではSingletonパターンをそのまま使うことはできないので、あまり汎用なパターンではない。
    • 上記のコードは並列処理に対応していません
    • 並列処理の場合、どの単位で「1つのインスタンス」にしたいのかによって実装を変更する必要があります
  • グローバル変数と全く同じデメリットがある 。Singletonへの依存性が隠されてしまう。
    • Singletonに依存したクラスの外側から、その依存性を動的に取り替えることができない。クラスの流動性が失われたり、テストがしづらくなったりする。
    • 暗黙的な依存が容易に増やせてしまう。Singletonが他の大量のクラスから依存されていても、その依存性がクラスレベルのコードに現れることはない。結果として、気づかないうちに流動的要素が大量のクラスにまたがってしまう可能性がある。

特に2つ目の問題は古典的なオブジェクト指向の抱える問題であり、これを解決するためのDI (依存性の注入) という概念があります。

依存性の注入 (DI, Dependency Injection) について

Singletonの問題点として「依存性が隠されてしまうこと」を挙げました。Singletonに限らず、 (staticメソッドを用いた) オブジェクトの生成はこのような依存性を生み出しやすく、大規模なプロダクトでは問題になりがちです。この問題を解決する方針は、依存性をオブジェクトに対して明示的に注入する ことです。これを ”依存性の注入 (DI, Dependency Injection) ” と呼び、最も簡単な手法として、単に依存性をオブジェクトとしてコンストラクタに渡すというものが考えられます (Constructor Injection) 。これによって依存性の取り替えも容易に行えますし依存性がコード上に明示されます。

しかし constructor injection はまだ問題を残しています。

  • 単にオブジェクトを横流しするだけのコードが増える。書くのが面倒だし、コードの変更もしづらくなる (流動性が下がる)。

JavaにはDIを行う様々なライブラリ・フレームワークがあります。どれを使えばいいか迷うかもしれませんが、Constructor Injectionからそれらのライブラリを使ったコードに書き換えるのは難しくありません。まだ不慣れな方は 迷ったらとりあえず Constructor Injection で済ませておく ことをおすすめします。

余談: DIをする色々な方法

DIに興味がある人向けの余談です。私が知っている範囲で、色々なDIの実現方法を紹介します。

  • Javaでは、新しい順に Dagger, Guice, Spring といったDIコンテナ (DIライブラリ) があります。DIだけが目的ならDaggerかGuiceを使用すると良いと思います。Daggerは軽量で、Guiceは優れたエルゴノミクスを持つようです。
  • Haskell (純粋関数型言語) では Reader (T) モナド / Reader effect がDIに使われます。関数型言語の初学者からするとモナドやeffectというのは少々とっつきにくい概念ですが、DIに限定された概念ではなく、関数型言語において手続き全般の抽象化に使われる汎用的な概念です。
  • Scala (オブジェクト指向と統合された関数型言語) は、伝統的なオブジェクト指向や命令型スタイルとの互換性を保ちながらもそれなりに強い型システムを備えています。Scala言語のコミュニティでは、Cake Pattern / Minimal Cake PatternというDI手法が生み出されました。また、HaskellのReaderモナドをDI手法として輸入する文化もあるようです 2

Prototype

Prototype (原型) パターンはオブジェクトを典型的なオブジェクト (原型) のセルフコピーとして生成するパターンです。セルフコピーをオブジェクトの生成手段として見ると、Abstract FactoryパターンにおいてFactoryとFactoryの生成するオブジェクトの型が一致したバージョンとみなすことができます。オブジェクト生成をPrototypeパターンで実装すると、 Abstract Factory より単純なクラス構造になります。

ただし、一般に可変で複合的なオブジェクトのコピーは複雑であり、shallow copy, deep copy, どちらでもない色々なコピー といった選択肢があります。コピーという概念を使った分だけ複雑性が増えるので、Prototypeパターンを使うのはあまりおすすめしません。

Flyweight

Flyweight (超軽量級) パターンは共有可能なオブジェクトを適切にプーリングすることでメモリを節約するパターンです。オブジェクト指向設計の実践というよりは、メモリを節約するありふれたテクニックです。クラス構造の設計という意味では、プール管理用のクラスを作る以外特に何もしません。

Chain of Responsibility

Chain of Responsibility (責任の連鎖) パターンは、処理の移譲先 (ディスパッチ先) が見つかるまで責任をたらい回しするパターンです。移譲先を一箇所で計算する代わりに、各処理担当に移譲可能かどうか尋ね、初めに見つかったところを移譲先とします。この考え方はオブジェクト指向とは直接関係ありませんが、一般に処理の分岐を設計する時に便利なことがあります。

Command

Command (命令) パターンは命令をオブジェクトとして扱うことで命令履歴の管理や命令の結合などを可能にするパターンです。このパターン自体に目新しい構造はないため省略します。

Memento

Memento (形見) パターンはオブジェクトを以前の状態に戻すためのオブジェクト (バックアップ、形見) を提供し、ロールバックを可能にするパターンです。ロールバックはオブジェクトの内部状態に強く依存する処理です。バックアップ処理をカプセル化することで強い依存関係がパターンの外に漏れ出さないようにします。

このパターンはそれほど頻出するわけではないので、気になった時に詳細を調べれば十分でしょう。個人的にはMementoという命名がかっこいいと思います。

なお、内部状態に関わる処理をなるべくカプセル化する、という発想はバックアップ以外にも活かすことができます。状態は代表的な流動的要素です。

まとめ

長い連載にお付き合いいただきありがとうございました。

この連載では、オブジェクト指向の理解の助けとなることを目指して、なるべく一貫した視点でGoFデザインパターンを解説しました。

具体的には、パターンの解説にあたって主に流動性 (variability) という概念に着目し、以下の原則を仮定しました。

  • 流動性を判別できる場合、単一の流動的要素を非流動的要素によってカプセル化する
  • 流動的要素のカプセル化には関連とインターフェイスを積極的に使う
  • データと振る舞いの分離は禁止されない

この連載を通して伝えたかったことの1つは、オブジェクト指向ではデータや振る舞いといったコードに現れやすい概念だけに気を取られがちですが、流動性に着目すると比較的理解しやすいということです。

しかしそれでもなお簡単ではないと、ここまでの執筆を通して感じました。 オブジェクト指向は慣習の多面的な集まりという性質も強く、理論的な説明が合わない部分がありました。 理論的な整合性を重視した結果、一部の説明が必要以上に回りくどくなってしまったと反省しています。

一方で、流動性がオブジェクト指向設計の色々な場面で現れる、ということはそれなりにうまく説明できたのではないかと感じています。

世の中は難しいことばかりですが、この連載がオブジェクト指向に関する悩みを少しでも減らせれば幸いです。

採用情報

朝日ネットでは新卒採用・キャリア採用を行っております。

新卒採用 キャリア採用|株式会社朝日ネット


  1. https://keens.github.io/blog/2017/02/09/rustnochottoyarisuginabuilderpata_n/. なお、HaskellではHKD (Higher-Kinded Types) を使うことでもう少し洗練できそうです。ただ、私はHaskellを書いていてBuilderパターンそのものが欲しくなったことはありませんし、それと同じ理由であまり議論されていない話題だと思います。
  2. https://cyberagent.ai/blog/tech/scala/3299/