コドモン Product Team Blog

株式会社コドモンの開発チームで運営しているブログです。エンジニアやPdMメンバーが、プロダクトや技術やチームについて発信します!

Decorator パターンでコントローラーの肥大化を抑える話 #デザインパターン

こちらの記事は「コドモン Advent Calendar 2023」の 17日目の記事です🎅

qiita.com

こんにちは!プロダクト開発部の宮平です。

コントローラークラスに責務を持たせすぎて、コントローラークラスが肥大化することはありませんか?

今回はデザインパターンの「Decorator」パターン(以下デコレータ)で肥大化を抑え、コントローラークラスで行っていた処理を凝集度高く分割し、再利用性を高める方法についてご紹介しようと思います。

※留意事項
通常、コントローラをデコレートする方法として、フレームワークが提供するMiddleware(ミドルウェア)と呼ばれる一般的な概念1があり、多くのフレームワークでサポートされています。例えば、Laravelでは公式ドキュメントでMiddlewareの使用方法が紹介されています。このような標準機能を活用することが一般的で、本記事では説明のために敢えてデコレーターパターンを用いた実装例を示していますが、実際の開発ではフレームワークの提供機能をなるべく活かすことをお勧めします。

どういった課題か?

本来はユーザーからのリクエストとビジネスロジックの橋渡しをするだけなはずのコントローラークラスが、認可処理やエラーハンドリング、ロギング処理などを担当し、その結果、責務が増えることにより、肥大化しがちになります。

また、肥大化するロジックの多くは他のコントローラークラスでも毎回同じような実装が繰り返されていることもあります。

他にもコントローラークラス内の処理で単体テストを実装したい場合、検証したい処理とは関係のない認可処理のMockを都度定義したりすることで、単体テストのコードの行数が増えてコードリーディングしづらくもなります。

Decoratorパターンでどのように解決するのか

本来は、飾り枠と中身を同一視して、飾り枠を幾重にも重ねることで、より柔軟な機能拡張方法を提供するパターンですが、

今回は、コントローラ内で行われる処理を同一視することで柔軟な設計にしていきます。

認可処理やロギング処理などの横断的な関心ごとを分離することで、再利用可能なモジュール性を高めるAOP(アスペクト指向プログラミング)のように、必要に応じてコントローラークラスに処理を重ねることができるようになります。

それでは実装を見ていきたいと思います。

実装例

弊社のプロダクトがPHPを利用しているため、PHPのコードで例を示します。

元々の実装
<?php

// デコレーターパターンを使用しない場合のコントローラークラス
class SimpleController
{
    public function handleRequest()
    {
        try {
            $this->log(‘リクエストログ’);

            // 認可チェック
            $this->checkAuthorization();

            // 本来の処理の実行
            // ...

            $this->log(‘レスポンスログ’);
        } catch (Exception $e) {
            // エラーハンドリング
            // ...

            // ロギング処理
            $this->log($e->getMessage());
        }
    }

    /**
     * @throws Exception 認可エラーの場合
     */
    private function checkAuthorization()
    {
        // 認可チェック処理の実装
        // ...
    }

    private function log(string $message)
    {
        // ロギング処理の実装
        // ...
    }
}

// 使用例
$controller = new SimpleController();
$controller->handleRequest();
Decoratorパターンによる実装
<?php

// コントローラーのインターフェース
interface Controller
{
    public function handleRequest();
}

// 実際のコントローラークラス
class ConcreteController implements Controller
{
    public function handleRequest()
    {
        // 本来の処理の実行
        // ...
    }
}

// 認可処理のデコレータ
class CheckAuthorizationDecorator implements Controller
{
    public function __construct(private readonly Controller $next)
    {
    }

    public function handleRequest()
    {
        // 認可チェック
        $this->checkAuthorization();

        // 次のコントローラークラスの処理を実行
        $this->next->handleRequest();
    }

    /**
     * @throws Exception 認可エラーの場合
     */
    private function checkAuthorization()
    {
        // 認可チェック処理の実装
        // ...
    }
}

// エラーハンドリングのデコレータ
class ErrorHandlingDecorator implements Controller
{
    public function __construct(private readonly Controller $next)
    {
    }

    public function handleRequest()
    {
        try {
            // 次のコントローラークラスの処理を実行
            $this->next->handleRequest();
        } catch (Exception $e) {
            // エラーハンドリングの実装
            // ...
            throw $e;
        }
    }
}

// ロギング処理のデコレータ
class LoggingDecorator implements Controller
{
    public function __construct(private readonly Controller $next)
    {
    }

    public function handleRequest()
    {
        try {
            $this->log(‘リクエストログ’);

            // 次のコントローラークラスの処理を実行
            $this->next->handleRequest();

            $this->log(‘レスポンスログ’);
        } catch (Exception $e) {
            // ロギング処理
            $this->log($e->getMessage());
        }
    }

    private function log()
    {
        // ロギング処理の実装
        // ...
    }
}

// 使用例
$controller = new LoggingDecorator(new ErrorHandlingDecorator(new CheckAuthorizationDecorator(new ConcreteController())));
$controller->handleRequest();

handleRequest関数内の処理をクラスとして分割し、各クラスでhandleRequest関数に処理を重ねています。 元々ひとつの関数の中で行っていた処理を、責務ごとに分けたクラスで扱えるようになったことが伝わるでしょうか。

メリット

メリットとして以下があります。

  • 柔軟性と拡張性が向上する
  • 単一責務の原則の遵守
  • 再利用性が高まる
  • オープン/クローズドの原則を守る
  • 単体テストがしやすくなる

一つずつ説明していきます。

柔軟性と拡張性が向上する

デコレーターパターンにより柔軟な構造になったことで、新しい機能や責務を簡単に追加できます。新しいデコレータを追加するだけになるので、既存のクラスの変更は不要となります。

単一責務の原則に則ってる

各デコレータは一つの責務や機能に焦点を当てることができるので、単一責務の原則(Single Responsibility Principle)に則って実装でき、凝集度が高まります。

再利用性が高まる

デコレーターパターンにより、コードを再利用することができます。異なるデコレータを組み合わせることで、さまざまな機能を持ったオブジェクトを簡単に作成することができるようになります。

オープン/クローズドの原則を守る

オープン/クローズドの原則に基づき、既存のクラスを変更せずに新しい機能を追加できる嬉しみもあります。

単体テストがしやすくなる

各デコレータが特定の責務や機能に焦点を当てているため、それぞれのクラスを単体でテストすることが容易になります。モックを使用して、各デコレーターの振る舞いを単体テストで確認しやすくなりますし、モックの数も減らすことができます。

デメリット

デメリットとしては以下があります。

  • 複雑性が増す
  • 順序の問題
  • デコレータの多重性

一つずつ説明していきます。

複雑性が増す

デコレーターパターンを過度に使用すると、呼び出し階層が複雑になることで理解や保守が難しくなる可能性があります。

順序の問題

デコレータが実行される順序は重要です。一部のデコレーターは順序に依存するため、適切な順序で適用する必要があります。 例えば、認可処理前に実際のコントローラーが実行された場合はセキュリティリスクにつながる恐れがあります。

デコレータの多重性

過度なデコレータの組み合わせがあると、処理の流れが予測しにくくなり、デバッグが難しくなる可能性があります。

過度なデコレータの組み合わせや、複雑性が増した場合は統合することを検討してみてもいいかもしれません。

まとめ/最後に

今回は、Decorator パターンにより肥大化を防ぐ方法について説明しました。

設計パターンの一環として、肥大化したコードのリファクタリングや保守性の向上に有益になることを感じていただけたのではないでしょうか。

これをきっかけに、他のデザインパターンにも興味を持つきっかけになれば大変嬉しいです。

最後まで読んでいただきありがとうございました! 

アドベントカレンダーはまだまだ続くので明日もお楽しみに!


  1. OSとアプリケーションのあいだのソフトウェアを示すミドルウェアではなく、アプリケーションに入る HTTP リクエストを検査・フィルタリングするクラス設計としてのミドルウェアのこと