こちらは ミライトデザイン Advent Calendar 2025 の 9 日目の記事となっています。
はじめに
この記事は書籍 Clean Architecture を読んだ内容を筆者なりにまとめ、個人の意見を追加したものです。
SOLID 原則とは
Robert Cecil Martin さん通称ボブおじさんがまとめた、
オブジェクト指向プログラミングにおける5つの重要な設計原則の頭文字を取ったものです。
- Single Responsibility Principle (単一責任の原則)
- Open/Closed Principle (開放閉鎖の原則)
- Liskov Substitution Principle (リスコフの置換原則)
- Interface Segregation Principle (インターフェース分離の原則)
- Dependency Inversion Principle (依存性逆転の原則)
SOLID 原則を守ることのメリット
一言で言えば、ソフトウェアの品質を向上させる ことができる。
具体的には次のようなシステムを作ることを目的としているもの。
- 変更に強い
- 理解しやすい
- 再利用可能
S ... Single Responsibility Principle (単一責任の原則)
「モジュールを変更する理由はたった一つであるべき。」
モジュールというといまいちピンとこない人もいるかもしれませんが、ここではクラスや、メソッドと考えてもらって問題ないと思います。
よくある勘違いとしてあるのが
「クラスやメソッドは一つのことだけをするべき。」だと受け取ってしまうというものです。
私も初めはそう思っていました。
PHP のレガシーコードでよくあるような、一つのクラスやファイルの中で複数のことをするのではなく、それぞれのクラスやメソッドに役割ごとに分割するようなイメージをしていました。
例えば、ユーザーの登録処理であれば、一つのクラスに、
- ユーザーが入力したフォームのバリデーション処理
- ユーザーを DB に追加する処理
- ユーザー登録完了のお知らせメールの送信処理
があるのではなく、それぞれを別のクラスに分けて分割するようなことをイメージしていました。
確かに、これも大切ですが、単一責任の原則とはニュアンスが異なります。
何が違うのでしょうか?
書籍 Clean Architecture では次のように記載されています。
モジュールはたった一つのアクターに対して責務をおうべきである。
ここでの、アクターとは、ユーザーやステイクホルダーのグループのことです。
もう一つ例を見てみましょう。
ある通販サイトで、商品の金額を計算するクラスがあったとします。
金額計算クラスの仕様は次のようになっています。
- 購入した商品の金額の合計値を計算
- 金額の合計が 30,000 円以上なら 10% off
- 送料は東京なら 1,000 円、それ以外なら 2,000 円
- 送料は、コンビニ受け取りなら 10% off
class 請求金額計算
{
public function 請求金額を計算する(array $金額リスト, string $住所, string $受け取り方法): int
{
// 金額の合計値を計算
$合計金額 = array_sum($金額リスト);
// 金額の合計が 30,000 円以上なら 10% off
if ($合計金額 >= 30000) {
$合計金額 = $this->割引計算($合計金額);
}
// 送料計算
$送料 = $this->送料計算($住所, $受け取り方法);
return $合計金額 + $送料;
}
private function 送料計算(string $住所, string $受け取り方法): int
{
// 送料は東京なら 1,000 円、それ以外は 2,000 円
$送料 = ($住所 === '東京') ? 1000 : 2000;
// 受け取り方法がコンビニ受け取りなら、送料が 10% off
if ($受け取り方法 === 'コンビニ受け取り') {
$送料 = $this->割引計算($送料);
}
return $送料;
}
private function 割引計算(int $金額): int
{
return (int)($金額 * 0.9);
}
}
一見問題なさそうに見えます。
このクラスは、請求金額を計算するという1つのことだけをしていて、DB 接続もメールの送信もしていません。
割引処理は、商品の割引も送料の割引も 10% off です。
同じロジックのコードが複数あるのは良くないので一つのメソッドにまとめています。
ただ、このクラスは送料計算と、商品の料金計算という2つの責務があります。
- 送料の金額や割引率の決定をしているのは、運送部門の責任者
- 商品の割引率の決定をしているのはマーケティング部門の責任者
だったとします。つまりこのクラスは複数のアクターに対して責務を負っていることになります。
とはいえ、どんな問題があるのでしょうか?
いくつか考えられる、例を挙げます。
今回はシンプルな例なので、もしかしたら大したことないと思うかもしれませんが、プロダクトコードはさらに複雑になる可能性も考慮して見ていただけたらと思います。
まずは、責務の異なる同じロジックのコード(割引計算のロジック)をまとめてしまっていることです。
マーケティング部門で商品の割引率を変更したとします。さらにユーザーの話題性を狙って 30% off にします。
当然この話は運送部門には関係ありません。コンビニ受け取りの割引率は 10% のままです。
開発者が、この割引計算のメソッドが、商品の割引と送料の割引に使われていることに気が付かなければ、大きな損害を生むような不具合になってしまいます。
または、不具合にならなくても、次のような可読性を下げるようなコードが生まれてくるかもしれません。
private function 割引計算(int $金額, bool $送料計算): int
{
if ($送料計算) {
return (int)($金額 * 0.9);
}
return (int)($金額 * 0.7);
}
また、このファイルは複数の理由で更新されることになります。
運送部門の開発チームとマーケティング部門の開発チームが別なこともあるでしょう。
それぞれのチームが異なる理由でコードを変更するので、マージの際の差分が大きくなることがあるかもしれません。その際の作業が不具合の原因になることも考えられるでしょう。
それでは、単一責任の原則にしたがってコードを書き直してみます。
class 商品の料金計算
{
public function 商品の料金を計算する(array $金額リスト): int
{
// 金額の合計値を計算
$合計金額 = array_sum($金額リスト);
// 金額の合計が 30,000 円以上なら 10% off
if ($合計金額 >= 30000) {
$合計金額 = $this->商品の割引計算をする($合計金額);
}
return $合計金額;
}
private function 商品の割引計算をする(int $金額): int
{
return (int)($金額 * 0.9);
}
}
class 送料計算
{
public function 送料の計算をする(string $住所, string $受け取り方法): int
{
// 送料は東京なら 1,000 円、それ以外は 2,000 円
$送料 = ($住所 === '東京') ? 1000 : 2000;
// 受け取り方法がコンビニ受け取りなら、送料が 10% off
if ($受け取り方法 === 'コンビニ受け取り') {
$送料 = $this->送料の割引計算をする($送料);
}
return $送料;
}
private function 送料の割引計算をする(int $金額): int
{
return (int)($金額 * 0.9);
}
}
class 請求金額計算
{
public function __construct(
private 商品の料金計算 $商品の料金計算,
private 送料計算 $送料計算,
) {
}
public function 請求金額を計算する(array $金額リスト, string $住所, string $受け取り方法): int
{
return $this->商品の料金計算->商品の料金を計算する($金額リスト) + $this->送料計算->送料の計算をする($住所, $受け取り方法);
}
}
リファクタという意味では、もっとこうした方がいいなどはあるでしょう。
- 金額リストは array でいいの?
- 送料、合計金額への再代入はやめた方がいいんじゃない?
- 請求金額計算はインターフェースに依存した方がいいんじゃない?
とか。
ここでは一旦おいておいて、単一責任の原則を満たしているか(クラスたった一つのアクターに対して責務を負っているか)という観点で見ると、
商品の料金計算と料金クラスはクラスに分割され、それぞれのアクター(理由)に対してのみ責任を負っていて、修正する際にもそれぞれのクラスを修正すればいいようになっています。
ただ、それぞれの割引金額を計算する同じロジックの処理が複数箇所に記載されています。
これは問題ないのでしょうか?
少なくとも私は、たとえ同じロジックのコードでも変更される理由が異なるのであれば、コードは分割されているべきだと思っています。
たまたまロジックが同じだけで、本質としては違うものだと考えているからです。
たとえば、今回、商品の割引と、送料の割引がたまたま 10% で同じですが、アクターが異なるため、送料の割引だけ 5% になる。などといったことは十分に考えられます。
コードが共通化されている場合は、リファクタ前のコードと同じく、仕様の変更によって呼び出し元によって if 文が増えたり、不具合の原因となることが考えられます。
割引率のみそれぞれのクラスに持って、ロジックは共通にした方がいい。という考えもできますが、その場合も同じく、今回たまたま同じだけじゃないかという視点は必要だと思っています。
たとえば、送料の方は xx % の割引ではなく、単純に 500 円引き、のようになったりする可能性はないか。などです。
逆に、このシステムを使用している会社では割引は 10% で統一されている。などという話があるのであれば、ここのコードは統一されているべきでしょう。
このあたりが個人的に設計で難しいところだと思っているのですが、挙動の話ではなくドメイン、そのシステムが使われる業務がどうなっているのかという知識がなければ適切な設計は難しいと思っています。
終わり
明日は ucan さんの Codex 関連の記事らしいです!!