##『Clean Architecture 達人に学ぶソフトウェアの構造と設計』を読んで
有名な書籍ですね。
が、なかなか抽象度が高く、例として出されるコードもC言語だったりするので隙間時間の読書で理解っていうのはちょっと難しくないですか?
実際の開発でこの原則に則った実装ができるよう、1原則=1記事で解説していきます。
- SRP(単一責任の原則) ← イマココ
- OCP(オープン・クローズドの原則)
- LSP(リスコフの置換原則)
- ISP(インターフェイス分離の原則)
- DIP(依存関係逆転の原則)
##単一責任の原則は何を言っているのか?
当たり前ですが、あるシステムが1度実装されたら、2度と変更されないものだとしたら、単一責任の原則を遵守する理由はそこまでありません。
この原則に違反する弊害は、「システムの変更や修正」をトリガーに発生します。
####アクターの異なるコードは分割するべきである
アクターとはシステムにおける登場人物の事ですね。とあるホテルの予約サイトを例に考えましょう。
「予約サイトの運営会社」「ホテルの運営会社」「ユーザー」ざっとこのくらいのアクターは存在しそうです。
1つのコードが、この3者のうちの2者以上に対して、何らかの責任を負っている状態はNGなわけです。
####「クラスを変更する理由が1つのみ」になるよう設計する
すなわちサービスにおける1つのクラスは、
「1つだけの概念において不正な処理を防ぎ、正常な動作を保証する」ものでなくてはならないという事です。
####クラスの凝集性を高め、疎結合にする
1つのクラスの責任範囲を明確に定め、その割り当てられた責任範囲に集中させる事を、「クラスの凝集性を高める」と言います。
抽象的ですね。
これを具体的に落とし込むために、「ホテルの予約サイトにおいてユーザーが宿泊できる部屋を予約する」というケースを考えてみます。
##ダメな実装例
今回の予約サイトでは、
「いつ予約してもホテル側の提示料金から5%OFFで宿泊できる」
「宿泊料金にポイントを利用できる」
という仕様が当初想定されていたとします。
これらをReservationというクラスに処理をまとめた実装例が下記です。
<?php
class Reservation
{
// ユーザーの検索条件(人数・チェックイン日・チェックアウト日)に基づいて、宿泊可能な部屋を取得する
public function getStayAbleRooms($searchParam)
{
//検索する
//検索結果の各部屋に対して、料金を算出する
}
// 各部屋の料金を算出する
public function calcRoomFee($rooms)
{
// 各部屋の金額を算出した上で、割引後の金額を算出する
$amount = $this->getRegularyDiscount($rooms->fee);
return $amount;
}
// 通常の割引処理を行う
public function getRegularyDiscount($amount)
{
return $amount * 0.95;
}
// ユーザーが選択した部屋を予約確定にする
public function saveReservation($reserveParam)
{
// ポイントを割り引く
$this->discountByPoint($reserveParam->amount, $reserveParam->point);
// 予約データを保存する
}
// ユーザーの予約確定前に、利用ポイント分金額を下げる
public function discountByPoint($amount, $usePoint)
{
return $amount - $usePoint;
}
}
まあなんでこうなったという感じですが、ユーザーの予約の一連の流れを1つのクラスに安易にまとめてしまったのでしょう。しかしこれでは
・割引の割合が変更される、もしくは割引に何らかの条件がつく
・ポイント利用に上限額が設けられる
など複数の異なる理由によりこのクラスのコードが変更される可能性が考えられます。
一旦これでリリース時点では動いていたとします。ではここに
「特定の期間はさらに5%OFFになる」という要求がきたとします。仕様を理解しているエンジニアは、もうすでに通常時の5%割引が実装されている事を知っています。そのため下記のように実装してしまいました。
<?php
class Reservation
{
// ユーザーの検索条件(人数・チェックイン日・チェックアウト日)に基づいて、宿泊可能な部屋を取得する
public function getStayAbleRooms($searchParam)
{
//検索する
//検索結果の各部屋に対して、料金を算出する
}
// 各部屋の料金を算出する
public function calcRoomFee($rooms)
{
// 各部屋の金額を算出した上で、割引後の金額を算出する
$amount = $this->getRegularyDiscount($rooms->fee);
//特定の期間内なら、さらに5%OFFにする!
$amount = $this->getRegularyDiscount($amount); //追加!!
return $amount;
}
// 通常の割引処理を行う
public function getRegularyDiscount($amount)
{
return $amount * 0.95;
}
// ユーザーが選択した部屋を予約確定にする
public function saveReservation($reserveParam)
{
// ポイントを割り引く
$this->discountByPoint($reserveParam->amount, $reserveParam->point);
// 予約データを保存する
}
// ユーザーの予約確定前に、利用ポイント分金額を下げる
public function discountByPoint($amount, $usePoint)
{
return $amount - $usePoint;
}
}
何という安直な実装。。笑
そろそろ地獄が始まりそうです。
そして、通常割引の仕様だけ変更依頼がきます。
「5%ではなく、常に500円OFFとする」というものです。
これを担当するエンジニアはgetRegularyDiscountが割引処理を行なっている事を理解します。
そして、処理を「金額の95%を算出する」から「500減算する」というものに変更します。しかし不幸なことに特定期間の割引でもgetRegularyDiscountが流用されていることに気づきませんでした。
<?php
class Reservation
{
// ユーザーの検索条件(人数・チェックイン日・チェックアウト日)に基づいて、宿泊可能な部屋を取得する
public function getStayAbleRooms($searchParam)
{
//検索する
//検索結果の各部屋に対して、料金を算出する
}
// 各部屋の料金を算出する
public function calcRoomFee($rooms)
{
// 各部屋の金額を算出した上で、割引後の金額を算出する
$amount = $this->getRegularyDiscount($rooms->fee);
//特定の期間内なら、さらに5%OFFにする!
$amount = $this->getRegularyDiscount($amount); // 5%OFFにしたいのに500円OFFされてしまう
return $amount;
}
// 通常の割引処理を行う
public function getRegularyDiscount($amount)
{
return $amount - 500; // 変更!!
}
// ユーザーが選択した部屋を予約確定にする
public function saveReservation($reserveParam)
{
// ポイントを割り引く
$this->discountByPoint($reserveParam->amount, $reserveParam->point);
// 予約データを保存する
}
// ユーザーの予約確定前に、利用ポイント分金額を下げる
public function discountByPoint($amount, $usePoint)
{
return $amount - $usePoint;
}
}
####何がダメなのか?
このReservationクラスはあまりに多くのことに責任を背負いすぎています。
宿泊予約という行為が完結するためには、部屋・料金・割引・ポイント・期間限定割引など多くの概念が存在することが考えられますが、
その全ての責任をReservationクラスに背負わせてしまったこと、これが単一責任の原則に反しています。
<?php
class SerchRoom
{
// ユーザーの検索条件(人数・チェックイン日・チェックアウト日)に基づいて、宿泊可能な部屋を取得する
public function getStayAbleRooms($searchParam)
{
//検索する
//検索結果の各部屋に対して、料金を算出する
}
}
class RoomFee
{
// 各部屋の料金を算出する
public function calcRoomFee($rooms)
{
// 各部屋の金額を算出した上で、割引後の金額を算出する
//特定の期間内なら、さらに5%OFFにする!
}
}
class RegularyDiscount()
{
// 通常の割引処理を行う
public function calcRegularyDiscount($amount)
{
return $amount - 500;
}
}
class SpecialyDisCount()
{
// 期間限定の特別割引を行う
public function calcSpecialyDiscount($amount)
{
return $amount * 0.95;
}
}
class DiscountByPoint()
{
// ユーザーの予約確定前に、利用ポイント分金額を下げる
public function discountByPoint($amount, $usePoint)
{
return $amount - $usePoint;
}
}
class Reservation
{
// ユーザーが選択した部屋を予約確定にする
public function saveReservation($reserveParam)
{
// 予約データを保存する
}
}
少々極端な例ですが、これなら各クラスを変更する理由は1つだけとなり、クラスの凝集性が高まったと言えるのではないでしょうか。
Reservationクラスは「予約データに関する事」だけに責任を持つようになりました。また各クラスは自分が責任を負っていること以上の事は知りません。
##ありがとうございました
ちょっともっといい例を思いついたら、随時ソースコードは修正していこうかなと。
次はSOLID原則の2つ目、**「オープン・クローズドの原則」**についてまとめます。