Laravelのコードで学ぶSOLIDの原則

Laravelのコードで学ぶSOLIDの原則

たまたま観た動画が、before-after形式でコードサンプルが書かれてあってわかりやすかったのでまとめる。
原題は「SOLIDの設計原則を用いてより良い開発者になるには」

SOLIDの原則とは?

詳しくは割愛
postd.cc

単一責任の原則

動画内でのコードは、Controller内でバリデーションとDBへの保存を行っている。

<?php
    public function store(Request $request, User $user)
    {
        $validateData = $request->validate([
            // バリデーションルール
        ]);

        $user->name = $request->name;
        $user->mail = $request->mail;
        $user->password = bcrypt($request->password);
        $user->save();

        return response()->json(['user' => $user], 201);
    }

Controllerの主な役割は入力(Request)を受けとり出力(Response)すること。
Controller内でバリデーションロジックを書いたり、Eloquentを使用して保存・更新している場合、Controllerは責務を負いすぎている。

<?php
    public function store(StoreUserRequest $request, UserRepository $userRepository)
    {
        $user = $userRepository->create($request);
        return response()->json(['user' => $user], 201);
    }

LaravelはFormRequestがあるのでFormRequestにバリデーションロジックを書くこと。
ユーザの保存はRepository層を挟んでDBへのアクセスを隠蔽すること。

開放・閉鎖原則

よくあるコード。
支払い方法のタイプを受けとり、typeに応じて支払い方法を変えるというもの。
Requestを受け取っているということは、あるController内に書かれたメソッドなのだと思う。

<?php
    public function pay(Request $request)
    {
        $payment = new Payment();

        // typeによって分ける
        if ($request->type === 'credit') {
            $payment->payWithCredit();
        } elseif ($request->type === 'paypal') {
            $payment->payWithPaypal();
        }
        // ...
    }
<?php
class Payment
{
    public function payWithCredit()
    {

    };

    public function payWithPaypal()
    {

    };
    //...
}

支払い方法が増えるたびに、payメソッド内にif文を追加し、Paymentクラス内に新しい支払い方法に応じた支払いロジックを追加しなければならない。

<?php
interface PayableInterface
{
    public function pay();
}

class PaypalPayment implements PayableInterface
{
    public function pay()
    {
        // implements
    }
}

class CreditPayment implements PayableInterface
{
    public function pay()
    {
        // implements
    }
}

class PaymentFactory
{
    public function initializePayment($type)
    {
        if ($type === 'credit') {
            return new CreditrCardPayment();
        } elseif ($type === 'paypal') {
            return new PaypelPayment();
        }
    }
}
<?php
    public function pay(Request $request)
    {
        $paymentFacory = new PaymentFactory();
        $payment = $paymentFacory->initializePayment($request->type);
        $payment->pay();
    }

支払い方法ごとにクラスを分割し、支払い方法が増えるたびにPaymentクラスのコードが長くなることがなくなった。
各クラスはPayableInterfaceをimplementsすることで、payメソッドを実装することを強制している。
またファクトリパターンを使うことで、利用者が実装の詳細を知る必要がなくなるとともに、新しい支払い方法が増えてもpayメソッドを変更する必要がなくなり変更箇所を一箇所に留めることができる。

リスコフの置換原則

よくあるアヒルクラスの例。
ゴム製のおもちゃのアヒルは飛べないので、派生型(RubberDuckクラス)は基底型(Duckクラス)と置換可能ではないので、リスコフの置換原則に違反している。
Duckクラスを継承するのではなく、各ふるまいをクラス化し、implementsすればよい。

scrapbox.io

chapter1 · kyamashiro/head-first-design-pattern Wiki · GitHub

インタフェース分離の原則

購読しているユーザに通知を送るコードの例。

<?php
class Subscriber extends Model
{
    public function subscribe()
    {

    }

    public function unsubscribe()
    {

    }

    public function getNotifyEmail()
    {

    }
}

class Notifications
{
    public function send(Subscriber $subscriber, $message)
    {
        Mail::to($subscriber->getNotifyEmail())->queue();
    }
}

Notificationsクラスのsendメソッドは使う必要のないSubscriberクラスのsubscribeメソッドやunsubscribeメソッドも使用できる。

<?php
interface NotifiableInterface
{
    public function getNotifyEmail();
}

class Notifications
{
    public function send(NotifiableInterface $subscriber, $message)
    {
        Mail::to($subscriber->getNotifyEmail())->queue();
    }
}

NotifiableInterfaceをタイプヒントに与えることで、NotificationsクラスのsendメソッドはgetNotifyEmailメソッドのみを使用する。

 依存性逆転の原則

<?php
    public function index(User $user)
    {
        $users = $user->where('created_at', '>=', Carbon::yesterday())->get();
        return response()->json(compact('users'), 200);
    }

ControllerでEloquentを使用して値を取得したりしている場合、単一責任の原則にも違反しているし、依存性逆転の原則にも違反している。

<?php
    public function index(UserRepositoryInterface $user)
    {
        $users = $user->getAfterDate(Carbon::yesterday());
        return response()->json(compact('users'), 200);
    }

/users のGETリクエストをController内でEloquentで取得して返す部分はDBに依存しないように、Repository層を作ってUserRepositoryInterfaceをタイプヒントに指定して抽象に依存するようにしている。
UserRepositoryInterfaceに依存するようにすることで、UserRepositoryInterfaceをimplementsしているクラスであればEloquentを使用していようとMongoDBを使用していようとDBを意識しないで$userを使用することができる。

SOLIDの原則にとらわれるな

  • SOLIDの原則はあくまでprinciplesであってrulesではない
  • SOLIDの原則のために過剰な分割をすることは避けよ
  • SOLIDの原則を達成しようとするのではなくメンテナビリティ(拡張・修正のしやすさ)を得るためにSOLIDの原則を使え

SOLIDの原則はSOLID principlesというらしいが、principlesってどういう意味なんだろうか。ここでのprinciplesとrulesの違いは何なのか。

  • principle

    (ものがよって立つ根本的な)原理、原則、原理、(科学上の)原理、法則、(機械などが動く)原理、仕組み、(人の行動のための)主義、根本方針、主義

  • rule

    (社会・会などで秩序・機能を維持するため相互に守るべき)規則、規定、ルール、規則、(科学・芸術上の)法則、方式、(数学上の)規則、解法、常習、習慣

いまいちよくわからないので、英英辞典で調べたら、

  • principle

    a moral rule or belief about what is right and wrong, that influences how you behave.

人の振る舞いに影響を与える、正悪についての道徳的な原理、信条

  • rule

    an official instruction that says how things must be done or what is allowed, especially in a game, organization, or job.

特定の競技や組織、仕事の中で、許されていることやしなければならない方法などを示す公式の教え

principlesは方針としての原則、rulesは守るべき規約の意味と捉えました。
カナヅチを持つとすべてがクギに見えるように、SOLIDの原則を知るとついついなんでも当てはめて使いたくなっちゃいますが、あくまで原則であると。SOLIDは原則であって目的ではないと。
実際SOLIDの原則を当てはめるとクラスやファイルが増えてしまいますが、大した規模のアプリでないのならわざわざ分割しないでControllerでEloquentを使用したり、ロジックベタ書きをしてもいいと思う。
しかし、その時は後で必要になったときに分割しようと思うのだけど、それがなされないままどんどん条件分岐が増えてしまうパターンが経験上多い・・・。
「後でやろう」は大体やらないのだから、最初から愚直にちゃんと分割していくべきかなと思う。ユニットテスト書きやすいし。
ファイルやクラスが増えるとかえって複雑になるだろへの反論はこのスライド見ればいいと思います。