SOLID Principles in Laravel

2019-05-08

【シューマイ】Tech Lead Engineerから最新技術を学べ!Laravel編

About Me

Ryuta Hamasaki

Laravel / Angular / Vue.js

Laracon US 2018

クラシコム =「北欧、暮らしの道具店」を運営

  • EC + メディア
  • 月間PV 1,600万
  • 月間UU 150万
  • 社内で開発・運用 💪
  • Laravel + Vue.js

管理画面はLaravel + Angular

SOLID Principles

オブジェクト指向プログラミング
において、メンテナンスしやすい
プログラムを作るための5つの原則

Robert C. Martin (Uncle Bob)

Clean Coder, Clean Architectureの人。
去年のLaraconのトリ。

SOLID Principles

  • Single Responsibility Principle
  • Open Closed Principle
  • Liskov Substitution Principle
  • Interface Segregation Principle
  • Dependency Inversion Principle

Single Responsibility Principle

Single  Responsibility Principle

  • 1つのクラスは1つのことだけに責任を
    持つべき
  • クラスを変更する理由は複数存在してはならない
  • 責任を適切に分けることで、読みやすくメンテナンスしやすいコードになる

事例:ECサイトの商品を仕入元に発注

仕入元

Web App

Slack通知

発注者

class OrderProcessor
{
    public function __construct(Slack $slack) {
        $this->slack = $slack;
    }

    public function process(Order $order)
    {
        if (! $order->getAmount() > 0) {
            throw new Exception('Order amount must be more than zero.');
        }

        $order->place();

        $message = $this->getMessage($order);
        $this->slack->to('#order')->send($message);
    }

    protected function getMessage(Order $order) : string
    {
        $format  = '@%s Order has been processed. OrderID: %d';
        $userName = $order->getUserName();
        $message  = sprintf($format, $userName, $order->getId());
        
        return $message;
    }
}

Before

  • Slack通知のロジックが含まれている
  • 通知チャンネルやメッセージが変わったらコードの修正が必要
interface OrderNotificationInterface
{
    public function notify(Order $order) : void;
}
class SlackNotification implements OrderNotificationInterface
{
    public function __construct(Slack $slack) {
        $this->slack = $slack;
    }

    public function notify(Order $order) : void
    {
        $message = $this->getMessage($order);
        $this->slack->to('#order')->send($message);
    }

    protected function getMessage(Order $order) : string
    {
        $format = '@%s Order has been processed. OrderID: %d';
        $userName = $order->getUserName();
        $message  = sprintf($format, $userName, $order->getId());
        
        return $message;
    }
}
class OrderProcessor
{
    public function __construct(OrderNotificationInterface $notification) {
        $this->notification = $notification;
    }

    public function process(Order $order)
    {
        if (! $order->getAmount() > 0) {
            throw new Exception('Order amount must be more than zero.');
        }

        $order->place();

        $this->notification->notify($order);
    }
}

After

  • Slack通知のロジックを別クラスに持たせた
  • 通知チャンネルやメッセージが変わってもコードの修正は不要
  • OrderProcessorは、通知方法がSlackであることすら知る必要はない

Open Closed Principle

Open Closed Principle

  • Code should be open for extension
    but closed for modification
  • 新しいコードをゼロから書くより、
    既存のコードにロジックを追加する機会の方が多い → 肥大化・複雑化
  • 新しいコードを書く感覚で、素早く安全に機能を追加できるようにしよう
class OrderProcessor
{
    public function __construct(OrderNotificationInterface $notification) {
        $this->notification = $notification;
    }

    public function process(Order $order)
    {
        if (! $order->getAmount() > 0) {
            throw new Exception('Order amount must be more than zero.');
        }

        $order->place();

        $this->notification->notify($order);
    }
}
class OrderProcessor
{
    public function __construct(OrderNotificationInterface $notification) {
        $this->notification = $notification;
    }

    public function process(Order $order)
    {
        if (! $order->getAmount() > 0) {
            throw new Exception('Order amount must be more than zero.');
        }

        if (! $order->isApproved()) {
            throw new Exception('Order is not approved.');
        }

        $order->place();

        $this->notification->notify($order);
    }
}
class OrderProcessor
{
    public function __construct(OrderNotificationInterface $notification) {
        $this->notification = $notification;
    }

    public function process(Order $order)
    {
        if (! $order->getAmount() > 0) {
            throw new Exception('Order amount must be more than zero.');
        }

        if (! $order->isApproved()) {
            throw new Exception('Order is not approved.');
        }

        if (! $order->isCancelled()) {
            throw new Exception('Cancelled order cannot be processed.');
        }

        $order->place();

        $this->notification->notify($order);
    }
}

Before

  • バリデーションルールが増えるたびに
    既存コードにロジックを追加し、肥大化
  • Unitテストがどんどん複雑になる
  • Open for modification, but closed for extension
interface OrderValidationInterface
{
    public function validate(Order $order) : void;
}
class OrderHasAmount implements OrderValidationInterface
{
    public function validate(Order $order) : void
    {
        if (! $order->getAmount() > 0) {
            throw new Exception('Order amount must be more than zero.');
        }
    }
}
class OrderIsApproved implements OrderValidationInterface
{
    public function validate(Order $order) : void
    {
        if (! $order->isApproved()) {
            throw new Exception('Order is not approved.');
        }
    }
}
class OrderIsNotCancelled implements OrderValidationInterface
{
    public function validate(Order $order) : void
    {
        if (! $order->isCancelled()) {
            throw new Exception('Cancelled order cannot be processed.');
        }
    }
}
class OrderProcessor
{
    public function __construct(
        OrderNotificationInterface $notification,
        array $validators
    ) {
        $this->notification = $notification;
        $this->$validators  = $validators;
    }

    public function process(Order $order)
    {
        foreach ($this->validators as $validator) {
            $validator->validate($order);
        }

        $order->place();

        $this->notification->notify($order);
    }
}
class OrderServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->bind(OrderProcessor::class, function ($app) {
            new OrderProcessor(
                $this->app->make(OrderNotificationInterface::class),
                [
                    new OrderHasAmount,
                    new OrderIsApproved,
                    new OrderIsNotCancelled
                ]
            );
        });
    }
}

After

  • 既存のコードを全く変更することなく
    ロジックを追加・変更できるようになった
  • Unitテストがすごくシンプル
  • Open for extension, but closed for modification

Liskov Substitution Principle

Liskov Substitution Principle

  • あるClassがInterfaceに依存している場合、そのInterfaceを実装したClassであれば
    どれでも置き換え可能であるべき
  • 定義だけ聞くと分かりづらいけど、
    具体例を見れば簡単な原則
  • この原則に違反すると
    Open Closed Principleにも違反している

Barbara Liskov

通知方法がメールに変わった

仕入元

Web App

メール通知

発注者

class EmailNotification implements OrderNotificationInterface
{
    protected $aws;
    protected $ses;

    public function __construct(Aws $aws) {
        $this->aws = $aws;
    }

    public function createSes() : void
    {
        $this->ses = $this->aws->createSes(['region' => 'us-east-1']);
    }

    public function notify(Order $order) : void
    {
        $options = [
            'from' => 'mail@example.com',
            'to' => $order->getUserEmail(),
            'subject' => 'Order has been processed',
            'body' => $this->getMessage($order)
        ];

        $this->ses->sendEmail($options);
    }
}
class OrderProcessor
{
    public function __construct(OrderNotificationInterface $notification, array $validators) {
        $this->notification = $notification;
        $this->$validators  = $validators;
    }

    public function process(Order $order)
    {
        foreach ($this->validators as $validator) {
            $validator->validate($order);
        }

        $order->place();

        if ($this->notification instanceof EmailNotification) {
            $this->notification->createSes();
        }
        $this->notification->notify($order);
    }
}

Before

  • オブジェクトの型に応じた処理を書かないといけない
  • 依存クラスのロジックがリークしている
  • SRP, OCPに違反している
class EmailNotification implements OrderNotificationInterface
{
    protected $ses;

    public function __construct(Aws $aws) {
        $this->aws = $aws;
    }

    public function createSes() : void
    {
        $this->ses = $this->aws->createSes(['region' => 'us-east-1']);
    }

    public function notify(Order $order) : void
    {
        $this->createSes();

        $options = [
            'from' => 'mail@example.com',
            'to' => $order->getUserEmail(),
            'subject' => 'Order has been processed',
            'body' => $this->getMessage($order)
        ];

        $this->ses->sendEmail($options);
    }
}
class OrderProcessor
{
    public function __construct(
        OrderNotificationInterface $notification,
        array $validators
    ) {
        $this->notification = $notification;
        $this->$validators  = $validators;
    }

    public function process(Order $order)
    {
        foreach ($this->validators as $validator) {
            $validator->validate($order);
        }

        $order->place();

        $this->notification->notify($order);
    }
}

After

  • OrderProcessorを修正することなく、
    OrderNotificationInterfaceの実装を
    差し替えられる

Interface Segregation Principle

Interface Segregation Principle

  • インターフェースにいろんな機能を詰め込みすぎない
  • インターフェースの役割を適切に分離する
  • インターフェースの実装クラスに中身が
    空のメソッドがあったら、インターフェースを分けるサイン

Dependency Inversion Principle

Dependency Inversion Principle

  • 上位レイヤーのコードは下位レイヤーの
    コードに依存すべきではない
  • 上位レイヤーのコードは抽象に依存すべき
  • 抽象=インターフェース
  • 抽象は実装の詳細に依存すべきではない
  • 実装の詳細が抽象に依存すべき

残り2つの原則は時間的に割愛します

(Laracastsにもシリーズがあります)

まとめ

  • 盲目的に守るべきルールではないし、
    どんな場合にも有効な完璧な原則でもない
  • 最初からやりすぎるとYAGNI
  • 設計、実装するときのガイドライン
  • 実装が複雑になってきたら見直す
  • それぞれの原則はお互いに関係し合っている

クラシコムではエンジニアを募集しています

Thank You!