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

こんにちは、hachi8833です。デザインパターン解説シリーズの最終回です(全3回)。

概要

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

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

人間が読んで理解できるデザインパターン解説#3: 振舞い系(翻訳)

Design Patterns For Humans


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

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


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


🚀 はじめに

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

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

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

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

⚠️ ご注意

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

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

デザインパターンの種別

#3 振舞い系デザインパターン

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

オブジェクト間での責務の割り当てに関連します。単に構造を指定するのではなく、メッセージ受け渡しや通信パターンの大枠を定める点が構造系 と異なります。言い換えると、「ある振舞いをソフトウェアコンポーネントでどのように実行するか」という問いに答える手助けをします。

Wikipediaではこうです。

ソフトウェアエンジニアリングにおける振舞い系デザインパターンとは、オブジェクト間で共通の通信パターンを指定することでパターンを実現するものである。これを実現すると、パターンによって通信実行時の柔軟性が高まる。

🔗 Chain of Responsibilityパターン(link

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

たとえば、3種類の異なる支払い方法(ABC)が設定された銀行口座を持っているとします。それぞれの(最大)支払額は異なっており、Aは100米ドル、Bは300米ドル、Cは1000米ドルです。支払い設定では「A、次にB、次にC」のように選択されます。そして210米ドルで何かを購入したとします。Chain of Responsibilityパターンを使って、最初にAで支払い可能かどうかをチェックします。支払い可能であればそのまま購入を行い、チェイン(連鎖)は終了します。支払い可能でない場合、リクエストは次のBに進んで支払額をチェックします。支払い可能な場合はチェインは終了し、支払い可能でない場合はリクエストを次に進め、適切なハンドラが見つかるまでこれを繰り返します。ここでいうABCはチェインのリンクであり、この動作全体がChain of Responsibilityパターンになります。

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

Chain of Responsibilityパターンは、オブジェクトのチェイン形成を支援します。リクエストはチェインの一方から他方へ、あるオブジェクトから別のオブジェクトへと進み、適切なハンドラが見つかるまでこれを繰り返します。

Wikipediaではこうです。

オブジェクト指向設計におけるChain of Responsibilityパターンは、コマンドオブジェクトの提供元と、一連の処理オブジェクトで構成される。各処理オブジェクトには、オブジェクトが扱えるコマンドオブジェクトの種類を定義するロジックが含まれ、それ以外のコマンドオブジェクトは、チェイン上の次の処理オブジェクトに渡される。

プログラム例

先の銀行口座の例を使います。最初に基本となる口座を記述し、口座と他の口座をチェインするロジックを記述します。

abstract class Account
{
    protected $successor;
    protected $balance;

    public function setNext(Account $account)
    {
        $this->successor = $account;
    }

    public function pay(float $amountToPay)
    {
        if ($this->canPay($amountToPay)) {
            echo sprintf('%sで%sドル支払われました。' . PHP_EOL, get_called_class(), $amountToPay);
        } elseif ($this->successor) {
            echo sprintf('%sで支払いできません。次の支払い方法に進みます。' . PHP_EOL, get_called_class());
            $this->successor->pay($amountToPay);
        } else {
            throw new Exception('残高が十分なアカウントがありません');
        }
    }

    public function canPay($amount): bool
    {
        return $this->balance >= $amount;
    }
}

class Bank extends Account
{
    protected $balance;

    public function __construct(float $balance)
    {
        $this->balance = $balance;
    }
}

class Paypal extends Account
{
    protected $balance;

    public function __construct(float $balance)
    {
        $this->balance = $balance;
    }
}

class Bitcoin extends Account
{
    protected $balance;

    public function __construct(float $balance)
    {
        $this->balance = $balance;
    }
}

上で定義したリンクを使ってチェインを準備します。ここではBankPaypalBitcoinにします。

// 次のようにチェインを形成する
//      $bank->$paypal->$bitcoin
//
// 銀行払い(bank)を優先
//      銀行払いできない場合はpaypalにする
//      paypalで払えない場合はbit coinにする

$bank = new Bank(100);          // Bank(残高: 100)
$paypal = new Paypal(200);      // Paypal(残高: 200)
$bitcoin = new Bitcoin(300);    // Bitcoin(残高: 300)

$bank->setNext($paypal);
$paypal->setNext($bitcoin);

// 最優先の支払い方法を試す(ここではBank)
$bank->pay(259);

// 出力は次のようになる:
// ==============
// Bankで支払いできません。次の支払い方法に進みます。
// PayPalで支払いできません。次の支払い方法に進みます。
// Bitcoinで259ドル支払われました。

👮 Commandパターン(link

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

一般的な例として、レストランで食事を注文するときを考えてみます。あなた(Client)がウエイター(Invoker)に何か食事を持ってくるように指示する(Command)と、ウエイターはリクエストを料理長(Receiver)に丸投げします。料理長はどんな材料をどのように料理するかを知っています。
別の例として、あなた(Client)がリモコン(Invoker)でテレビ(Receiver)の電源をオンにするところも考えられます。

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

commandパターンは、操作をオブジェクト内にカプセル化できるようにします。このパターンの背後にある重要なアイデアは、クライアントをレシーバーから分離する手段を提供することです。

Wikipediaではこうです。

オブジェクト指向プログラミングにおけるcommandパターンは振舞い系デザインパターンの一種であり、後で操作の実行やイベントのトリガに必要となるすべての情報をカプセル化するのにオブジェクトを用いる。この情報には、メソッド名、メソッドを持つオブジェクト、メソッドパラメータ用の値が含まれる。

プログラム例

最初に、実行される可能性のあるすべての操作を実装するレシーバを記述します。

// レシーバ
class Bulb
{
    public function turnOn()
    {
        echo "電球がつきました!";
    }

    public function turnOff()
    {
        echo "真っ暗!";
    }
}

続いて、各コマンドを実装するインターフェイスを記述し、コマンドのセットを記述します。

interface Command
{
    public function execute();
    public function undo();
    public function redo();
}

// コマンド
class TurnOn implements Command
{
    protected $bulb;

    public function __construct(Bulb $bulb)
    {
        $this->bulb = $bulb;
    }

    public function execute()
    {
        $this->bulb->turnOn();
    }

    public function undo()
    {
        $this->bulb->turnOff();
    }

    public function redo()
    {
        $this->execute();
    }
}

class TurnOff implements Command
{
    protected $bulb;

    public function __construct(Bulb $bulb)
    {
        $this->bulb = $bulb;
    }

    public function execute()
    {
        $this->bulb->turnOff();
    }

    public function undo()
    {
        $this->bulb->turnOn();
    }

    public function redo()
    {
        $this->execute();
    }
}

次に、クライアントが任意のコマンドでやり取りする相手であるInvokerを記述します。

// Invoker
class RemoteControl
{
    public function submit(Command $command)
    {
        $command->execute();
    }
}

最後に、クライアントでどのように利用できるかを見てみましょう。

$bulb = new Bulb();

$turnOn = new TurnOn($bulb);
$turnOff = new TurnOff($bulb);

$remote = new RemoteControl();
$remote->submit($turnOn); // 電球がつきました!
$remote->submit($turnOff); // 真っ暗!

commandパターンは、トランザクションベースのシステムの実装にも利用できます。コマンド実行と同時にコマンド履歴を保存する場合、最終コマンド実行に成功すればOK、実行に失敗したら履歴を列挙し、それまで実行したすべてのコマンドに対してひたすらundoを実行し続けます。

➿ Iteratorパターン(link

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

iteratorパターンのよい例は昔のラジオです。ラジオの視聴者はどこかのチャンネルからスタートして[前]/[次]ボタンでチャンネルを順に切り替えることができます。あるいは、MP3プレイヤーやテレビの[前]/[次]ボタンでチャンネルを順に切り替えるところを想像してもよいでしょう。
言い換えると、ラジオやテレビやMP3プレイヤーは、チャンネルや曲やラジオ局のそれぞれのリストを介して列挙(iterate)するインターフェイスを1つ提供していることになります。

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

iteratorパターンは、背後にある表現を隠したまま、あるオブジェクトの1つ以上の要素にアクセスする手段を提供します。

Wikipediaではこうです。

オブジェクト指向プログラミングにおけるiteratorパターンは、コンテナ内をiteratorでトラバース(traverse: スキャン)してコンテナの要素にアクセスするデザインパターンである。iteratorパターンはコンテナからアルゴリズムを切り離すが、アルゴリズムがコンテナに特化しなければならない場合は切り離せない。

プログラム例

PHPのSPL(Standard PHP Library)を使うとiteratorパターンをかなり簡単に書けます。先のラジオ局の例を使うことにします。最初にRadioStationを記述します。

class RadioStation
{
    protected $frequency;

    public function __construct(float $frequency)
    {
        $this->frequency = $frequency;
    }

    public function getFrequency(): float
    {
        return $this->frequency;
    }
}

続いてiteratorを記述します。

use Countable;
use Iterator;

class StationList implements Countable, Iterator
{
    /** @var RadioStation[] $stations */
    protected $stations = [];

    /** @var int $counter */
    protected $counter;

    public function addStation(RadioStation $station)
    {
        $this->stations[] = $station;
    }

    public function removeStation(RadioStation $toRemove)
    {
        $toRemoveFrequency = $toRemove->getFrequency();
        $this->stations = array_filter($this->stations, function (RadioStation $station) use ($toRemoveFrequency) {
            return $station->getFrequency() !== $toRemoveFrequency;
        });
    }

    public function count(): int
    {
        return count($this->stations);
    }

    public function current(): RadioStation
    {
        return $this->stations[$this->counter];
    }

    public function key()
    {
        return $this->counter;
    }

    public function next()
    {
        $this->counter++;
    }

    public function rewind()
    {
        $this->counter = 0;
    }

    public function valid(): bool
    {
        return isset($this->stations[$this->counter]);
    }
}

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

$stationList = new StationList();

$stationList->addStation(new RadioStation(89));
$stationList->addStation(new RadioStation(101));
$stationList->addStation(new RadioStation(102));
$stationList->addStation(new RadioStation(103.2));

foreach($stationList as $station) {
    echo $station->getFrequency() . PHP_EOL;
}

$stationList->removeStation(new RadioStation(89)); // ラジオ局89を削除する

👽 Mediatorパターン(link

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

一般的な例として、スマホで誰かに話しかけるところを考えてみます。あなたと通話相手の間にはネットワークプロバイダが介在し、会話の内容は通話相手に直接送信されずにプロバイダを経由します。このネットワークプロバイダがmediator(仲介者)です。

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

mediatorパターンは、mediatorと呼ばれる第三者的なオブジェクトを追加することで、colleague(同僚)と呼ばれる2つのオブジェクト同士のやりとりを制御します。このパターンでは、クラスは相手のクラスの実装を知る必要がないため、クラス間の相互通信の結合を弱める働きがあります。

Wikipediaではこうです。

ソフトウェアエンジニアリングにおけるmediatorパターンは、オブジェクト同士のやりとりの設定をカプセル化するオブジェクトを定義するものである。同パターンは振舞い系デザインパターンと呼ばれるが、これはプログラム実行中の動作を改変する方法が由来となっている。

プログラム例

ここでは最もシンプルな例として、チャットルーム(mediatorに相当)と、互いにメッセージを送信するユーザー(colleagueに相当)を使います。

最初に、チャットルームのmediatorを記述します。

interface ChatRoomMediator 
{
    public function showMessage(User $user, string $message);
}

// mediator
class ChatRoom implements ChatRoomMediator
{
    public function showMessage(User $user, string $message)
    {
        $time = date('M d, y H:i');
        $sender = $user->getName();

        echo $time . '[' . $sender . ']:' . $message;
    }
}

続いてユーザー(colleague)を記述します。

class User {
    protected $name;
    protected $chatMediator;

    public function __construct(string $name, ChatRoomMediator $chatMediator) {
        $this->name = $name;
        $this->chatMediator = $chatMediator;
    }

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

    public function send($message) {
        $this->chatMediator->showMessage($this, $message);
    }
}

利用法は次のとおりです。

$mediator = new ChatRoom();

$john = new User('John Doe', $mediator);
$jane = new User('Jane Doe', $mediator);

$john->send('こんちは!');
$jane->send('よう!');

// Output will be
// Feb 14, 10:58 [John]: こんちは!
// Feb 14, 10:58 [Jane]: よう!

💾 Mementoパターン(link

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

電卓を例にします。電卓をoriginator(発起人)とし、電卓で何か計算すると、最後の計算がメモリ上に保存されます。これがmemento(形見、遺品)に相当します。何か操作ボタンを押すと保存された計算を後で呼び戻すことができます。この操作ボタンはcaretaker(世話人)に相当します。

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

mementoパターンは、オブジェクトの現在のステート(状態)をキャプチャし、後でスムーズに取り出せる形式で保存します。

Wikipediaではこうです。

mementoパターンはソフトウェアにおけるデザインパターンであり、オブジェクトのステートを以前のステートに戻す(ロールバックによるundo)能力を提供する。

mementoパターンは、何らかのundo機能を提供する必要がある場合に便利です。

プログラム例

テキストエディタを例に取ることにします。このテキストエディタは定期的にステートの保存を繰り返し、必要に応じてステートを戻すことができます。

最初に、エディタのステートを保持できるmementoオブジェクトを記述します。

class EditorMemento
{
    protected $content;

    public function __construct(string $content)
    {
        $this->content = $content;
    }

    public function getContent()
    {
        return $this->content;
    }
}

続いて、mementoオブジェクトを使うエディタ(ここではoriginator)を記述します。

class Editor
{
    protected $content = '';

    public function type(string $words)
    {
        $this->content = $this->content . ' ' . $words;
    }

    public function getContent()
    {
        return $this->content;
    }

    public function save()
    {
        return new EditorMemento($this->content);
    }

    public function restore(EditorMemento $memento)
    {
        $this->content = $memento->getContent();
    }
}

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

$editor = new Editor();

// 何か入力する
$editor->type('最初の文です。');
$editor->type('次の文です。');

// 後で戻したいステートを保存する: 「最初の文です。」「次の文です。」
$saved = $editor->save();

// もう少し入力する
$editor->type('3番目の文です。');

// Contentを出力して保存
echo $editor->getContent(); // 「最初の文です。」「次の文です。」「3番目の文です。」

// 最後に保存したステートに戻す
$editor->restore($saved);

$editor->getContent(); // 「最初の文です。」「次の文です。」

😎 Observerパターン(link

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

observer(観測者)パターンのよい例として、求職者(job seeker)が求職サイトに登録(subscribe)し、マッチする求人があれば常に通知するところを考えるとよいでしょう。

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

オブジェクト間の依存関係を定義して、オブジェクトが自身のステートを変更するとすべての依存相手に通知されるようにします。

Wikipediaではこうです。

observerパターンはソフトウェアのデザインパターンのひとつであり、subject(主体)と呼ばれるオブジェクトが、observerと呼ばれる依存相手のリストを保持し、何らかのステートが変更されたときに自動的にobserverに通知する。ステートの変更は、主にobserverが持つメソッドの呼び出しによって行われる。

プログラム例

先の求職サイトの例を使うことにします。最初に、求人が投稿されたときの通知を求める求職者を定義します。

class JobPost
{
    protected $title;

    public function __construct(string $title)
    {
        $this->title = $title;
    }

    public function getTitle()
    {
        return $this->title;
    }
}

class JobSeeker implements Observer
{
    protected $name;

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

    public function onJobPosted(JobPost $job)
    {
        // 求人が投稿されたときの処理を何か書く
        echo 'こんにちは、' . $this->name . 'さん。新しい求人が投稿されました: '. $job->getTitle();
    }
}

続いて、求職者が登録する求人の投稿(JobPostings)を記述します。

class JobPostings implements Observable
{
    protected $observers = [];

    protected function notify(JobPost $jobPosting)
    {
        foreach ($this->observers as $observer) {
            $observer->onJobPosted($jobPosting);
        }
    }

    public function attach(Observer $observer)
    {
        $this->observers[] = $observer;
    }

    public function addJob(JobPost $jobPosting)
    {
        $this->notify($jobPosting);
    }
}

これで、次のように書けます。

// 登録者の作成
$johnDoe = new JobSeeker('John Doe');
$janeDoe = new JobSeeker('Jane Doe');

// 投稿のパブリッシャーを作成して登録者をアタッチする
$jobPostings = new JobPostings();
$jobPostings->attach($johnDoe);
$jobPostings->attach($janeDoe);

// 求人を1つ追加して、求職者に通知されるかどうかをチェック
$jobPostings->addJob(new JobPost('ソフトウェアエンジニア'));

// Output
// こんにちは、John Doeさん。新しい求人が投稿されました: ソフトウェアエンジニア
// こんにちは、Jane Doeさん。新しい求人が投稿されました: ソフトウェアエンジニア

🏃 Visitorパターン(link

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

ドバイに行く旅行者で考えてみます。ドバイに入国するまでは、入国手段(ビザなど)だけが必要です。ドバイに到着した後は、いちいち許可を得たり面倒な手続きを行ったりせずに、ドバイ国内のどんな場所でも自由に移動できるようになります。旅行者にドバイ国内の場所を知らせると、旅行者はそこに行けるようになるものとします。visitorパターンはちょうどこのようなものです。訪問する場所をひとたび追加すると、面倒な手続きなしにいつでもそこを訪問できるようになります。

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

visitorパターンは、オブジェクト改変を必要とせずに他の操作を追加します。

Wikipediaではこうです。

オブジェクト指向プログラミングやソフトウェアエンジニアリングにおけるvisitorデザインパターンは、visitorが操作するオブジェクトの構造からアルゴリズムを切り離す方法である。アルゴリズムの分離による直接の結果として、既存のオブジェクトに新しい操作を加えられるようになり、かつオブジェクトの構造を変更する必要が生じない。visitorパターンは、開放/閉鎖の原則に沿うための方法のひとつである。

プログラム例

動物園のシミュレーションを例に使います。動物園にはさまざまな種類の動物がいて、鳴き声は動物によって違います。visitorパターンでプログラムに置き換えてみましょう。

// 訪問される側(visitee)
interface Animal
{
    public function accept(AnimalOperation $operation);
}

// 訪問する側(visitor)
interface AnimalOperation
{
    public function visitMonkey(Monkey $monkey);
    public function visitLion(Lion $lion);
    public function visitDolphin(Dolphin $dolphin);
}

続いて、それぞれの動物の実装を記述します。

class Monkey implements Animal
{
    public function shout()
    {
        echo 'ウキャッ、キャッ';
    }

    public function accept(AnimalOperation $operation)
    {
        $operation->visitMonkey($this);
    }
}

class Lion implements Animal
{
    public function roar()
    {
        echo 'ガオォオオ';
    }

    public function accept(AnimalOperation $operation)
    {
        $operation->visitLion($this);
    }
}

class Dolphin implements Animal
{
    public function speak()
    {
        echo 'キキキ、キキキ';
    }

    public function accept(AnimalOperation $operation)
    {
        $operation->visitDolphin($this);
    }
}

続いてvisitorを実装しましょう。

class Speak implements AnimalOperation
{
    public function visitMonkey(Monkey $monkey)
    {
        $monkey->shout();
    }

    public function visitLion(Lion $lion)
    {
        $lion->roar();
    }

    public function visitDolphin(Dolphin $dolphin)
    {
        $dolphin->speak();
    }
}

これで、次のように書けます。

$monkey = new Monkey();
$lion = new Lion();
$dolphin = new Dolphin();

$speak = new Speak();

$monkey->accept($speak);    // ウキャッ、キャッ
$lion->accept($speak);      // ガオォオオ
$dolphin->accept($speak);   // キキキ、キキキ

動物クラスの継承階層を作るだけでもこれと同じことはできます(訳注: オブジェクト指向プログラミングの継承で必ずといってよいほど引き合いに出される手法です)が、その場合動物に新しい動作を追加するたびに動物クラスを変更しなければならなくなります。

しかしvisitorパターンなら動物のクラスを変更する必要はありません。たとえば、動物に「ジャンプ」という動作を加えたい場合は、visitorをひとつ作成するだけで済みます。

class Jump implements AnimalOperation
{
    public function visitMonkey(Monkey $monkey)
    {
        echo '20フィートジャンプして木に登った!';
    }

    public function visitLion(Lion $lion)
    {
        echo '7フィートジャンプして着地した!';
    }

    public function visitDolphin(Dolphin $dolphin)
    {
        echo '水上を少し進んでから姿を消した';
    }
}

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

$jump = new Jump();

$monkey->accept($speak);   // ウキャッ、キャッ
$monkey->accept($jump);    // 20フィートジャンプして木に登った!

$lion->accept($speak);     // ガオォオオ
$lion->accept($jump);      // 7フィートジャンプして着地した!

$dolphin->accept($speak);  // キキキ、キキキ
$dolphin->accept($jump);   // 水上を少し進んでから姿を消した

💡 Strategyパターン(link

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

ソートで考えてみましょう。バブルソートを実装したものの、データが増えるに連れてバブルソートが遅くなってきたので、クイックソートを実装して対応しました。しかしクイックソートのアルゴリズムは大規模データセットでは良好ですが、データセットが小さいと逆にものすごく遅くなります。この問題に対応するためにstrategyパターンを実装し、小規模データセットにはバブルソート、大規模データセットにはクイックソートがそれぞれ用いられるようにしました。

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

strategyパターンを使うことで、状況に応じてアルゴリズムや戦略を切り替えられます。

Wikipediaではこうです。

コンピュータ・プログラミングにおけるstrategyパターン(policyパターンとも呼ばれる)は、アルゴリズムの振舞いを実行時に指定できるようにするための振舞い系ソフトウェアデザインパターンである。

プログラム例

上のソートの例を用います。最初にstrategyのインターフェイスを記述し、それとは別にstrategyの実装を記述します。

interface SortStrategy
{
    public function sort(array $dataset): array;
}

class BubbleSortStrategy implements SortStrategy
{
    public function sort(array $dataset): array
    {
        echo "バブルソートを実行";

        // ここでソートする
        return $dataset;
    }
}

class QuickSortStrategy implements SortStrategy
{
    public function sort(array $dataset): array
    {
        echo "クイックソートを実行";

        // ここでソートする
        return $dataset;
    }
}

これにより、クライアントは任意のstrategyを使えるようになります。

class Sorter
{
    protected $sorter;

    public function __construct(SortStrategy $sorter)
    {
        $this->sorter = $sorter;
    }

    public function sort(array $dataset): array
    {
        return $this->sorter->sort($dataset);
    }
}

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

$dataset = [1, 5, 4, 3, 2, 8];

$sorter = new Sorter(new BubbleSortStrategy());
$sorter->sort($dataset); // 出力: バブルソートを実行

$sorter = new Sorter(new QuickSortStrategy());
$sorter->sort($dataset); // 出力: クイックソートを実行

💢 Stateパターン(link

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

何らかの作図アプリを使っていて、ペイントブラシを選択したとします。すると、選択している色などに応じてブラシの振る舞いが変わります。赤を選べば赤で描けるようになり、青を選べば青で描けるようになる、といった具合です。

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

stateパターンは、ステートの変更に伴ってクラスの振る舞いを変更します。

Wikipediaではこうです。

stateパターンは振る舞い系のソフトウェアデザインパターンであり、ステートマシン(state machine: 状態機械)をオブジェクト指向的に実装したものである。stateパターンを用いると、各ステートをstateパターンインターフェイスの派生クラスとして実装する形でステートマシンが実装され、パターンのスーパークラスで定義されたメソッドを呼ぶことでステートの遷移を実装する。
stateパターンはstrategyパターン(後述)として解釈することもできる。このstrategyパターンは、そのパターンのインターフェイスに定義されたメソッド呼び出しを通じて現在のストラテジーを切り替えることができる。

プログラム例

テキストエディタを例に使います。入力したテキストのステートを変更してみましょう。ボールドを選択すれば、以後テキストはボールドになり、イタリックを選択すれば以後イタリックになる、という具合です。

訳注: 例では英文の大文字小文字を変更しています。

最初に、ステートのインターフェイスと、その実装をいくつか記述します。

interface WritingState
{
    public function write(string $words);
}

class UpperCase implements WritingState
{
    public function write(string $words)
    {
        echo strtoupper($words);
    }
}

class LowerCase implements WritingState
{
    public function write(string $words)
    {
        echo strtolower($words);
    }
}

class Default implements WritingState
{
    public function write(string $words)
    {
        echo $words;
    }
}

続いて、テキストエディタを記述します。

class TextEditor
{
    protected $state;

    public function __construct(WritingState $state)
    {
        $this->state = $state;
    }

    public function setState(WritingState $state)
    {
        $this->state = $state;
    }

    public function type(string $words)
    {
        $this->state->write($words);
    }
}

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

$editor = new TextEditor(new Default());

$editor->type('First line');

$editor->setState(new UpperCase());

$editor->type('Second line');
$editor->type('Third line');

$editor->setState(new LowerCase());

$editor->type('Fourth line');
$editor->type('Fifth line');

// 出力:
// First line
// SECOND LINE
// THIRD LINE
// fourth line
// fifth line

📒 Template Methodパターン(link

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

家を建設しているところを考えてみましょう。家造りはだいたい次のような手順になるでしょう。
– 家の基礎部分を作る
– 壁を作る
– 屋根を追加する
– 他の床を追加する

家造りの手順は、まずこれ以外の順序にはならないはずです。屋根を作ってから壁を作る人はいないでしょう。それぞれの手順は、壁を木製にするか、ポリエステル製にするか、石造りにするか、などさまざまに変わります。

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

template methodパターンは、特定のアルゴリズムの実行手順の骨格部分を定義します。手順は子クラスで実装します。

Wikipediaではこうです。

ソフトウェアエンジニアリングにおけるtemplate methodパターンは振舞い系デザインパターンの一種であり、ある操作のアルゴリズムのプログラム的な骨格を定義し、手順の一部はサブクラスにする。template methodパターンを用いることで、アルゴリズムの特定の手順を、アルゴリズムの構造を変更せずに再定義できるようになる。

プログラム例

ソフトウェアテストを支援するビルドツールを例にします。このツールは、カバレッジやlintのレポートを出力したり、アプリをテストサーバーにデプロイしたりします。

最初に、ビルドのアルゴリズムの骨格を示す基本クラス(Builder)を記述します。

abstract class Builder
{

    // Template method
    final public function build()
    {
        $this->test();
        $this->lint();
        $this->assemble();
        $this->deploy();
    }

    abstract public function test();
    abstract public function lint();
    abstract public function assemble();
    abstract public function deploy();
}

続いて実装を記述します。

class AndroidBuilder extends Builder
{
    public function test()
    {
        echo 'Androidのテストを実行';
    }

    public function lint()
    {
        echo 'AndroidコードのLintを実行';
    }

    public function assemble()
    {
        echo 'Androidビルドのアセンブリを実行';
    }

    public function deploy()
    {
        echo 'Androidビルドをサーバーにデプロイ';
    }
}

class IosBuilder extends Builder
{
    public function test()
    {
        echo 'iOSのテストを実行';
    }

    public function lint()
    {
        echo 'iOSコードのLintを実行';
    }

    public function assemble()
    {
        echo 'iOSビルドのアセンブリを実行';
    }

    public function deploy()
    {
        echo 'iOSビルドをサーバーにデプロイ';
    }
}

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

$androidBuilder = new AndroidBuilder();
$androidBuilder->build();

// 出力:
// Androidのテストを実行
// AndroidコードのLintを実行
// Androidビルドのアセンブリを実行
// Androidビルドをサーバーにデプロイ

$iosBuilder = new IosBuilder();
$iosBuilder->build();

// 出力:
// iOSのテストを実行
// iOSコードのLintを実行
// iOSビルドのアセンブリを実行
// iOSビルドをサーバーにデプロイ

🚦 まとめ

以上でおしまいとなります。本記事は今後も改良を加える予定です。当リポジトリのwatch、スター追加、再訪問を歓迎します。

なお、アーキテクチャパターンについても本記事のようなわかりやすい解説記事を構想中です。どうぞご期待ください。

👬 本記事への貢献方法

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

License

License: MIT

バックナンバー

関連記事

Railsで重要なパターンpart 1: Service Object(翻訳)

RubyのModule Builderパターン #1 モジュールはどのように使われてきたか(翻訳)

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

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

JSの非同期処理を初めてES6のPromiseを使ったものに書き換えてみた

デザインも頼めるシステム開発会社をお探しならBPS株式会社までどうぞ 開発エンジニア積極採用中です! Ruby on Rails の開発なら実績豊富なBPS

この記事の著者

hachi8833

Twitter: @hachi8833、GitHub: @hachi8833 コボラー、ITコンサル、ローカライズ業界、Rails開発を経てTechRachoの編集・記事作成を担当。 これまでにRuby on Rails チュートリアル第2版の半分ほど、Railsガイドの初期翻訳ではほぼすべてを翻訳。その後も折に触れてそれぞれ一部を翻訳。 かと思うと、正規表現の粋を尽くした日本語エラーチェックサービス enno.jpを運営。 実は最近Go言語が好き。 仕事に関係ないすっとこブログ「あけてくれ」は2000年頃から多少の中断をはさんで継続、現在はnote.muに移転。

hachi8833の書いた記事

週刊Railsウォッチ

インフラ

ActiveSupport探訪シリーズ