クマのブログ

つまづいたところ、学びを書いていきます

はじめてのデザインパターン #1

はじめに

ちょこちょこ自分で機能を作ることはできてきたけど、設計がイケてなかったりして苦戦していた時に先輩に相談したところ「デザインパターンとか知っておくと損ないかもね」と教えてもらった。

とはいえ「デザインパターンって何?」状態だったので調べてみたのでメモ。

参考

自分がPHP使いということもあり、以下を参考にしようとした

PHPによるデザインパターン入門

とおもったら、廃版になってて著者の方が本内容をはてブに掲載してくれていたのでこちらでインプット

Do You PHP はてブロ

感謝です。

デザインパターンとは

ソフトウェアを何度も設計していると、あちらこちらで共通する機能や問題があることに気づくことがあります。...。経験を積んだ開発者であれば、「こういった問題を解決するには、こういう構造でプログラムを書けばうまくいく」といったノウハウを持っているものです。

このノウハウを、他の開発者が再利用できるようカタログ化したものがデザインパターンです。

一言でいうとオブジェクト指向プログラミングにおいて開発者がソフトウェア設計する上での「型」

記事内では

「設計のコツの虎の巻」

と表現されている。

メリット

じゃあ、「デザインパターン学んでどうなるのさ?」という話で行くといくつか列挙されていますが個人的には

  • 再利用性が向上する
  • 保守性が向上する

が大きいメリットだなと感じます。

再利用性が向上

クラスの一つ一つの独立性が担保される設計になるので、クラス同士が疎結合になり再利用性が高くなる

保守性が向上する

独立性が高くなるとクラスが持つ役割がしっかり分担されるので、急な仕様変更や機能追加の対応がしやすくなる

例えば、LaravelでDBから値を取得する際にAカラムではなくBカラムの値をwhereする、みたいな修正があった時に

// 当初仕様
$user = User::where('Aカラム', $A_value)->first();
↓
// 仕様変更
$user = User::where('Bカラム', $B_value)->first();

これが一か所のクラスのメソッドにまとまって(抽象化されて)いて、呼び出し側は全て抽象メソッドを呼び出していれば修正は一か所ですが、上記の記述をそのまま呼び出し側に書いていたら修正規模は100、200...箇所と膨大なものになる、という話。

デメリット

デメリットもいくつか掲載されていますが例えば

  • 開発者同士がデザインパターンを知らないとコミュニケーションコストが生まれる
  • 使いこなすまでに時間がかかる

など、物理的に仕方ないかな、ということが多いです。

ただ、

  • 無理にでもパターンを適用しようとする
  • パターンのクラス図をそのまま適用しようとする

あたりはデザインパターンを利用する上で注意点かなと思います。

冒頭に「デザインパターンは虎の巻」と記載されていても絶対的なものではなく、むしろやりすぎると逆にイケていない設計になる、ということも注意しておく必要あり、と学びました。

どんなものがあるか

とは言え、やはり同じプロダクトの中で開発者同士が共通の型を使って、再利用性の富んだ設計ができるようになるのは大きな魅力なので、どんなものがあるか見ていきたい。

※第1回なので超基礎のみにしています。

#1, テンプレートメソッドパターン

こんな時に使う

今目の前に以下のようなクラスが2つあるとします。

↓ListDisplayクラス

<?php
require_once 'AbstractDisplay.class.php';

class ListDisplay
{
  /**
     * 表示するデータ
     */
    private $data;

    /**
     * コンストラクタ
     * @param array 表示するデータ
     */
    public function __construct($data)
    {
        $this->data = $data;
    }

    /**
     * クライアントから呼び出されるメソッド
     */
    public function display()
    {
        $this->displayHeader();
        $this->displayBody();
        $this->displayFooter();
    }

    /**
     * ヘッダを表示する
     */
    protected function displayHeader()
    {
        echo '<dl>';
    }

    /**
     * ボディ(クライアントから渡された内容)を表示する
     */
    protected function displayBody()
    {
        foreach ($this->getData() as $key => $value) {
            echo '<dt>Item ' . $key . '</dt>';
            echo '<dd>' . $value . '</dd>';
        }
    }

    /**
     * フッタを表示する
     */
    protected function displayFooter()
    {
        echo '</dl>';
    }
}

↓TableDisplayクラス

<?php
require_once 'AbstractDisplay.class.php';

class TableDisplay
{
  /**
     * 表示するデータ
     */
    private $data;

    /**
     * コンストラクタ
     * @param array 表示するデータ
     */
    public function __construct($data)
    {
        $this->data = $data;
    }

    /**
     * クライアントから呼び出されるメソッド
     */
    public function display()
    {
        $this->displayHeader();
        $this->displayBody();
        $this->displayFooter();
    }

    /**
     * ヘッダを表示する
     */
    protected function displayHeader()
    {
        echo '<table border="1" cellpadding="2" cellspacing="2">';
    }

    /**
     * ボディ(クライアントから渡された内容)を表示する
     */
    protected function displayBody()
    {
        foreach ($this->getData() as $key => $value) {
            echo '<tr>';
            echo '<th>' . $key . '</th>';
            echo '<td>' . $value . '</td>';
            echo '</tr>';
        }
    }

    /**
     * フッタを表示する
     */
    protected function displayFooter()
    {
        echo '</table>';
    }
}

↓利用側(クライアント)

<?php
require_once 'ListDisplay.class.php';
require_once 'TableDisplay.class.php';

$data = array('Design Pattern',
              'Gang of Four',
              'Template Method Sample1',
              'Template Method Sample2');

$display1 = new ListDisplay($data);
$display2 = new TableDisplay($data);

$display1->display();
echo '<hr>';
$display2->display();

TableDisplayクラスとListDisplayクラスを見てあなたは思います。

「なんだかすごく似ている処理をしているぞ。。。」

この時に使うのがテンプレートメソッド。

使い方

  • 複数の似た処理のクラスの抽象化クラスを1個作る
  • 抽象クラスには似た処理のクラス(具象クラス)でやってほしい処理のabstractでの定義と共通の処理を定義
  • 抽象クラスをexxtendsした似た処理のクラス(具象クラス)では、abstractでのメソッドの具体的な処理の実装を定義
  • 利用側(クライアント)で、具象クラス経由で抽象クラスの共通処理を呼び出す

言葉で話してもわかりにくいのでソースコードにて

修正後

↓AbstractDisplayクラス(新規作成) ここに共通の処理と具象クラスで処理してほしいメソッドを定義

<?php
/**
 * AbstractClassクラスに相当する
 */
abstract class AbstractDisplay
{
    /**
     * 表示するデータ。下記コンストラクタで値が格納される
     */
    private $data;

    /**
     * コンストラクタ。実際にインスタンス化するのは具象クラスなので、具象クラスをnewしたら呼ばれる
     * @param array 表示するデータ
     */
    public function __construct($data)
    {
        $this->data = $data;
    }

    /**
     * template methodに相当する(共通処理)
     */
    public function display()
    {
        $this->displayHeader();
        $this->displayBody();
        $this->displayFooter();
    }

    /**
     * データを取得する(共通処理)
     */
    public function getData()
    {
        return $this->data;
    }

    // 具象クラスで実装するメソッドたち
    /**
     * ヘッダを表示する
     * サブクラスに実装を任せる抽象メソッド
     */
    protected abstract function displayHeader();

    /**
     * ボディ(クライアントから渡された内容)を表示する
     * サブクラスに実装を任せる抽象メソッド
     */
    protected abstract function displayBody();

    /**
     * フッタを表示する
     * サブクラスに実装を任せる抽象メソッド
     */
    protected abstract function displayFooter();

}

↓ListDisplayクラス

データをリストで表示するための処理を書く

<?php
require_once 'AbstractDisplay.class.php';

/**
 * 具象クラスに相当する
 */
class ListDisplay extends AbstractDisplay
{
    // 抽象クラスで実装することを強制したabstractで定義したメソッドたち(これがないとエラーになる)

    /**
     * ヘッダを表示する
     */
    protected function displayHeader()
    {
        echo '<dl>';
    }

    /**
     * ボディ(クライアントから渡された内容)を表示する
     */
    protected function displayBody()
    {
        foreach ($this->getData() as $key => $value) {
            echo '<dt>Item ' . $key . '</dt>';
            echo '<dd>' . $value . '</dd>';
        }
    }

    /**
     * フッタを表示する
     */
    protected function displayFooter()
    {
        echo '</dl>';
    }
}

↓TableDisplayクラス

データをテーブルで表示するための処理を書く

<?php
require_once 'AbstractDisplay.class.php';

/**
 * ConcreteClassクラスに相当する
 */
class TableDisplay extends AbstractDisplay
{
        // 抽象クラスで実装することを強制したabstractで定義したメソッドたち(これがないとエラーになる)

    /**
     * ヘッダを表示する
     */
    protected function displayHeader()
    {
        echo '<table border="1" cellpadding="2" cellspacing="2">';
    }

    /**
     * ボディ(クライアントから渡された内容)を表示する
     */
    protected function displayBody()
    {
        foreach ($this->getData() as $key => $value) {
            echo '<tr>';
            echo '<th>' . $key . '</th>';
            echo '<td>' . $value . '</td>';
            echo '</tr>';
        }
    }

    /**
     * フッタを表示する
     */
    protected function displayFooter()
    {
        echo '</table>';
    }
}

↓利用側(クライアント)・・・変更なし

<?php
require_once 'ListDisplay.class.php';
require_once 'TableDisplay.class.php';

$data = array('Design Pattern',
              'Gang of Four',
              'Template Method Sample1',
              'Template Method Sample2');

$display1 = new ListDisplay($data);
$display2 = new TableDisplay($data);

$display1->display();
echo '<hr>';
$display2->display();
  • やっているのは「継承」を使った実装
  • 重複部分を抽象クラスに実装することで、実際にインスタンス化する具象クラスの記述量を減らすことができ、拡張性もUP
    • 似たような処理(ただ文字列を羅列するだけの処理など)が欲しくなったときに同じように具象クラスのメソッドを実装してあげればいい
  • 呼び出し側に影響が出ないので、呼び出し側は抽象クラスのメソッドを使っていることを意識しなくていいのは嬉しい

おわりに

  • Google検索では「デザインパターン 古い」などと予測検索出てきますが、僕みたいなイケていない設計をしてしまうペーペーにとってはいい「虎の巻」になるなと思っている。
  • 実際にこれを実務で使いましたが、かなり良かった。
    • 今回は6つの具象クラスの共通部分を抽象クラスに移行できたので具象クラスが非常にわかりやすく再利用性の高さも実感しました。
  • スイマセン、意外と1パターンでも盛沢山だったので、続きは次回に回します。。。