Vapor Trail

明るく楽しく元気よく

Strategyパターン

Strategyパターン

  1. 目的
    様々なアルゴリズムを、それが発生するコンテキストに応じて使い分けられるようにする。

  2. 問題
    要求を行うクライアントや操作対象データによって、適用するアルゴリズムを選択する。

  3. 解決策
    該当アルゴリズムの実装からアルゴリズムを個別に切り出す。これによりコンテキストによる選択が可能になる。

  4. 問題例
    方言のモードは設定可能
    設定している方言によって出力を切り替える

パターン適用前

f:id:kyamashiro:20181223150331p:plain

<?php
class Client
{
    public function anOperation()
    {
        $kyotoPerson = new DialectSpeaker(DialectSpeaker::KYOTO);
        $kyotoPerson->sayWelcome();
        $kyotoPerson->sayThanks();
    }
}

<?php
class DialectSpeaker
{
    const KYOTO = 1;
    const NAGOYA = 2;

    /**
     * @var int
     */
    private $dialect;

    /**
     * DialectSpeaker constructor.
     * @param $dialect
     */
    public function __construct(int $dialect)
    {
        $this->dialect = $dialect;
    }

    public function sayWelcome()
    {
        switch ($this->dialect) {
            case self::KYOTO :
                echo "おいでやす";
                break;
            case self::NAGOYA :
                echo "いりゃあせ";
                break;
        }
    }

    public function sayThanks()
    {
        switch ($this->dialect) {
            case self::KYOTO :
                echo "おおきに";
                break;
            case self::NAGOYA :
                echo "ありがとう";
                break;
        }
    }
}

パターン適用

ストラテジーパターンを適用する前に、どの部分がストラテジーアルゴリズムに当たるのかを考える必要がある。 f:id:kyamashiro:20181223150403p:plain

<?php
interface Dialect
{
    public function sayWelcome(): void;

    public function sayThanks(): void;
}

<?php
class KyotoDialect implements Dialect
{
    public function sayWelcome(): void
    {
        echo "おいでやす";
    }

    public function sayThanks(): void
    {
        echo "おおきに";
    }
}

<?php
class NagoyaDialect implements Dialect
{
    public function sayWelcome(): void
    {
        echo "いりゃあせ";
    }

    public function sayThanks(): void
    {
        echo "ありがとう";
    }
}

<?php
class DialectSpeaker
{
    const KYOTO = 1;
    const NAGOYA = 2;

    /**
     * @var Dialect
     */
    private $dialect;

    /**
     * DialectSpeaker constructor.
     * @param int $dialect
     */
    public function __construct(int $dialect)
    {
        switch ($dialect) {
            case self::KYOTO :
                $this->dialect = new KyotoDialect();
                break;
            case self::NAGOYA :
                $this->dialect = new NagoyaDialect();
                break;
        }
    }

    public function sayWelcome()
    {
        $this->dialect->sayWelcome();
    }

    public function sayThanks()
    {
        $this->dialect->sayThanks();
    }
}

パターンを適用したことにより、実際の処理はDialectインターフェースを実装したKyotoDialect, NagoyaDialectクラスのメソッドによって実行される。

  • メリット
    • 条件文が複数から1つになる 方言を見て種類を切り替えるswitch文がDialectSpeakerクラス内のsayWelcome()とsayThanks()メソッドの2箇所に存在していた。 もしsayGoodBye()というメソッドを新たに追加した場合、switch文が増えることになる。それに対して、パターン適用後は、方言の種類を見て処理を切り替える箇所は、DialectSpeakerクラスのコンストラクタの1箇所のみ。
    • 方言による処理がひとまとまりになる
      方言を戦略(アルゴリズム)と捉え、これを別クラスにしたことで、方言の具体的な処理をサブクラスに記述できる。京都弁に問題があったら、KyotoDialectを修正するだけで済む。

オブジェクト指向のこころ (SOFTWARE PATTERNS SERIES)

オブジェクト指向のこころ (SOFTWARE PATTERNS SERIES)

増補改訂版Java言語で学ぶデザインパターン入門

増補改訂版Java言語で学ぶデザインパターン入門

Javaデザインパターン徹底攻略 (標準プログラマーズライブラリ)

Javaデザインパターン徹底攻略 (標準プログラマーズライブラリ)

独習デザインパターン

独習デザインパターン

PHPによるデザインパターン入門 - Strategy〜戦略を切り替える - Do You PHP はてな

Adapterパターン

Adapterパターン

  1. 目的
    修正することのできない既存オブジェクトを、特定のインターフェースに適合させる。 既存のクラスを修正することなく、適切なインターフェース(メソッド)を追加することができる。

  2. 問題
    使用したいデータや振る舞いが既存システム内に存在しているものの、そのインターフェースが正しくない場合。

  3. 解決策
    必要なインターフェースを保持したラッパーをAdapterによって提供する。

問題例

<?php
class MyNameViewer
{
    public function viewName(MyPersonB $person): void
    {
        echo $person->getName();
    }
}
<?php
class MyPersonB
{
    /**
     * @var string
     */
    private $name;

    /**
     * MyPersonB constructor.
     * @param string $name
     */
    public function __construct(string $name)
    {
        $this->name = $name;
    }

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

以上のような既存のプログラムがあり、 viewName(MyPersonB $person)はMyPersonBクラスの型が指定されている。

<?php
$viewer = new App\MyNameViewer();
$viewer->viewName(new App\MyPersonB("John"));

John
と表示される。

<?php
class MyPersonA
{
    /**
     * @var string
     */
    private $firstName;
    /**
     * @var string
     */
    private $lastName;

    /**
     * MyPersonA constructor.
     * @param string $firstName
     * @param string $lastName
     */
    public function __construct(string $firstName, string $lastName)
    {
        $this->firstName = $firstName;
        $this->lastName = $lastName;
    }

    /**
     * @return string
     */
    public function getFirstName(): string
    {
        return $this->firstName;
    }

    /**
     * @return string
     */
    public function getLastName(): string
    {
        return $this->lastName;
    }
}

このMyPersonAオブジェクトをMyNameViewerに渡して表示したいときにどうするか。

Fatal error: Uncaught TypeError: Argument 1 passed to App\MyNameViewer::viewName() must be an instance of App\MyPersonB, instance of App\MyPersonA given

当然MyPersonAを入れてもエラーになる。

こういうときにAdapterパターンを使う。

<?php
class Adapter extends MyPersonB
{
    /**
     * @var MyPersonA
     */
    private $person;

    /**
     * Adapter constructor.
     * @param MyPersonA $person
     */
    public function __construct(MyPersonA $person)
    {
        $this->person = $person;
    }

    public function getName(): string
    {
        return $this->person->getFirstName() . $this->person->getLastName();
    }
}

MyPersonBをかぶせたAdapterを作成する。

<?php
$adapter = new Other\Adapter(new \Other\MyPersonA("Ken", "Thompson"));
$viewer = new Other\MyNameViewer();
$viewer->viewName($adapter);

アダプティクラス(MyPersonBクラス)の仕様を修正することなく、別のインターフェースへの拡張を容易にする。 元々、MyPersonBクラスがFirstNameとLastNameを両方表示していれば問題なかった。このレベルならMyPersonBに追加したほうが早い。 インターフェースを使用したパターン。

<?php
interface IMyPerson
{
    public function getName(): string;
}
<?php
class MyPersonA
{
    /**
     * @var string
     */
    private $firstName;
    /**
     * @var string
     */
    private $lastName;

    /**
     * MyPersonA constructor.
     * @param string $firstName
     * @param string $lastName
     */
    public function __construct(string $firstName, string $lastName)
    {
        $this->firstName = $firstName;
        $this->lastName = $lastName;
    }

    /**
     * @return string
     */
    public function getFirstName(): string
    {
        return $this->firstName;
    }

    /**
     * @return string
     */
    public function getLastName(): string
    {
        return $this->lastName;
    }

    public function getName(): string
    {
        return $this->getFirstName() . $this->getLastName();
    }
}
<?php
class MyPersonB implements IMyPerson
{
    /**
     * @var string
     */
    private $name;

    /**
     * MyPersonB constructor.
     * @param string $name
     */
    public function __construct(string $name)
    {
        $this->name = $name;
    }

    /**
     * @return string
     */
    public function getName(): string
    {
        return $this->name;
    }
}
<?php
class Adapter implements IMyPerson
{
    /**
     * @var MyPersonA
     */
    private $person;

    /**
     * Adapter constructor.
     * @param MyPersonA $person
     */
    public function __construct(MyPersonA $person)
    {
        $this->person = $person;
    }

    public function getName(): string
    {
        return $this->person->getName();
    }
}
<?php
class MyNameViewer
{
    /**
     * @param IMyPerson $person
     */
    public function viewName(IMyPerson $person): void
    {
        echo $person->getName();
    }
}
<?php
$adapter = new Other\Adapter(new \Other\MyPersonA("Ken", "Thompson"));
$viewer = new Other\MyNameViewer();
$viewer->viewName($adapter);

『オブジェクト指向のこころ』

オブジェクト指向のこころ (SOFTWARE PATTERNS SERIES)

オブジェクト指向のこころ (SOFTWARE PATTERNS SERIES)

まえがき

デザインパターンオブジェクト指向プログラミングの本当の姿を知り、習熟するにはかなりの努力が必要となる。
本書は、概念の根底に横たわっている、基本的な原理と動機、すなわち「やること」と「その理由」が理解できたときに、一気にその概念の理解が進むという信念・経験に基づいている。
そして、デザインパターンを考察することで、「オブジェクト指向の本当の姿」というものが理解できるようになる。

デザインパターンは孤立して存在しているのではなく、他のデザインパターンと強調し、より堅牢なアプリケーションを作成するための土台になるということも理解できるはず。 →保守が容易で、柔軟かつ完成度の高いソフトウェアを作成する準備が整う。

  • デザインパターンは個々の部品として積み上げていくものではなく、協調させて使用すべきもの

    問題を解決するためには、パターンをつむぎ合わせていかなくてはならない。
    ソフトウェア中のパターンはデザインパターンとして洗い出されているため、これらは設計(デザイン)時に適用されるものであると考えていたのです。つまりパターンというものは、クラス間の適切な関連として、設計時に導入されるものだと考えていたわけです。
    私の失敗は、問題領域からクラスを抽出し、その後、それらクラス群をつなぎ合わせて最終的なシステムを作ろうとしていたがために起こったのです。

  • 同時に学ぶメリット

デザインパターンの学習にはオブジェクト指向設計に関する十分な基礎が必要であると言われていた。 実際には、オブジェクト指向設計デザインパターンを同時に学んだ生徒は、単にオブジェクト指向設計だけを学んだ生徒よりも短期間でオブジェクト指向設計を学習できる。

  • 身についたとは

デザインパターンが見えてくる」ということは、デザインパターンによって解決できそうな問題を見るだけで、パターンから学んだ原則や戦略を用いて、パターンで表現された解決策が浮かんでくるようになるということです。

第1章 オブジェクト指向パラダイム

この章の目的

エキスパートが実践しているオブジェクト指向設計方法論を理解するための準備体操。
オブジェクト指向パラダイムは、構造化プログラミングの欠点を補完するために生まれてきたもの。
構造化プログラミングの欠点を明確にし、OOPの利点をより良く捉え、そのメカニズムをさらに深く理解する。

1. オブジェクト指向パラダイム以前

  • 機能分解(functional decomposition)
    機能分解=問題を小さな機能にブレークダウンし、その問題を構成する機能要素の洗い出しを行う。 大きな問題をそれらを構成する小さな問題に分割する。

  • 機能分解の落とし穴
    分解した機能を適切な順序で呼び出す、「メイン」プログラムが必要になる。
    メインプログラムにはすべてを正しく動作させる、機能の組み合わせと呼び出し順序を正しく制御するという、大きな責任が課せられる。
    →これがソースコードを複雑にする。

  • 委譲(delegation)
    部分機能に対してそれ自体の振る舞いに関する責任をもたせ、実行支持を行うだけで、後を任せる。
    機能分解でプログラムを作成すると将来の変更に対して、柔軟に対応することが難しい。

『多くのバグはソースコードの変更によって生み出されている。』
どれだけ頑張ったとしても、どれだけうまく分析したとしても、ユーザから全ての要件を引き出すことなんてできっこありません。明日のことなんて誰にも判らず、万物は流転していくのです。これは絶対不変の真理なのです。

  • 要求における問題

    私の30年にわたるソフトウェア開発経験で、要求について学んだ大事なことといえば、要求は常に変化するということです。

要求の変更にうまく対処できるソースコードを記述している開発者なんて、ほとんどいない。
変化は発生する。それに対応することが大事。

要求の変化に対して愚痴をこぼすよりも、開発プロセスを変え、変化に対して効率的に取り組めるようにするべきなのです。

ソースコードを工夫すれば、要求の変化から生まれる影響を最小化することも可能。

  • 変化に対応する
    巨大な関数をモジュールに分割する。
    モジュール化戦略=モジュールを用いて影響を局所化する
    図形を表示する処理。
例 :
入力 : 図形のタイプと詳細
switch (図形のタイプ)
    case 正方形 : 正方形の表示処理
    case 円形 : 円形の表示処理

新しく三角形の図形を表示したいという要求がでても、このモジュールだけを修正すればよい。
しかし、入力が異なる形式のタイプと詳細では対応できない。

  • 2つの重要な問題
    • 「低い凝集度」
      凝集度が低いクラスとは、関係のない作業が数多く詰め込まれたクラス。いわゆる神オブジェクト。
    • 「高い結合度」
      結合度が高い場合、好ましくない副作用(=機能やそれが使用するデータの変更により、他の機能に悪影響が及ぶ)を引き起こす。

機能分解を用いた場合、ある機能やデータを変更すると他の機能や他のデータに影響が及び、それがまた他の機能に影響を及ぼしていくという修正の連鎖が起こる。

  • 要求の変更に取り組む
    例 : 大学で開催されるセミナーの講師として任命される

構造化プログラミングの場合、以下の手順を取る。

1. 学生のリストを取得する
2. リストの学生に対して以下の処理を行う
    1. 学生の次のセミナーを調べる
    2. そのセミナーの教室を調べる
    3. 今の教室から次のセミナーの教室までの経路を調べる
    4. その学生に教室への生き方を伝える

現実にはこんなことはせず、セミナーの教室へ行く方法のポスターを教室の後ろに貼って、学生に自分で行動してもらう。
つまり、学生が自身の振る舞いに責任を持つ。責任を移行する(=委譲)。

2. オブジェクト指向パラダイム

問題領域を機能に分解するのではなく、オブジェクトに分類していく。

  • 利点 オブジェクト自体に責任をもたせて、物事を定義できる。オブジェクト内のデータによってその状態を識別でき、オブジェクト内のコードによって、適切な機能を実現できる。

優れた設計におけるオブジェクトは、明確に定義された責任を自分自身に保持しています。
オブジェクトには責任が課され、自らのことについて責任をもつことになるため、やってほしいことをオブジェクトに伝える方法が必要となります。

他のオブジェクトからも呼び出し可能なメソッドの集合を公開インターフェースと呼ぶ。
オブジェクト指向アプローチの場合、

1. 学生のコレクション(集合体)を実体化する
2. コレクションに対して、各学生を次の教室に移動させる旨の指示を出す
3. コレクションは、各学生に対して次の教室に移動する旨を伝える
4. 各学生は、
    1. 次の教室がどこか調べる
    2. 行き方を調べる
    3. そこに移動する

他の種類の学生を追加する場合にどうするか?
コレクションは様々な学生型を保持する必要がある。
コレクションに様々な種類の学生を参照させるには、どうすればよいか?
→ある型のグループを包含するような汎用の方を用意する。

f:id:kyamashiro:20181217001040p:plain
Studentオブジェクト
Student(学生)クラスは抽象、RegularStudent(学部生)クラス・GraduateStudent(大学院生)クラスは具象。is-aの関係にある。
この場合、派生クラスは、抽象クラスが定義した共通メソッドを使用するか、自らが実装したメソッドを使用するのか決定できる。

  • カプセル化
    公開インタフェースという考えからカプセル化の概念が導出される。
    public/protected/private
    カプセル化は内部のデータ構造を外部に公開しないという単なるデータ隠蔽ではなく、あらゆる種類の隠蔽を含む。
    Studentという抽象クラスによって、そのクラスから派生した具象クラスの型情報が隠蔽される。

  • ポリモーフィズム
    poly=多くの
    morph=形態
    Student(学生)は種類によって、異なった行動形態を取る。