第16章 堅牢(SOLID)なオブジェクト設計
SOLID(堅牢)と呼ばれるオブジェクト指向の原則があります。それぞれの頭文字を取ったものです。
- 単一責任原則(Single Responsibility)
- 開放/閉鎖原則(Open/Close Principle)
- リスコフ置換原則(Liskov Substitution Principle)
- インターフェイス分離原則(Interface Segregation Principle)
- 依存関係逆転原則(Dependency Inversion Principle)
これを合わせると、ベストプラクティスの代表です。従うなら、開発するソフトウェアは保守が簡単になり、いつでも拡張が容易になります。
この章では、各原則の詳細を説明していきます。
|
私をSOLIDにして?もしプログラマーがあなたのところにやってきて、「私をSOLIDにしてくれますか?」と尋ねたら、 何を尋ねているのか明確にしましょう。5 |
単一責任原則
SOLID(堅牢)の最初の文字、’S’は”Single Responsibility”の頭文字です。 この原則の内容は:
「すべてのクラスは、単一の責任を持たなくてはならない。 その責任はクラスにより完全にカプセル化される必要がある。 そのサービスは、その責任に厳格に沿わなくてはならない。」
別の考え方をすれば、クラスは一つだけ、唯一の、変更理由を持たなくてはなりません。
このクラスは、コンパイルと市場リポートをユーザーに送信する役割を持っていると言えます。配布方法の変更があればどうなるでしょう?ユーザーがテキストでレポートを欲しがった場合は?もしくは、Webで見たがった場合は?レポートのフォーマットを変更したい場合は?レポートの情報源を変更する場合は?この例のクラスには、こうした多くの理由で変更が起きる可能性があります。
|
AND(〜と)をクラスの目的に使わない単一責任原則を実践する簡単な方法は、 クラスが何を行うのかを定義するために、「〜と」や、「〜して」「および」という類の言葉(AND)を使わないことです。 |
これをコードで示してみましょう。
1 // クラスの目的:市場レポートを組み立て「て」、ユーザーにメールする
2 class MarketingReport {
3 public function execute($reportName, $userEmail)
4 {
5 $report = $this->compileReport($reportName);
6 $this->emailUser($report, $userEmail);
7 }
8 private function compileReport($reportName) { ... }
9 private function emailUser($report, $email) { ... }
10 }
このケースの場合、クラスを2つに分け、どんなレポートを送るのか、誰にどうやって送るのかをより上位の責任者に決定してもらいましょう。
1 // クラスの目的:市場レポートを組み立てる。
2 class MarketingReporter {
3 public function compileReport($reportName) { ... }
4 }
5
6 interface ReportNotifierInterface {
7 public function sendReport($content, $destination);
8 }
9
10 // クラスの目的:ユーザーにレポートをメールする。
11 class ReportEmailer implements ReportNotifierInterface {
12 public function sendReport($content, $email) { ... }
13 }
こっそりとインターフェイスを紛れ込ませたのに気がついましたか?その通り。私はインターフェイスを密かに滑りこませる奴なんですよ。
|
より頑丈なクラス単一責任原則に従うことにより、クラスを頑丈にすることができます。 唯一の関心ごとに焦点をあてることができるため、 クラスの外の機能を変更により、 壊してしまうようなことが少なくなります。 |
開放/閉鎖原則
SOLIDの2番めの文字が表すのは”Open/Closed principle”です。この原則の内容は:
「ソフトウェアのエンティティー(クラス、モジュール、ファンクションなど)は、拡張に対して門戸を開き、 変更に対して門戸を閉じるべきである。」
言い換えれば、一度コードを書いたら、バグ修正以外の理由で、決して変更してはいけないのです。もし機能追加が必要であれば、そのクラスを拡張する必要があります。
この原則はあなたに「どうやったら変更できるか?」を考えることを強要します。ここでも、経験が最高の教師です。以前の状況で共通して使用してきたパターンを基礎として、ソフトウェアの設計方法を学んでいるのです。
|
あまりにも厳格になるなこの原則を厳格に固執すると、その結果、技術の使いすぎに 陥ってしまうことに私は気が付きました。これが起きるのはプログラマーが クラスが利用される可能性全部を考える時に起きます。これを少し考えてみるのは、 良いことです。しかし、あまりに考えすぎると、複雑にする必要がないクラスまで、 余計に複雑にしてしまう結果になります。 もし、この原則の頑固な支持者の気分を害したら、ごめんなさい。けど、ねえ、私はただ、見たままを叫んでいるだけです。6 これは原則で、法律ではありません。 |
そうだとしても、良くある例を学びましょう。あなたはアカウントの請求に取り組んでいて、特に払い戻し(refund)の処理の部分を担当しています。
1 class AccountRefundProcessor {
2 protected $repository;
3
4 public function __construct(AccountRepositoryInterface $repo)
5 {
6 $this->repository = $repo;
7 }
8 public function process()
9 {
10 foreach ($this->repository->getAllAccounts() as $account)
11 {
12 if ($account->isRefundDue())
13 {
14 $this->processSingleRefund($account);
15 }
16 }
17 }
18 }
では、上のコードを確認して行きましょう。アカウントのストレージはリポジトリークラストとして分離され、依存注入されています。いいですね。
不幸なことに、ある日仕事に来てみると、上司が怒っています。管理部門が大きな払い戻しは、担当者による見直しが必要だと決めたのです。うわーー。
では、上のコードのどこが悪いのでしょう?これを変更しなくてはなりません。ですから、これは変更に対して門戸を閉じていません。これをサブクラスに拡張することはできますが、process()のコードのほとんどは重複してしまうでしょう。
ですから、あなたは払い戻し手順をリファクターするのです。
1 interface AccountRefundValidatorInterface {
2 public function isValid(Account $account);
3 }
4 class AccountRefundDueValidator implements AccountRefundValidatorInterface {
5 public function isValid(Account $account)
6 {
7 return ($account->balance > 0) ? true : false;
8 }
9 }
10 class AccountRefundReviewedValidator implements
11 AccountRefundValidatorInterface {
12 public function isValid(Account $account)
13 {
14 if ($account->balance > 1000)
15 {
16 return $account->hasBeenReviewed;
17 }
18 return true;
19 }
20 }
21 class AccountRefundProcessor {
22 protected $repository;
23 protected $validators;
24
25 public function __construct(AccountRepositoryInterface $repo,
26 array $validators)
27 {
28 $this->repository = $repo;
29 $this->validators = $validators;
30 }
31 public function process()
32 {
33 foreach ($this->repository->getAllAccounts() as $account)
34 {
35 $refundIsValid = true;
36 foreach ($this->validators as $validator)
37 {
38 $refundIsValid = ($refundIsValid and $validator->isValid($account));
39 }
40 if ($refundIsValid)
41 {
42 $this->processSingleRefund($account);
43 }
44 }
45 }
46 }
これで、AccountRefundProcessはコンストラクターでバリデーターの配列を受け取ります。次のビジネスルールの変更時には、新しいバリデーターをサッと取り出し、バリデーターの配列に追加、それであなたは平穏安泰となります。
リスコフ置換原則
SOLIDの”L”は”Liskov substitution principle”を表します。この原理の内容は:
「コンピューターのプログラミングにおいて、SがTのサブタイプの場合、タイプTのオブジェクトは そのプログラムの望ましい特性(正当性、動作など)を一切変更すること無く タイプSのオブジェクトと 置き換えることができる。
何だって?他の原則よりも更に混乱を引き起こす、この原則を堅牢な設計の一部だと考える冒険に出たのです。
この原則は本当にすべて「代用可能性(Substitutability)」に関することです。ですが、”Substitutability”の”S”を省略形として採用すると、SOLIDの代わりにSOSIDになります。こんな会話が想像できますか?
プログラマー1:「クラスを設計するなら、S.O.S.I.D.原則に従わなくちゃ。」
プログラマー2:「ソー…セージ?」
プログラマー1:「いいや、SOSID。」
プログラマー2:「シーフード?」
プログラマー1:(Michael Feathersに電話をかける。)「ねえ、もっと良い省略形が必要なようだよ。」
単純に言えばリスコフ置換原則は、クラスを使用するところならどこでも、クラスのサブクラスを使用できるようにプログラムすることを意味しています。
言い換えれば、もし長方形クラスがあるなら、その長方形クラスと、派生した正方形クラスをプログラムで使用するのです。それから、長方形クラスを使用する場所であればどこでも、正方形クラスを使えるようにします。
まだ混乱していますか?私も最初に学んだ時とても混乱したことを覚えています。だって「はあー?当たり前だろう。」ですからね。今でも、まだ理解していないことがあるんじゃないか心配しているんです。こんなに当たり前のことを堅牢な設計の教義に、なぜ入れているんでしょう?
この原則が主張しているサブタイプの前提(タイプの継承)には微妙な側面があるのですが、強調すべきではないでしょう。そして事後条件(良い部分の変更なし)は弱めるべきでありません。他に何かあるのでしょうか?それとスーパータイプが投げるのと同じ(もしくは継承した)例外を投げるサブタイプは持てません。
おお!他に何かあるのでしょうか?リスコフの原則について、他の詳細をここではもう紹介しません。本当に時間を使いすぎたのではないかと心配しているんです。
リスコフの原則は厄介ではありません。
何ですって?
そうです!言った通りです。(技術的に原則は厄介ですから、私は嘘を付いたことになります。しかし、その言い訳をさせてください。)
PHPでは、以下の3ガイドラインに従えば、リスコフの99%は守ったことになります。
1.インターフェイスを使用する
インターフェイスを使ってください。理にかなっているところであれば、どこにでも使ってくだい。インターフェイスを追加しても、オーバーヘッドはそんなに大きくありませんし、リスコフ置換原則に従っていることを保証する抽象化を自然なレベルで達成できる結果を手に入れることができます。
2.インターフェイスには実装の詳細を含めない
インターフェイスを使用するなら、実装の詳細を見せてはいけません。UserAccountInterfaceにストレージがどの程度残っているかという詳細を含める必要がありますか?
3.タイプをチェックするコードに気をつける
特定のタイプにだけ、別の操作を行うコードを書けば、リスコフ置換原則(たぶん、それと他の堅牢原則)を破ることになるでしょう。
以下のコードを考えてください。
1 class PluginManager {
2 protected $plugins; // プラグインの配列
3
4 public function add(PluginInterface $plugin)
5 {
6 if ($plugin instanceof SessionHandler)
7 {
8 // ユーザーがログインしている場合のみ追加
9 if ( ! Auth::check())
10 {
11 return;
12 }
13 }
14 $this->plugins[] = $plugin;
15 }
16 }
この小さいコードのどこが悪いのでしょうか?このタスクはオブジェクトタイプにより、異なった処理を行なっているため、リスコフを破っています。これは仕事をこなす精神に則り、小さなアプリケーションで、よく見られるコードです。これを紹介できて、とっても嬉しいのです。
しかしですね…あるadd()メソッドが何を追加させるのかをなぜ選り好みさせる必要があるのでしょう。皆さんも考えてもらえば、’add()’は多少コントロールマニアであると思いませんか。深呼吸し、コントロールをあきらめさせる必要があります。(ああ。コントロールの逆転のことだな。)
インターフェイス分離原則
SOLIDの’I’が意味しているのは”Interface Segregation Principle”です。インターフェイス分離原則の内容は:
「クライアントは使用しないメソッドへ依存を強要されてはならない。」
普通の英語で表現するなら、太りすぎたインターフェイスを使用するなです。「単一責任原則」をインターフェイスに適用すれば、通常問題はありません。
私達がこの書籍で設計するアプリケーションは小さく、この原則は、多分さほど適用できません。インターフェイスの分離はアプリケーションがもっと大きくなった場合に重要になります。
この原則はドライバーで頻繁に破られます。キャッシュドライバーについて考えましょう。データーの値を一時的に保管することだけしない、本当に単純なキャッシュです。ですから、save()かput()メソッドが必要で、それに対応してload()かget()メソッドも必要です。しかし、キャッシュの設計者が頻繁にやってしまうのは、余計なおまけを付けてしまうことです。次のようにインターフェイスを設計します。
1 interface CacheInterface {
2 public function put($key, $value, $expires);
3 public function get($key);
4 public function clear($key);
5 public function clearAll();
6 public function getLastAccess($key);
7 public function getNumHits($key);
8 public function callBobForSomeCash();
9 }
キャッシュを実装しようとしてみましょう。するとgetLastAccess()は実装不可能だと気づきます。ストレージがサポートしていないからです。同様にgetNumHits()も問題です。それから、callBobForSomeCash(お金についてボブに電話を掛ける)にはお手上げです。「Bobって誰だい?電話代は誰が払うんだい?」インターフェイスを実装するなら、ただ例外を投げることになるでしょう。
1 class StupidCache implements CacheInterface {
2 public function put($key, $value, $expires) { ... }
3 public function get($key) { ... }
4 public function clear($key) { ... }
5 public function clearAll() { ... }
6 public function getLastAccess($key)
7 {
8 throw new BadMethodCallException('not implemented');
9 }
10 public function getNumHits($key)
11 {
12 throw new BadMethodCallException('not implemented');
13 }
14 public function callBobForSomeCache()
15 {
16 throw new BadMethodCallException('not implemented');
17 }
18 }
美しくない。美しくないですよね?
これがインターフェイス分離原則のポイントとなります。代わりに以下のように小さなインターフェイスを作成しましょう。
1 interface CacheInterface {
2 public function put($key, $value, $expires);
3 public function get($key);
4 public function clear($key);
5 public function clearAll();
6 }
7 interface CacheTrackableInterface {
8 public function getLastAccess($key);
9 public function getNumHits($key);
10 }
11 interface CacheFromBobInterface {
12 public function callBobForSomeCash();
13 }
論理的でしょう?
依存逆転原則
SOLIDの最後の’D’は、”Dependency Inversion Principle”を意味します。これは2つの内容を含んでいます。
A. ハイレベルのモジュールは、ローレベルのモジュールに依存してはならない。 両モジュールとも抽象に依存しなくてはならない。
B. 抽象は詳細に依存してはならない。 詳細は抽象に依存しなくてはならない。
すごいですね。でも何を意味しているのでしょうか?
- ハイレベル
- ハイレベルコードはローレベルコードより複雑で、ローレベルコードの機能を利用します。
- ローレベル
- ローレベルコードは基本的な部分を実行し、ファイルシステムへのアクセスや、データベース管理のような操作に集中します。
ローレベルとハイレベルの間には幅があります。例えば、セッション管理はローレベルと考えましょう。でもセッション管理は、さらに別のローレベルコードであるセッションストレージを利用します。
それぞれの関係に置いて、ハイレベルとローレベルがあると考えるほうが便利です。ユーザーのログイン処理のような、アプリケーションの特定の機能に比べれば、明らかにセッション管理はローレベルです。しかし、セッション管理はデータベースのアクセス層に比べればハイレベルです。
「ああ、制御の逆転(IoC)を実装したら、依存逆転なんだ」とか、「いいや。制御の逆転を行なっているものは、依存注入だ」と話しているのを聴いたことがあります。両方の答えとも部分的には正解です。両方の答えとも、依存逆転の実装だからです。
依存逆転原則はクラス依存の分離テクニックを良く表しています。分離することにより、ハイクラスロジックも、ローレベルオブジェクトも、お互いに直接関連しません。代わりに抽象と関係します。
PHPの世界であれば…依存逆転は何により上手く構築できるでしょうか?お考えの通り、インターフェイスです。
全設計原則が、どのようにいっしょに働くのか、興味がありませんか?それと、この原則が、最も使用されないPHPの構造であるインターフェイスをいかに頻繁に活用しているのか、知りたくありませんか?
一例
ユーザー認証は良い例になります。一番上位レベルのコードはユーザーを認証することです。
1 <?php
2 interface UserInterface {
3 public function getPassword();
4 }
5
6 interface UserAuthRepositoryInterface {
7 /**
8 * $usernameのUserInterfaceを返す
9 */
10 public function fetchByUsername($username);
11 }
12
13 class UserAuth {
14 protected $repository;
15
16 /**
17 * リポジトリーを依存注入する
18 */
19 public function __construct(UserAuthRepositoryInterface $repo)
20 {
21 $this->repository = $repo;
22 }
23
24 /**
25 * $usernameと$passwordが有効であればtrueを返す
26 */
27 public function isValid($username, $password)
28 {
29 $user = $this->repository->fetchByUsername($username);
30 if ($user and $user->getPassword() == $password)
31 {
32 return true;
33 }
34 return false;
35 }
36 }
37 ?>
これで、ハイレベルクラスのUserAuthができ、抽象であるUserAuthRepositoryInterfaceとUserInterfaceに関連しています。では、とてもありふれた残り2つの抽象を実装しましょう。
1 <?php
2 class User extends Eloquent implements UserInterface {
3 public function getPassword()
4 {
5 return $this->password;
6 }
7 }
8 class EloquentUserRepository implements UserAuthRepositoryInterface {
9 public function fetchByUsername($username)
10 {
11 return User::where('username', '=', $username)->first();
12 }
13 }
14 ?>
簡単、簡単、超簡単。レモンを絞るぐらい簡単です。
では、UserAuthが使えるように、EloquentUserRepositoryで組み立てるか、自動で注入されるようにEloquentUserRepositoryをインターフェイスに結合しましょう。
LaravelのAuthファサードを使ったこの章の認証サンプルコードで、
混乱しないでください。原則を示すためのサンプルに過ぎません。
Laravelの実装も、これとよく似ていますが、もっと上手くやっています。