Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails以外の開発一般

[保存版]人間が読んで理解できるデザインパターン解説#1: 作成系(翻訳)

こんにちは、hachi8833です。今回から3回に渡って『Design Patterns for Humans』の日本語訳を公開します。あえてクラス図などを使わず、デザインパターンをストーリーで理解できるように書かれた異色のデザインパターン解説です。

概要

原著者の許諾を得て、MITライセンスに基づき翻訳・公開いたします。

「Design Patterns for Humans」は商標(TM)です。

  • 2017/10/12: 初版公開
  • 2020/12/03: 細部を更新

人間が読んで理解できるデザインパターン解説#1: 作成系(翻訳)

Design Patterns For Humans


🎉 究極にシンプルなデザインパターン解説! 🎉

誰もがつい心躍ってしまうタイトルにしてみました。タイトルに負けないよう、本記事ではこれ以上不可能なまでにシンプルな方法にこだわってデザインパターンを解説しています。


本ガイドを気に入っていただいた方はコーヒー一杯おごってくださいまし
他の記事も読んでみたい方は、ぜひHugobotsのご購読をお願いいたします。


はじめに🚀

デザインパターンとは、常に繰り返される問題を解決するためのものであり「特定の問題に取り組む方法のガイドライン」です。デザインパターンはクラスではありませんし、アプリに追加するだけで奇跡を起こしてくれるようなパッケージやライブラリでもありません。デザインパターンはそうしたものではなく、特定の状況で特定の問題に取り組む方法を示すガイドラインであるとご理解ください。

デザインパターンとは、常に繰り返される問題を解決するためのものであり「特定の問題に取り組む方法のガイドライン」です。

Wikipediaには次のように書かれています。

ソフトウェアエンジニアリングにおけるデザインパターンとは、ソフトウェア設計上の特定コンテキストにおいてよく発生する問題に対する「一般的かつ再利用可能な問題解決法」である。デザインパターンは、ソースコードやマシンコードに直接置換えられるような最終設計ではない。デザインパターンは、さまざまな状況に適用可能な問題解決法の記述、またはテンプレートである。

ご注意⚠️

  • デザインパターンは、あらゆる問題を解決する「銀の弾丸」ではありません
  • デザインパターンを強制してはなりません。無理にデザインパターンを適用すれば良くない結果が生じることでしょう。デザインパターンは、問題が起こってからそれ解決するものであって、問題をあら捜しして解決するものではありません。デザインパターンに過剰な期待を持たないことです。
  • 適切な場所で適切に用いられたデザインパターンは、救いの神になるでしょう。そうでないデザインパターンは荒れ狂い、コードをめちゃめちゃにしてしまうことでしょう。

追伸: 以下のコードサンプルではPHP-7を使っていますが、コンセプトはどの言語でも同じなので、どうかページを閉じないでください。ついでながら、他の言語のサポートについては現在作業中です

デザインパターンの種別

#1 「作成系」デザインパターン

わかりやすくまとめるとこうです。

作成系パターンとは、もっぱらオブジェクトのインスタンス化や、関連するオブジェクトのグループ化を行うパターンです。

Wikipediaではこうです。

ソフトウェアエンジニアリングにおける「作成系」デザインパターンとは、オブジェクト作成メカニズムを扱うデザインパターンであり、状況に即した方法でオブジェクトを作成しようとするものである。オブジェクト作成の基本フォームは、ともすると設計上の問題を引き起こしたり、設計が複雑になってしまうことがある。作成系デザインパターンは、オブジェクト作成を何らかの方法で制御することによってこの問題を解決する。

Simple Factoryパターン🏠

現実世界になぞらえるとこうです。

家造りでドアが必要になったときを考えてみましょう。家の中でドアが必要になるたびにいちいち作業着に着替えてドアを一からこしらえていたのでは煩雑になるばかりです。そんなことをするより、既製品のドアを工場(factory)から運んでくるのが普通でしょう。

わかりやすくまとめるとこうです。

simple factoryパターンは、クライアントが必要とするインスタンスを単に生成するものです。このとき、インスタンス生成ロジックはクライアントから見えないようにうまく隠しておきます。

Wikipediaではこうです。

オブジェクト指向プログラミング(OOP)におけるfactoryとは、他のオブジェクトを作成するオブジェクトのことである。元々factoryは、メソッド呼び出しからさまざまなプロトタイプやクラスのオブジェクトを返す関数やメソッド(newを前提)のことを指していた。

プログラム例

最初に、ドア(Door)のインターフェイスと実装を記述します。

interface Door
{
    public function getWidth(): float;
    public function getHeight(): float;
}

class WoodenDoor implements Door
{
    protected $width;
    protected $height;

    public function __construct(float $width, float $height)
    {
        $this->width = $width;
        $this->height = $height;
    }

    public function getWidth(): float
    {
        return $this->width;
    }

    public function getHeight(): float
    {
        return $this->height;
    }
}

次に、ドアのfactoryを記述します。このfactoryはドアを作成して返します。

class DoorFactory
{
    public static function makeDoor($width, $height): Door
    {
        return new WoodenDoor($width, $height);
    }
}

ここまでできたら、次のように使います。

$door = DoorFactory::makeDoor(100, 200);
echo 'Width: ' . $door->getWidth();
echo 'Height: ' . $door->getHeight();

使いみち

オブジェクトを作成するときに、引数を渡す他にロジックも少々加えたいのであれば、そうしたコードをあちこちで繰り返し書くよりも、それ専用のfactoryを作ってそこに入れておく方が合理的です。

Factory Methodパターン🏭

現実世界になぞらえるとこうです。

ある採用担当の管理職(hiring manager)を題材にして考えてみましょう。一般に、面接官(interviewer)があらゆる職種(開発、営業、経理など)向けの面接をひとりですべてこなすのは不可能です。欠員の生じた職種によっては、面接を別の人に委任(delegate)しなければならないでしょう。

わかりやすくまとめるとこうです。

factory methodは、インスタンス化のロジックを子クラスに委譲する手段を提供するものです。

Wikipediaではこうです。

クラスベースのプログラミングにおけるfactory methodパターンは、オブジェクト作成時に、作成するオブジェクトのクラスを厳密に指定しないで済むようにしたいという問題にfactory methodを使う。問題解決は、factory method呼び出しでオブジェクトを作成することで行われる。方法は2とおり: factory methodをインターフェイスで指定し実装は子クラスで行うか、ベース(親や先祖)クラスに実装し、必要なら派生クラスでオーバーライドする。コンストラクタの呼び出しでは行わない。

プログラム例

面接の例を使うことにします。最初に面接官(Interviewer)のインターフェイスと実装を少々記述します。

interface Interviewer
{
    public function askQuestions();
}

class Developer implements Interviewer
{
    public function askQuestions()
    {
        echo 'デザインパターンについて尋ねる';
    }
}

class CommunityExecutive implements Interviewer
{
    public function askQuestions()
    {
        echo 'コミュニティ育成について尋ねる';
    }
}

続いて採用担当の管理職(HiringManager)を記述します。

abstract class HiringManager
{

    // Factory method
    abstract protected function makeInterviewer(): Interviewer;

    public function takeInterview()
    {
        $interviewer = $this->makeInterviewer();
        $interviewer->askQuestions();
    }
}

これで、どの子クラスも拡張可能になり、必要な面接官を提供できるようになります。

class DevelopmentManager extends HiringManager
{
    protected function makeInterviewer(): Interviewer
    {
        return new Developer();
    }
}

class MarketingManager extends HiringManager
{
    protected function makeInterviewer(): Interviewer
    {
        return new CommunityExecutive();
    }
}

これで、次のように使うことができます。

$devManager = new DevelopmentManager();
$devManager->takeInterview();       // 出力: デザインパターンについて尋ねる

$marketingManager = new MarketingManager();
$marketingManager->takeInterview(); // コミュニティ育成について尋ねる

使いみち

あるクラスに何らかの一般的な処理があるが、実行時にサブクラスを動的に決定する必要がある場合に便利です。言い換えると、サブクラスで必要になりそうなものをクライアントが正確には知らない(または知る必要がない)場合です。

Abstract Factoryパターン🔨

現実世界になぞらえるとこうです。

simple factoryのドアのたとえをもう少し広げて考えてみましょう。木製ドアは木製ドア専門店、鉄製ドアは鉄専門店、塩ビドアはそれ専門の店から入手したいとします。かつ、ドアの種類ごとに違う職人が必要になるとします(木製なら大工、鉄製なら溶接工、など)。すなわち、現在取り付けられているドアの種類に応じて、木製ドアには大工を、鉄製ドアなら溶接工を、といった具合に職人を頼む必要があります。

わかりやすくまとめるとこうです。

複数のファクトリーに対する1つのファクトリー(a factory of factories)は、個別のファクトリーを束ねてグループ化しますが、関連する(または依存する)ファクトリーの具体的なクラスを指定しません。

Wikipediaではこうです。

abstract factoryパターンは、テーマの共通な個別のファクトリーのグループをカプセル化する手段を提供する。このとき、ファクトリーの具体的なクラスを指定しない。

プログラム例

先のドアの例に手を加えます。最初に、Doorインターフェイスと実装を少々記述します。

interface Door
{
    public function getDescription();
}

class WoodenDoor implements Door
{
    public function getDescription()
    {
        echo '私は木のドアです';
    }
}

class IronDoor implements Door
{
    public function getDescription()
    {
        echo '私は鉄のドアです';
    }
}

続いて、ドアの種類に応じた取付職人(fitting expert)を記述します。

interface DoorFittingExpert
{
    public function getDescription();
}

class Welder implements DoorFittingExpert
{
    public function getDescription()
    {
        echo '私が取り付けられるのは鉄のドアだけです';
    }
}

class Carpenter implements DoorFittingExpert
{
    public function getDescription()
    {
        echo '私が取り付けられるのは木のドアだけです';
    }
}

これでabstract factoryができました。これによって、関連するオブジェクト(木製ドアのfactoryは木製ドアと木製ドア取付職人を1つずつ作成し、鉄製ドアのfactoryは鉄製ドアと鉄製ドア取付職人を1つずつ作成する、など)をファミリーごとにまとめることができます。

interface DoorFactory
{
    public function makeDoor(): Door;
    public function makeFittingExpert(): DoorFittingExpert;
}

// 木製ドアのファクトリーは大工と木製ドアを返す
class WoodenDoorFactory implements DoorFactory
{
    public function makeDoor(): Door
    {
        return new WoodenDoor();
    }

    public function makeFittingExpert(): DoorFittingExpert
    {
        return new Carpenter();
    }
}

// 鉄製ドアのファクトリーで鉄製ドアとそれに合う取付職人を取得
class IronDoorFactory implements DoorFactory
{
    public function makeDoor(): Door
    {
        return new IronDoor();
    }

    public function makeFittingExpert(): DoorFittingExpert
    {
        return new Welder();
    }
}

これで、以下のように使うことができます。

$woodenFactory = new WoodenDoorFactory();

$door = $woodenFactory->makeDoor();
$expert = $woodenFactory->makeFittingExpert();

$door->getDescription();   // 出力: 私は木のドアです
$expert->getDescription(); // 出力: 私が取り付けられるのは木のドアだけです

// 鉄製ドアの場合も同様
$ironFactory = new IronDoorFactory();

$door = $ironFactory->makeDoor();
$expert = $ironFactory->makeFittingExpert();

$door->getDescription();   // 出力: 私は鉄のドアです
$expert->getDescription(); // 出力: 私が取り付けられるのは鉄製ドアだけです

見てのとおり、木製ドアのfactoryにはcarpenterwooden doorがカプセル化され、鉄製ドアのfactoryにはiron doorwelderがカプセル化されています。これで、作成したドアの種類に合わない取付職人を呼び出すことがなくなります。

使いみち

依存関係が双方向で、作成のロジックがある程度以上複雑な場合。

Builderパターン👷

現実世界になぞらえるとこうです。

ハーディーズ(Hardee's: 米国のレストランチェーン)で何か注文するところを考えます。たとえば「ビッグハーディ」を頼めば、店の人は何も聞かずに持ってきます。これだとsimple factoryの例になります。しかしたとえば、少し凝ったサブウェイ風のが欲しい場合、ハンバーガーの作り方にはいろいろなオプションが考えられます(どんなバンズにするか、ソースの種類は何がいいか、チーズはどれにするか、など)。このような場合にはbuilderパターンが助けに来てくれるでしょう。

わかりやすくまとめるとこうです。

コンストラクタを汚さないようにしながら、さまざまなフレーバー(flavor: 味や香り、転じて追加的な要素)を持つオブジェクトを作成できるようになります。オブジェクトにさまざまなフレーバーがある場合や、オブジェクトの作成に関連する手順が多い場合に有用です。

Wikipediaではこうです。

builderパターンとは、オブジェクト作成における「telescoping constructor」アンチパターンの解決を目指す意図を持つソフトウェアデザインパターンである。

「telescoping constructor」アンチパターンについて少しだけ説明しておこうと思います。ひと頃、以下のようなコンストラクタをよく見かけたものです。

public function __construct($size, $cheese = true, $pepperoni = true, $tomato = false, $lettuce = true)
{
}

見てのとおり、コンストラクタのパラメータ数はあっという間に増えて手に負えなくなり、パラメータの並びを理解するのが困難になってしまいます。さらに、今後オプションを追加したくなればパラメータリストは大きくなる一方でしょう。これが「telescoping constructor」アンチパターンです。

プログラム例

これに対するまともな手法がbuilderパターンです。最初に、作りたいハンバーガーBurgerを記述します。

class Burger
{
    protected $size;

    protected $cheese = false;
    protected $pepperoni = false;
    protected $lettuce = false;
    protected $tomato = false;

    public function __construct(BurgerBuilder $builder)
    {
        $this->size = $builder->size;
        $this->cheese = $builder->cheese;
        $this->pepperoni = $builder->pepperoni;
        $this->lettuce = $builder->lettuce;
        $this->tomato = $builder->tomato;
    }
}

続いてbuilderを記述します。

class BurgerBuilder
{
    public $size;

    public $cheese = false;
    public $pepperoni = false;
    public $lettuce = false;
    public $tomato = false;

    public function __construct(int $size)
    {
        $this->size = $size;
    }

    public function addPepperoni()
    {
        $this->pepperoni = true;
        return $this;
    }

    public function addLettuce()
    {
        $this->lettuce = true;
        return $this;
    }

    public function addCheese()
    {
        $this->cheese = true;
        return $this;
    }

    public function addTomato()
    {
        $this->tomato = true;
        return $this;
    }

    public function build(): Burger
    {
        return new Burger($this);
    }
}

これで以下のように書けます。

$burger = (new BurgerBuilder(14))
                    ->addPepperoni()
                    ->addLettuce()
                    ->addTomato()
                    ->build();

使いみち

オブジェクトにいくつものフレーバーがあり、telescoping constructorを避ける場合に有用です。factoryパターンとの大きな違いは、factoryパターンは作成が1ステップでできる場合に使うのに対し、builderパターンは作成に複数のステップを踏む場合に使う点です。

Prototypeパターン🐑

現実世界になぞらえるとこうです。

クローン羊ドリーを覚えていますか?詳しい点はともかく、ここで重要なのは「クローン」です。

わかりやすくまとめるとこうです。

prototypeパターンは、既存のオブジェクトを「クローン」することでオブジェクトを作成します。

Wikipediaではこうです。

prototypeパターンとは、ソフトウェア開発における作成系デザインパターンであり、作成するオブジェクトの種類はプロトタイプとしてのインスタンスによって決定される。このインスタンスをクローンすることで新しいオブジェクトを生成する。

要するに、オブジェクトをスクラッチから苦労して作成・セットアップするのではなく、既存のコピーを作成して必要に応じて改変できるのがprototypeパターンです。

プログラム例

PHPではcloneで簡単にクローンを実現できます。

class Sheep
{
    protected $name;
    protected $category;

    public function __construct(string $name, string $category = 'オオツノヒツジ')
    {
        $this->name = $name;
        $this->category = $category;
    }

    public function setName(string $name)
    {
        $this->name = $name;
    }

    public function getName()
    {
        return $this->name;
    }

    public function setCategory(string $category)
    {
        $this->category = $category;
    }

    public function getCategory()
    {
        return $this->category;
    }
}

これで以下のようにクローンを実行できるようになります。

$original = new Sheep('ジョリー');
echo $original->getName();     // ジョリー
echo $original->getCategory(); // オオツノヒツジ

// クローン後、必要なものを変更する
$cloned = clone $original;
$cloned->setName('ドリー');
echo $cloned->getName();     // ドリー
echo $cloned->getCategory(); // オオツノヒツジ

__cloneというマジックメソッドを使うと、クローンする際の振る舞いを変更することもできます。

使いみち

オブジェクトが既存オブジェクトに近いものであることが求められる場合や、オブジェクト作成のコストがクローンよりも高い場合に有用です。

Singletonパターン💍

現実世界になぞらえるとこうです。

一国の大統領は、普通1人だけです。実際に表敬訪問する大統領は常に同一人物でなければなりません。大統領はsingletonなのです。

わかりやすくまとめるとこうです。

あるクラスのオブジェクトが常に1つだけ作成されることを保証します。

Wikipediaではこうです。

ソフトウェア・エンジニアリングにおけるsingletonパターンとは、クラスのインスタンスを常に1オブジェクトに制限するソフトウェアデザインパターンです。システム全体に渡る操作を1つのオブジェクトだけが扱う必要がある場合に役に立つ。

singletonパターンは実際にはアンチパターンであると考えられており、使い過ぎは慎むべきです。それ自体は悪くありませんし、有用なユースケースもいくつかありますが、アプリケーションにグローバルステートを持ち込んでしまうのと、ある箇所での変更が他の箇所にも影響してしまうことでデバッグがかなり難しくなってしまうため、利用の際には十分注意が必要です。

もうひとつ問題なのは、コードが密結合になってしまい、シングルトンのモックを作ることが難しくなってしまう点です。

プログラム例

singletonを作成するには、コンストラクタをprivateにし、クローンを無効にし、拡張(extend)を無効にしてから、静的変数を作成してインスタンス内に配置します。

final class President
{
    private static $instance;

    private function __construct()
    {
        // コンストラクタを隠す
    }

    public static function getInstance(): President
    {
        if (!self::$instance) {
            self::$instance = new self();
        }

        return self::$instance;
    }

    private function __clone()
    {
        // クローンを無効にする
    }

    private function __wakeup()
    {
        // unserializeを無効にする
    }
}

使うときは以下のようにします。

$president1 = President::getInstance();
$president2 = President::getInstance();

var_dump($president1 === $president2); // true

本記事への貢献方法👬

  • 問題をissueで報告する
  • 改善点をプルリクで送る
  • 本記事を広める
  • フィードバックを送る Twitter URL

License

License: MIT

バックナンバー

関連記事

Rubyで学ぶデザインパターンを社内勉強会で絶賛?連載中

RailsでGraphQL APIをつくる: Part 3 – ベストプラクティス集(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。