腐敗防止層と依存性の逆転を駆使して、ミニマムに変更に強くしてみる

この記事は「ウィルゲート Advent Calendar 2024」の 21日目の記事です。 adventar.org

こんにちは、プロダクト事業部開発グループ ソリューション開発ユニットの武田です。 以下の記事に引き続き、「ウィルゲート Advent Calendar 2024」2つ目の記事です。 興味がある方はこちらの記事も覗いてみてください✌️ tech.willgate.co.jp

私はここ1年いくつかのプロダクトを渡り歩いています。
様々なプロダクトで開発していく中で、あまり設計が重視されていないプロダクトに出会うこともありました。
だからといって、その都度大規模なリファクタリングをするなんてことはできません。
しかし、今から作る新しい機能だけでも、より良いものにしたいと思ってしまうのはITエンジニアとしての性でしょうか。

ということで、本記事では「ミニマム」に「変更」に「強く」していく方法をお話していきます。

この記事で分かることは以下のとおりです。

  • 腐敗防止層の実例
  • 依存性逆転の原則の実例

以下の内容には言及しないのであしからず。

  • SOLIDの原則について
  • 厳密な腐敗防止層について
  • 厳密な依存性逆転の原則について

はじめに

この記事では簡単な実例を踏まえて説明するために、以下の機能を作ることになった、という体で進めていきます。

  • 特定の内容をフィルタリングしたものをTwitterに投稿する機能

※ 以降も強い意志で Twitter と呼称します。

また、 Laravel のサービスコンテナ等で DI が適用されている前提です。

変更に弱い機能の具体例

まずは典型的な変更に弱い例を見ていきましょう。

Services/
└─ TwitterPostService.php
namespace App\Services;

use GuzzleHttp\Client;

class TwitterPostService
{
    private Client $httpClient;

    public function __construct(Client $httpClient)
    {
        $this->httpClient = $httpClient;
    }

    public function postTweet(string $content): void
    {
        $filteredContent = $this->filterContent($content);

        // Twitter API への投稿(例外処理等は略してます)
        $response = $this->httpClient->post('https://api.twitter.com/post', [
            'json' => ['text' => $filteredContent],
        ]);
    }

    private function filterContent(string $content): string
    {
        // フィルタリング処理
    }
}

これの問題点は以下のとおりです。

  • TwitterPostService が TwitterAPI へ依存してしまっている
  • ビジネスロジックと Twitter との通信処理が混在している

このままでは、 TwitterAPI に変更があったときに、この TwitterPostService を使用する箇所全てに影響が及んでしまいます。
また、ビジネスロジックと通信処理が混在していることにより、コードが複雑になり、ロジックの理解や変更が難しくなっています。

それではこれらの問題を解決するため、まずは腐敗防止層を導入してみます。

腐敗防止層で影響範囲を小さくする

さっそく腐敗防止層導入後のディレクトリやコードを見てみましょう。

Services/
├─ PostService.php
└─ Twitter/
   └─ TwitterClient.php
// PostService
namespace App\Services;

use App\Twitter\TwitterClient;

class PostService
{
    private TwitterClient $twitterClient;

    public function __construct(TwitterClient $twitterClient)
    {
        $this->twitterClient = $twitterClient;
    }

    public function post(string $content): void
    {
        $filteredContent = $this->filterContent($content);

        // 投稿処理
        $this->twitterClient->post($filteredContent);
    }

    private function filterContent(string $content): string
    {
        // フィルタリング処理
    }
}
// TwitterClient
namespace App\Twitter;

use GuzzleHttp\Client;

class TwitterClient
{
    private Client $httpClient;

    public function __construct(Client $httpClient)
    {
        $this->httpClient = $httpClient;
    }

    public function post(string $content): void
    {
        $response = $this->httpClient->post('https://api.twitter.com/post', [
            'json' => ['text' => $content],
        ]);
    }
}

TwitterClient が腐敗防止層として機能し、TwitterAPIに変更(たとえばリクエストの仕方が変わるとか)が発生しても、 PostService への影響を最小限に抑えることができます。
しかし、これだけだと少し不十分です。
仮に「Twitterから他のSNSへ乗り換える」なんてケースが発生したとしましょう。その変更は PostService にも波及します。

では、次は依存性逆転の原則を取り入れてみましょう。

依存性を逆転させて、柔軟な設計にする

さっそく依存性逆転の原則導入後のディレクトリやコードを見てみましょう。

Services/
├─ PostService.php
├─ SNSGatewayInterface.php
└─ Twitter/
   ├─ TwitterGateway.php
   └─ TwitterClient.php
// PostService
namespace App\Services;

class PostService
{
    private SNSGatewayInterface $gateway;

    public function __construct(SNSGatewayInterface $gateway)
    {
        $this->gateway = $gateway;
    }

    public function post(string $content): void
    {
        $filteredContent = $this->filterContent($content);

        // 投稿処理
        $this->gateway->post($filteredContent);
    }

    private function filterContent(string $content): string
    {
        // フィルタリング処理
    }
}
// SNSGatewayInterface
namespace App\Services;

interface SNSGatewayInterface
{
    public function post(string $content): void;
}
// TwitterGateway
namespace App\Twitter;

use App\Services\SNSGatewayInterface;

class TwitterGateway implements SNSGatewayInterface
{
    private TwitterClient $client;

    public function __construct(TwitterClient $client)
    {
        $this->client = $client;
    }

    public function post(string $content): void
    {
        $this->client->post($content);
    }
}

TwitterClient は変わらないので省略。

ここではクラス図に注目してみましょう。
これまでは、依存の方向が上から下に向かっていましたが、依存性逆転の原則を適用したことによって、 PostServiceTwitterGatewaySNSGatewayInterface に依存するようになりました。
これによって、 PostService は具体的なSNSに依存しないようになり、柔軟に変更、拡張が可能になります。

例えば、ポスト先を Twitter から Facebook に変更するとなったとしましょう。
しかし、 SNSGatewayInterface を実装するだけで変更が完了します。
※ もちろん、必要に応じて FacebookClient の実装も必要です。

さらに良くするなら?

この記事の例はあくまでも「ミニマム」に適用した例となっています。
例えば、 TwitterClientGuzzleHttp\Client に依存しています。
GuzzleHttp\Client の腐敗防止層を用意すれば、 GuzzleHttp\Client から別のライブラリへ乗り換えた場合でも、 TwitterClient に影響が及ばなくなります。 さらに、HTTP通信機能を再利用できるようになり、 TwitterClient だけでなく、先ほど例に挙げた FacebookClient の通信部分に流用することもできるようになります。

など、まだまだ改善できる点はたくさんあります。

おわりに

この記事では、腐敗防止層と依存性逆転の原則を駆使することで、機能をミニマムに柔軟に変更に強くする方法を説明しました。
変更に強くする方法は他にも色々ありますが、いきなり大きく変更するのではなく、まずは今回紹介したような内容を「ミニマム」に取り入れていくところから始めるのをおすすめします。また、この記事では触れていませんが、ここで紹介したものはエラーハンドリングの設計にも適用することができるので、ぜひ試してみてください。
今回の内容を出発点に、より規模の大きいリファクタリングへの足がかりへとなれたら幸いです。

ウィルゲート Advent Calendar 2024」、翌日は佐々木さんによる「【全国制覇】ウィルゲート開発室2024年カンファレンス登壇まとめ!!」です。 お楽しみに!