よき開発者になるためのS.O.L.I.D.原則(後編)

よき開発者になるためのS.O.L.I.D.原則(後編)

こんにちは、ラクミニです。

引き続き、S.O.L.I.D.の原則です。L以降を解説します。
前回のブログはこちら↓
https://enjoyworks.jp/tech-blog/7285

L : Liskov Substitution Principle / リスコフの置換原則

S が T の派生型であれば、プログラム内で T 型のオブジェクトが使われている箇所は全て S 型のオブジェクトで置換可能

基底型・基底クラスと派生型・派生クラスはいつでも置換可能でないといけない、という原則です。例を見ていきましょう。

基底となるDuckクラスと派生したRubberDuck(ゴム製のアヒル)クラスがあるとします。


class RubberDuck extends Duck
{
    public function quack()
    {
        $person = new Person();

        if ($person->squeezeDuck($this)) {
            return 'The duck is quacking!';
        } else {
            throw new Exception("A rubber duck can't quack on its own.");
        }
    }

    public function fly()
    {
        throw new Exception("A rubber duck can't fly.");
    }

    public function swim()
    {
        $person = new Person();

        if ($person->throwDuckInTub($this)) {
            return 'The duck is swimming!';
        } else {
            throw new Exception("A rubber duck can't swim on its own.");
        }
    }
}

Rubber Duckは人の手を使えば鳴かせたり泳がせたりすることはできますが、本来のアヒルのように飛ぶことはできません。これでは、基底のDuckクラスと置換するとエラーになってしまうので、リスコフの原則に違反していることになります。

では、どうすれば回避できるのか。

まずは、1つ1つの振る舞いをクラスにします。


interface QuackableInterface
{
    public function quack(): string;
}

interface FlyableInterface
{
    public function fly(): string;
}

interface SwimmableInterface
{
    public function swim(): string;
}

そして、RubberDuckクラスをDuckクラスから継承するのではなく、interfaceを実装したクラスに変更します。


class RubberDuck implements QuackableInterface, SwimmableInterface
{
    public function quack(): string
    {
        $person = new Person();

        if ($person->squeezeDuck($this)) {
            return 'The duck is quacking!';
        } else {
            throw new Exception("A rubber duck can't quack on its own.");
        }
    }

    public function swim(): string
    {
        $person = new Person();

        if ($person->throwDuckInTub($this)) {
            return 'The duck is swimming!';
        } else {
            throw new Exception("A rubber duck can't swim on its own.");
        }
    }
}

リスコフの置換原則では、クラスの継承だけでなくinterfaceの実装でも派生型が成り立つと考えるため、これで原則に遵守することができました。

参考:https://qiita.com/yuki153/items/142d0d7a556cab787fad

I : Interface Segregation Principle / インタフェース分離の原則

クライアントは使わないインタフェースの実装を強制されるべきではない

購読しているユーザーにメールを送信する処理を実装するとします。


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

sendメソッドで呼び出されているSubscriberモデルには、メール通知用のメソッド以外にもsubscribeメソッドとunsubscribeメソッドが存在します。


class Subscriber extends Model
{
    use HasFactory;

    public function subscribe()
    {
        # code...
    }

    public function unsubscribe()
    {
        # code...
    }

    public function getNotifyEmail()
    {
        # code...
    }
}

sendメソッドで必要なSubscriberモデルのメソッドはgetNotificatfyEmailメソッドのみ。しかし、Subscriberモデルを直接呼び出しているため、不要なメソッド(subscribeメソッドとunsubscribeメソッド)にも依存している(ubscribeメソッドとunsubscribeメソッドを直接呼び出せてしまう)状態です。Subscriberモデルに修正を加えると、Notificationsクラスのsendメソッドにも影響を及ぼします。このように、不必要なメソッドに依存している状態は、インターフェース分離の原則に違反していることになります。では、どうすれば良いのでしょう?


interface NotifiableInterface
{
    public function getNotifyEmail(): string;
}

getNotifyEmailメソッドだけを持つインターフェースを作ります。そして、Notificationsクラスを以下のように書き換えます。


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

先ほどまでsendメソッドに直接Subscriberモデルを渡していましたが、NotifiableInterfaceをタイプヒントに与えます。こうすることで、NotifiableInterfaceを実装したgetNotifyEmailメソッドしか呼び出せなくなるため、具象クラスへの依存を回避することが出来ます。

D : Dependency Inversion Principle / 依存性逆転の原則

上位のモジュールは下位のモジュールに依存してはならない。どちらのモジュールも、抽象に依存すべきである。
抽象は、実装の詳細に依存すべきではない。実装の詳細が、抽象に依存すべきである。

実践ドメイン駆動設計 p.118

昨日以降に作成されたユーザー情報をjsonで返す処理があるとします。


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

このindexメソッドはControllerにあります。ユーザー情報の取得はeloquentを使っています。Controllerはrequestを受け取ってresponsを返す場所なので、ここでeloquentを使ってDBからデータを取得するのは単一責任の原則に違反しています。また、直接Userモデルを呼び出しすことで依存関係になっているため、依存性逆転の原則にも違反しています。

これを回避するために、RepositoryInterfaceを使います。


interface UserRepositoryInterface
{
    public function getAfterDate(Carbon $data);
    public function create(object $userData);
}

class UserRepository implements UserRepositoryInterface
{
    public function getAfterDate(Carbon $data)
    {
        return User::where('created_at', '>=', $data)->get();
    }

    public function create($userData)
    {
        $user = new User();
        $user->name = $userData->name;
        $user->email = $userData->email;
        $user->password = bcrypt($userData->password);
        $user->save();

        return $user;
    }
}

UserRepositoryInterfaceを実装したUserRepositoryにユーザ情報を取得する処理を移します。

そして、先ほどのControllerのindexメソッドには、Userモデルの代わりにUserRepositoryInterfaceをタイプヒントとして渡します。


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

このようにUserRepositoryInterface(抽象クラス)に依存することで、例えUserRepository(具象クラス)でデータの取得方法に何を選択しても、Controller側では特に意識することなく$userを使うことが出来ます。

2回にわたってSOLIDの原則について解説しました。かなり大雑把な解説であり、私自身、広く浅く理解した程度です。詳細な説明をしている書籍やブログはたくさんあるので、もう少し詳しく知りたい方は是非調べてみてください。あくまで原則ですが、実際の開発でもこれに則った実装ができるようになりたいですね〜。

以上、よき開発者になるためのSOLIDの原則でした。

一覧へ戻る

最新記事