こんにちは、エンジニアの王です。
今回はデザインパターンと、デザインパターンの中の「Strategy」について紹介したいと思います。
デザインパターンとは?
端的にいうと、「よくある問題へのよくある解決策」です。
ここでは、あくまでもソフトウェア設計の場合に限定しているのですが、さまざまなコンテキストで活かせる概念です。
「今までの経験上、この手の問題なら、この方法(パターン)でやればうまくいくよ!」という経験則は誰にでもあると思います。それがゲームの場合なら「攻略法」、料理の場合なら「レシピ」、語学の場合なら「定型文」だったりします。
ソフトウェア設計の場合、特にオブジェクト指向プログラミングにおいて言うなら、「デザインパターン」とは、過去のソフトウェア設計者が失敗に失敗を重ね、試行錯誤の中から導き出した再利用しやすいノウハウの集大成のようなものです。
そう、要するに、柔軟性、拡張性、再利用性、可読性が高く、保守や機能追加などが後から容易にできるコードを書くためのノウハウです。まさに先人たちの汗と努力の結晶です! 生かさないままにしておくのは、もったいなさすぎです!
なお、言語によってはよく使われるパターンや実装方法も違ってきますが、考え方は一緒です。その考え方を学ぶには、Javaのような重たい言語とJavaScriptのようなカジュアルな言語の間くらいに位置しているPHPを選ぶのがほどよい選択だと思います。
Strategy パターン
このシリーズの第1回目は、「Strategy」(別名:Policy) パターンについて解説したいと思います。Strategy パターンはソフトウェア設計で「デザインパターン」の概念を普及させた引き金だと言われています。そのくらい汎用性の高いパターンです。
継承のやりすぎ問題
では、試しに「鳥類」のクラスデザインについて考えてみましょう。分かりやすくするためにUML図を使って説明します(GenMyModelというオンラインアプリを使っています)。
まず「Birds」という名前のスーパークラス(抽象クラス)を作ることから始めます。
次に抽象クラスである「Birds」を拡張して、サブクラス「Raven」(カラス)と「Owl」(フクロウ)を作成します。うむ……今のところは問題なさそうですな!
では、新たに「駝鳥」というクラスが欲しくなったので、同じ要領で作成しますね。
ここでちょっと問題が起きました。気づきましたか? 駝鳥は飛ぶことができないのに、flyというメソッドを継承してしまっているのです。それどころか、実のところ駝鳥は「走る」ので、「fly」の代わりに「run」メソッドがあってもおかしくないですよね?
他にもペンギンや鶏など、飛べないけど走れる鳥類が将来的に追加されるかもしれません。そうなったらそれぞれのサブクラスで「run」の実装が重複します。さらに「run」の実装を変えたいとき、それぞれのサブクラスの「run」を変えなければならないので、再利用性が低下し、管理もどんどん煩雑になっていくことが予想されます。
このように、クラスを作った当初は問題がなくても、後々、こうした予期せぬ問題にぶつかったりします。継承を使っていて、なおかつpublicなメソッドであるため、継承を余儀なくされます。
では、どう解決しようか? 確実に鳥類に共通する部分(eat,sleep,describe)だけ残しておいて、作成する鳥類によっては変動しそうな部分(fly,run)はインターフェイス化しても良さそうですね。
飛べる鳥なら「Flyable」を、走れる鳥なら「Runable」、両方できるのなら両方を実装します。
どうでしょうか? これでだいぶ「いい感じ」になったでしょう?
しかし! 残念ながら、やはり問題が残ります。図面上では確かに良くはなりましたが、コードの重複は依然として避けられないです。
なぜなら、インターフェイス化したところで、結局、すべてのサブクラスでrunやflyメソッドを実装しなければならないため、実装部分のコードの再利用はまるでできないのです!
Strategy パターンで問題解決!
インターフェイス化作戦は、変動しやすい部分をクラスから分離するという意味では、なかなかいい線を行っているとは思いませんか?
変動しやすい部分をなるべく分離することで得られるメリットはこちらです。
- スーパークラスへの変更を避けることができる(変更によりサブクラスに及ぼす影響が軽減。つまり、OCPに準拠している)
- 臨機応変に組み合わせることができる(不要な継承を避けることができる)
- 「共通」と「変動」部分が明確になり、見通しがよくなる
これらのメリットに、なんとかして「再利用性の向上」が加わればシメシメですよね!
インターフェイスを「継承」する
Strategy パターンでは具体的なメソッドを継承せず、抽象的な「インターフェイス」を「継承」することで、コードの再利用性を向上させます。
やり方としては、あるインターフェイス(FlyBehavior,RunBehavior)を実装したインスタンス(flyBehavior,runBehavior)をメンバーに持ち、該当のメソッド(fly,run)の中でそのインスタンスを使ってある挙動をさせる。抽象的すぎて申し訳ないのですが、下記のUML図と実際のコードを読めば、分かってもらえると思います。
実装コード
で、結局どういうこと? コードを見れば全ての疑問が晴れると思います!
<?php
// 飛行のインターフェイス
interface FlyBehavior {
public function fly();
}
interface RunBehavior {
public function run();
}
// 具体的なクラス
class FlyHigh implements FlyBehavior {
public function fly() {
echo "高く飛んでいる";
}
}
// 具体的なクラス
class FlyLow implements FlyBehavior {
public function fly() {
echo "低く飛んでいる";
}
}
// 具体的なクラス
class RunFast implements RunBehavior {
public function run() {
echo "飛べないよ";
}
}
// 鳥類の「ベースクラス」
abstract class Birds {
abstract function __construct();
abstract function display();
public function sleep() {
echo "寝ている";
}
public function eat() {
echo "餌を食べている";
}
}
// 飛ぶタイプの鳥類
abstract class FlyableBirds extends Birds implements FlyBehavior {
/** @type FlyBehavior */
protected $flyBehavior;
public function fly() {
$this->flyBehavior->fly();
}
}
// 走るタイプの鳥類
abstract class RunableBirds extends Birds implements RunBehavior {
/** @type RunBehavior */
protected $runBehavior;
public function run() {
$this->runBehavior->run();
}
}
// 具体的な鳥のクラス(フクロウ)
class Owl extends FlyableBirds {
function __construct() {
$this->flyBehavior = new FlyLow();
}
function display() {
echo 'フクロウです';
}
}
// 具体的な鳥のクラス(カラス)
class Raven extends FlyableBirds {
function __construct() {
$this->flyBehavior = new FlyHigh();
}
function display() {
echo 'カラスです';
}
}
// 具体的な鳥のクラス(駝鳥)
class Ostrich extends RunableBirds {
function __construct() {
$this->runBehavior = new RunFast();
}
function display() {
echo '駝鳥です';
}
}
$ostrich = new Ostrich();
$raven = new Raven();
$owl = new Owl();
?>
<h1>駝鳥</h1>
<p>
<?php $ostrich->run(); ?>
</p>
<p>
<?php $ostrich->eat(); ?>
</p>
<p>
<?php $ostrich->sleep(); ?>
</p>
<p>
<?php $ostrich->display(); ?>
</p>
<h2>カラス</h2>
<p>
<?php $raven->fly(); ?>
</p>
<p>
<?php $raven->eat(); ?>
</p>
<p>
<?php $raven->sleep(); ?>
</p>
<p>
<?php $raven->display(); ?>
</p>
<h2>フクロウ</h2>
<p>
<?php $owl->fly(); ?>
</p>
<p>
<?php $owl->eat(); ?>
</p>
<p>
<?php $owl->sleep(); ?>
</p>
<p>
<?php $owl->display(); ?>
</p>
「IS-A 関係」と「HAS-A 関係」
「IS-A 関係」
「継承」はオブジェクト指向プログラミングでのコアコンセプトです。
通常、「Aクラス is a Bクラス」の関係の場合、継承が使われます。例えば、「電子書籍 is a 本」であれば、電子書籍は本を継承します。この場合、電子書籍と本は「IS-A」関係にあります。こういった時、「継承」は正しい選択の場合が多い。
が、しかし……上記の例のような「継承のやりすぎ問題」もしばしば起こりますので、注意が必要ということです。不適切な継承は柔軟性を損なわせ、後からの変更(メンテナンスや機能追加)を難しくしてしまいます。
「HAS-A 関係」
「IS-A 関係」に対して、「HAS-A」関係とは、上記の「カラス」の例で言うと、「カラス has a FlyBehavior」、すなわち、「カラスは、飛行能力を持っている」ということですね。この場合、「継承」ではなく、「合成」を使います。要は必要なだけ組み合わせてやるってことです。飛べる鳥なら「飛行能力」を合成、走れる鳥なら「走る能力」を合成、両方の能力を持っていれば両方とも合成すればいいわけです。組み合わせ方次第で色んなバージョンの鳥が簡単に作れるのですね! 下手な「継承」よりずっと柔軟性が高いでしょう?
ポイントとなるのは、無闇に「継承」を使わないこと。「合成」と「継承」の選択肢が両方存在する場合、原則的には「合成」を使うようにすれば後々は楽です。
まとめ
上記の方法で解決できる問題と解決策はこちらです。
解決できる問題は?
- 継承したくないメソッドを継承することでサブクラスが肥大化し、管理が難しくなる
- メソッドの再利用性が低下し、コード量が増える
- 挙動を変更したい時、スーパークラスの変更を余儀なくされるため、サブクラスが思わぬ影響を受けてしまう危険性を孕んでいる
解決策
- 「継承」ではなく、「合成」を使う
- 「合成要素」は同一のインターフェイスを実装するようにすることで互換性アップ
余談ですが、Javaなどの静的型付けプログラミング言語の場合、ランタイムにメソッドの挙動をすり替えるためにしばしば用いられる手法でもあります。ランタイムで臨機応変にアルゴリズム(戦略→strategy)を切り替えることから「Strategy」という名が付けられたと思われます。
PHPの場合はそもそも動的にメソッド名を変えられるので、あまり意味はないのですがね……JavaScriptやRubyのような「インターフェイス」という考え方をそもそも持たない言語はMixinパターンで似たようなことができますので、気になる方は調べてみてくださいね。
【バックエンジニア王の効率化講座】
※ console.logのラッパーを作成して業務を効率化する方法
※ Retina対応のスプライトを作成するときに便利なCompass用のmixinを作ってみた
※ Sublime Text3で構文エラーをチェックするプラグインが超絶便利
※ Web屋ならチェックしておきたい!作業効率が激変するChrome DevToolsの便利な使い方まとめ
※ JavaScriptで`undefined`の代わりに、`void 0`を使ったほうがいい理由
LIGはWebサイト制作を支援しています。ご興味のある方は事業ぺージをぜひご覧ください。