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

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

概要

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

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

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

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

デザインパターンの概要については#1の概要をご覧ください。

デザインパターンの種別

#2 「構造系」デザインパターン

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

構造系パターンとは、オブジェクトのcomposition(構成)を主眼に据えたものです。言い換えると、どのようにしてエンティティ同士が互いを利用するかを記述します。さらに別の言い方をすると、「ソフトウェアのコンポーネントをどのように組み立てるのか?」という問題に答えるためのものです。

Wikipediaではこうです。

ソフトウェアエンジニアリングにおける構造系パターンとは、エンティティ間の関連付けを実現するシンプルな方法を1つに定めることによって設計を容易にするデザインパターンである。

Adapterパターン🔌

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

メモリーカードに画像がいくつか保存されており、これを自分のパソコンにコピーしたいとします。メモリカードをパソコンに挿入して画像を転送するには、自分のパソコンのポートと互換性のある何らかのアダプタが必要です。このカードリーダーが、adapterです。

別の例: 電源プラグにはプラグのピンが3本のものと2本のものがあります。3本タイプのプラグは2本プラグ用のコンセントには挿し込めませんので、挿入するには2本プラグ用コンセントと互換性のある変換アダプタが必要になります。これもadapterです。

さらに別の例: 通訳者は、ある人物の話す言葉を翻訳して、別の人物に伝えます。この通訳者もadapterです。

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

adapterパターンとは、互換性のない別のオブジェクトをadapterでラップして、別のクラスとの互換性を得るためのものです。

Wikipediaではこうです。

ソフトウェアエンジニアリングにおけるadapterパターンとは、既存のクラスのインターフェイスを別のインターフェイスとして利用できるようにするものである。adapterパターンは、ソースコードを改変せずに既存のクラスが別のクラスと協調して動くようにさせる目的で頻繁に利用される。

プログラム例

ゲームで考えてみましょう。ここではハンターがライオンを狩るゲームにしました。

最初にLionというインターフェイスを記述します。これにはすべての種類のライオンを実装しなければなりません。

interface Lion
{
    public function roar();
}

class AfricanLion implements Lion
{
    public function roar()
    {
    }
}

class AsianLion implements Lion
{
    public function roar()
    {
    }
}

ハンターは、Lionインターフェイスのすべての実装について狩りを行えることを期待します。

class Hunter
{
    public function hunt(Lion $lion)
    {
    }
}

ここで、ゲームにWildDogという野犬を追加しなければならないとしましょう。もちろんこれもハンターが狩りの対象にできることが求められます。しかし野犬のインターフェイスはライオンのと同じではないので、直接狩りの対象にはできません。ハンターとの互換性を得るには、互換性のあるadapterを1つ作成しなければなりません。

// これをゲームに加える必要がある
class WildDog
{
    public function bark()
    {
    }
}

// 野犬をadapterでラップして、ゲームとの互換性を得られるようにする
class WildDogAdapter implements Lion
{
    protected $dog;

    public function __construct(WildDog $dog)
    {
        $this->dog = $dog;
    }

    public function roar()
    {
        $this->dog->bark();
    }
}

これで、WildDogAdapterを使えばゲームでWildDogを利用できるようになります。

$wildDog = new WildDog();
$wildDogAdapter = new WildDogAdapter($wildDog);

$hunter = new Hunter();
$hunter->hunt($wildDogAdapter);

Bridgeパターン🚡

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

あなたはWebサイトをひとつ所有しているとします。Webサイトにはさまざまなページがあり、ユーザーがテーマを自由に変更できるようにすることを求められているとします。どのようにすればよいでしょうか: テーマの数だけページのコピーを作成しますか?それとも独立したテーマを作成し、ユーザー設定に応じてテーマを読み込ませますか?bridgeパターンを使って、後者のような手法を実現できます。

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

bridgeパターンは、継承ではなくcompositionを優先するためのものです。ある階層の実装の詳細は、別の階層にある別のオブジェクトにプッシュされます。

Wikipediaではこうです。

bridgeパターンとは、ソフトウェアエンジニアリングにおいて「実装からその抽象(abstraction)を切り離して、両者を別々に変更できるようにする」ために使うデザインパターンである。

プログラム例

先のWebPageの例を使うことにします。ここではWebPageという階層を設定します。

interface WebPage
{
    public function __construct(Theme $theme);
    public function getContent();
}

class About implements WebPage
{
    protected $theme;

    public function __construct(Theme $theme)
    {
        $this->theme = $theme;
    }

    public function getContent()
    {
        return $this->theme->getColor() . "のAboutページ";
    }
}

class Careers implements WebPage
{
    protected $theme;

    public function __construct(Theme $theme)
    {
        $this->theme = $theme;
    }

    public function getContent()
    {
        return $this->theme->getColor() . "のCareersページ";
    }
}

続いて、それとは独立したThemeという階層を記述します。


interface Theme { public function getColor(); } class DarkTheme implements Theme { public function getColor() { return 'Dark Black'; } } class LightTheme implements Theme { public function getColor() { return 'Off white'; } } class AquaTheme implements Theme { public function getColor() { return 'Light blue'; } }

これで、2つの階層を次のように使えます。

$darkTheme = new DarkTheme();

$about = new About($darkTheme);
$careers = new Careers($darkTheme);

echo $about->getContent();   // "Dark BlackのAboutページ";
echo $careers->getContent(); // "Dark BlackのCareersページ";

Compositeパターン🌿

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

どんな組織も従業員(employee)で構成されているものです。各従業員は同じ機能(給与や責務など)を共有しており、従業員によっては報告の義務があったりなかったり、部下がいたりいなかったりします。

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

compositeパターンは、クライアントが個別のオブジェクトを統一的な方法で扱えるようにするためのものです。

Wikipediaではこうです。

ソフトウェアエンジニアリングにおけるcompositeパターンとは、分割(partitioning)のためのデザインパターンである。compositeパターンは、オブジェクトのグループを単一オブジェクトのインスタンスと同じに扱えるよう記述する。compositeパターンの意図は、オブジェクトをツリー構造として「構成する(compose)」ことであり、このツリー構造は部分/全体階層を表現する。compositeパターンを実装することで、クライアントは個別のオブジェクトやcompositionを統一的に扱えるようになる。

プログラム例

先の従業員の例を使うことにします。従業員にはDeveloperDesignerなど、いくつかの異なる種別があるものとします。

interface Employee
{
    public function __construct(string $name, float $salary);
    public function getName(): string;
    public function setSalary(float $salary);
    public function getSalary(): float;
    public function getRoles(): array;
}

class Developer implements Employee
{
    protected $salary;
    protected $name;
    protected $roles;

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

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

    public function setSalary(float $salary)
    {
        $this->salary = $salary;
    }

    public function getSalary(): float
    {
        return $this->salary;
    }

    public function getRoles(): array
    {
        return $this->roles;
    }
}

class Designer implements Employee
{
    protected $salary;
    protected $name;
    protected $roles;

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

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

    public function setSalary(float $salary)
    {
        $this->salary = $salary;
    }

    public function getSalary(): float
    {
        return $this->salary;
    }

    public function getRoles(): array
    {
        return $this->roles;
    }
}

次に、さまざまな種別の従業員で構成された組織Organizationを記述します。

class Organization
{
    protected $employees;

    public function addEmployee(Employee $employee)
    {
        $this->employees[] = $employee;
    }

    public function getNetSalaries(): float
    {
        $netSalary = 0;

        foreach ($this->employees as $employee) {
            $netSalary += $employee->getSalary();
        }

        return $netSalary;
    }
}

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

// 従業員を準備
$john = new Developer('John Doe', 12000);
$jane = new Designer('Jane Doe', 15000);

// 組織に追加
$organization = new Organization();
$organization->addEmployee($john);
$organization->addEmployee($jane);

echo "正味の給与: " . $organization->getNetSalaries(); // 正味の給与: 27000

Decoratorパターン☕

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

あなたは自動車のサービスショップを経営しており、さまざまなサービスを提供しているとします。このとき、料金をどのように計算しますか: サービスを1つ選んでは動的にサービスごとの価格を加算して最終的なコストを計算するでしょう。ここでいう各サービスがdecoratorです。

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

decoratorパターンは、オブジェクトをdecoratorクラスのオブジェクトにラップすることで、実行時にオブジェクトの振舞いを動的に変更できるようにします。

Wikipediaではこうです。

オブジェクト指向プログラミングにおけるdecoratorパターンとは、個別のオブジェクトに静的または動的に振舞いを追加できるようにするデザインパターンである。このとき、同じクラスの別のオブジェクトは影響を受けない。decoratorパターンは、独自の関心対象となっているクラス間で機能を分割できるので、「単一責任の原則(Single Responsibility Principle)」を尊守する場合に有用であることが多い。

プログラム例

ここではコーヒーを例にします。最初にCoffeeインターフェイスとそのシンプルな実装SimpleCoffeeを記述します。

interface Coffee
{
    public function getCost();
    public function getDescription();
}

class SimpleCoffee implements Coffee
{
    public function getCost()
    {
        return 10;
    }

    public function getDescription()
    {
        return 'Simple coffee';
    }
}

必要に応じてオプションで動作を変更できるよう、コードを拡張可能にしたいとします。アドオン(decorator)をいくつか作成してみましょう。

class MilkCoffee implements Coffee
{
    protected $coffee;

    public function __construct(Coffee $coffee)
    {
        $this->coffee = $coffee;
    }

    public function getCost()
    {
        return $this->coffee->getCost() + 2;
    }

    public function getDescription()
    {
        return $this->coffee->getDescription() . '、ミルク';
    }
}

class WhipCoffee implements Coffee
{
    protected $coffee;

    public function __construct(Coffee $coffee)
    {
        $this->coffee = $coffee;
    }

    public function getCost()
    {
        return $this->coffee->getCost() + 5;
    }

    public function getDescription()
    {
        return $this->coffee->getDescription() . '、ホイップ';
    }
}

class VanillaCoffee implements Coffee
{
    protected $coffee;

    public function __construct(Coffee $coffee)
    {
        $this->coffee = $coffee;
    }

    public function getCost()
    {
        return $this->coffee->getCost() + 3;
    }

    public function getDescription()
    {
        return $this->coffee->getDescription() . '、バニラ';
    }
}

それではコーヒーを淹れてみましょう。

$someCoffee = new SimpleCoffee();
echo $someCoffee->getCost(); // 10
echo $someCoffee->getDescription(); // Simple Coffee

$someCoffee = new MilkCoffee($someCoffee);
echo $someCoffee->getCost(); // 12
echo $someCoffee->getDescription(); // Simple Coffee、ミルク

$someCoffee = new WhipCoffee($someCoffee);
echo $someCoffee->getCost(); // 17
echo $someCoffee->getDescription(); // Simple Coffee、ミルク、ホイップ

$someCoffee = new VanillaCoffee($someCoffee);
echo $someCoffee->getCost(); // 20
echo $someCoffee->getDescription(); // Simple Coffee、ミルク、ホイップ、バニラ

Facadeパターン📦

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

パソコンの電源はどうやってオンにしますか?「電源ボタンを押す」ですよね。このように考えられるのも、パソコンが外部に対してシンプルなインターフェイスを提供しているという確信があるからこそです(内部的には電源投入でさまざまな処理が行われています)。複雑なサブシステムへのシンプルなインターフェイス、それがfacadeです(訳注: ファサードと読みます)。

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

facadeパターンは、複雑なサブシステムへのシンプルなインターフェイスを提供します。

Wikipediaではこうです。

facadeとは、大規模なコード(クラスライブラリなど)の本体への単純化されたインターフェイスを提供するオブジェクトである。

プログラム例

先のパソコンの例を使います。Computerクラスを記述します。

class Computer
{
    public function getElectricShock()
    {
        echo "ビリビリ!";
    }

    public function makeSound()
    {
        echo "ピッ!ポッ!";
    }

    public function showLoadingScreen()
    {
        echo "読み込み中...";
    }

    public function bam()
    {
        echo "準備ができました!";
    }

    public function closeEverything()
    {
        echo "ビーッ!ビーッ!ビビビビビ!";
    }

    public function sooth()
    {
        echo "(シーン)";
    }

    public function pullCurrent()
    {
        echo "プシューッ!";
    }
}

次にfacadeを記述します。

class ComputerFacade
{
    protected $computer;

    public function __construct(Computer $computer)
    {
        $this->computer = $computer;
    }

    public function turnOn()
    {
        $this->computer->getElectricShock();
        $this->computer->makeSound();
        $this->computer->showLoadingScreen();
        $this->computer->bam();
    }

    public function turnOff()
    {
        $this->computer->closeEverything();
        $this->computer->pullCurrent();
        $this->computer->sooth();
    }
}

これで、facadeを次のように使えます。

$computer = new ComputerFacade(new Computer());
$computer->turnOn();  // ビリビリ! ピッ!ポッ! 読み込み中... 準備ができました!
$computer->turnOff(); // ビーッ!ビーッ!ビビビビビ! プシューッ! (シーン)

Flyweightパターン🍃

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

露店でフレッシュティーをお飲みになったことはありますか?こうした露店では、一杯ずつお茶を沸かすのではなく、他の客の分もまとめて一度に沸かしてガス代などを節約しているものです。flyweightパターンは要するにこのような「共有」のことです。

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

同じようなオブジェクトをできるだけ共有することで、メモリ使用量や計算コストを最小化します。

Wikipediaではこうです。

コンピュータプログラミングにおけるflyweightは、デザインパターンの一種である。flyweightとは、他の類似のオブジェクトをできるだけ共有することでメモリ使用量を最小限に抑えるオブジェクトを指す。単純な表現を繰り返すとメモリ使用量が許容範囲を超えてしまう状況において、多数のオブジェクトを利用する手法である。

プログラム例

先のフレッシュティーを例に使います。最初にお茶の種類とティーメーカーを記述します。

// キャッシュされるものはすべてflyweightになる。
// ここではお茶の種類がflyweightになる。
class KarakTea
{
}

// Factoryとして振舞い、お茶を保存する
class TeaMaker
{
    protected $availableTea = [];

    public function make($preference)
    {
        if (empty($this->availableTea[$preference])) {
            $this->availableTea[$preference] = new KarakTea();
        }

        return $this->availableTea[$preference];
    }
}

続いてTeaShopを記述し、注文を受けてお茶を出すようにします。

class TeaShop
{
    protected $orders;
    protected $teaMaker;

    public function __construct(TeaMaker $teaMaker)
    {
        $this->teaMaker = $teaMaker;
    }

    public function takeOrder(string $teaType, int $table)
    {
        $this->orders[$table] = $this->teaMaker->make($teaType);
    }

    public function serve()
    {
        foreach ($this->orders as $table => $tea) {
            echo "テーブル# " . $table . "にお茶を出す";
        }
    }
}

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

$teaMaker = new TeaMaker();
$shop = new TeaShop($teaMaker);

$shop->takeOrder('砂糖少なめ', 1);
$shop->takeOrder('ミルク多め', 2);
$shop->takeOrder('砂糖なし', 5);

$shop->serve();
// テーブル# 1 にお茶を出す
// テーブル# 2 にお茶を出す
// テーブル# 5 にお茶を出す

Proxyパターン🎱

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

カードキー(アクセスカード)を使ってドアの鍵を開けたことはありますか?通常、こうしたドアの開け方はいくつもあるものです(カードキーで開ける、ボタンを押してセキュリティをバイパスするなど)。ドアの主要な機能は「ドアが開く」ことですが、いくつかの機能を追加するため、その機能の上にproxyが置かれています。この後のコード例でもう少し詳しく説明します。

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

proxyパターンを使うと、あるクラスが別のクラスの機能を表現できるようになります。

Wikipediaではこうです。

最も一般的な意味でのproxyは、「別の何か」へのインターフェイスとして機能するクラスのことである。proxyは、舞台裏で実際にサービスを提供するオブジェクトにアクセスするために、クライアントから呼び出されるラッパーまたはエージェントオブジェクトである。proxyを使うと、実際のオブジェクトに単に転送したり、追加ロジックを提供したりできるようになる。proxyでは別の機能を提供することも可能である: たとえば、実際のオブジェクトでの操作がリソース集約的である場合にキャッシュしたり、実際のオブジェクト呼び出しでの操作の事前条件をチェックしたりできる。

プログラム例

先のセキュリティドアを例に使います。最初にDoorインターフェイスとその実装を記述します。

interface Door
{
    public function open();
    public function close();
}

class LabDoor implements Door
{
    public function open()
    {
        echo "研究室のドアを開く";
    }

    public function close()
    {
        echo "研究室のドアを閉じる";
    }
}

続いて、必要なすべてのドアをセキュアにするproxyを記述します。

class Security
{
    protected $door;

    public function __construct(Door $door)
    {
        $this->door = $door;
    }

    public function open($password)
    {
        if ($this->authenticate($password)) {
            $this->door->open();
        } else {
            echo "絶対ダメ!開けられません。";
        }
    }

    public function authenticate($password)
    {
        return $password === '$ecr@t';
    }

    public function close()
    {
        $this->door->close();
    }
}

これで以下のように使えます。

$door = new Security(new LabDoor());
$door->open('invalid'); // 絶対ダメ!開けられません。

$door->open('$ecr@t');  // 研究室のドアを開く
$door->close();         // 研究室のドアを閉じる

proxyパターンの他の例として、ある種のデータマッパーの実装も考えられます。たとえば、著者は最近このパターンを用いてMongoDB向けのODM(Object Data Mapper)を作成しました。このときは、マジックメソッド__call()を使って、mongoクラスをラップするproxyを書きました。元のmongoクラスへのすべてのメソッド呼び出しはproxy化されるので、取り出された結果はそのままの形で返されますが、findfindOneデータは、requireされたクラスオブジェクトにマップされ、Cursorの代わりにそのオブジェクトを返すようにしました。

本記事への貢献方法👬

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

License

License: MIT

バックナンバー

関連記事

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


CONTACT

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