この記事では、単一責任原則とそれに関連するいくつかのテクニックが、コードにまさにこの品質を与える方法について説明します。優れたコードを書くのは芸術ですが、いくつかの原則は、開発作業が堅牢で保守可能なソフトウェアを作成するために必要な方向性を常に与えるのに役立ちます。 モデルがすべてです
新しい MVC (MVP、MVVM、またはその他の M**) フレームワークに関するほぼすべての本には、悪いコードの例が散りばめられています。これらの例は、フレームワークが提供するものを示しようとしています。しかし、初心者にとって悪いアドバイスにもなります。「モデルにはこの ORM X があり、ビューにはテンプレート エンジン Y があり、これらすべてを管理するコントローラーがあるとします」などの例は、巨大なコントローラー以外の何ものにもなりません。
これらの本を擁護するために言うと、例はフレームワークを使い始めるのが簡単であることを示すためのものです。ソフトウェア設計を教えるためのものではありません。しかし、これらの例に従う読者は、何年も経ってから初めて、プロジェクトにモノリシックなコード チャンクが存在することがどれほど非生産的であるかに気づきます。
モデルはアプリの心臓部です。モデルをアプリケーション ロジックの残りの部分から分離しておけば、アプリケーションがどれだけ複雑になっても、メンテナンスがはるかに簡単になります。複雑なアプリケーションでも、モデルを適切に実装すれば、非常に表現力豊かなコードを作成できます。それを実現するには、まずモデルが本来の機能だけを実行し、モデルを中心に構築されたアプリの機能には関心を持たないようにします。さらに、モデルは基盤となるデータ ストレージ レイヤーが何であるかにも関心を持ちません。アプリは SQL データベースに依存しているのでしょうか、それともすべてをテキスト ファイルに保存しているのでしょうか。
この記事を読み進めていくと、優れたコードとは関心の分離に大きく関係していることに気付くでしょう。
単一責任原則
おそらく、SOLID 原則について聞いたことがあるでしょう。単一責任、オープンクローズ、リスコフ置換、インターフェース分離、依存性反転です。最初の文字 S は単一責任原則 (SRP) を表し、その重要性はいくら強調してもし過ぎることはありません。私は、それが優れたコードに必要かつ十分な条件であるとさえ主張します。実際、下手に書かれたコードには、複数の責任を持つクラスが必ず見つかります。数千行のコードを含む form1.cs や index.php はそれほど珍しいものではなく、おそらく誰もが見たことがあるか、やったことがあるでしょう。
C# (ASP.NET MVC および Entity Framework) の例を見てみましょう。C# 開発者でなくても、OOP の経験があれば簡単に理解できるでしょう。
これは通常の OrderController クラスで、その Create メソッドが表示されています。このようなコントローラーでは、Order クラス自体がリクエスト パラメーターとして使用されるケースがよく見られます。ただし、私は特別なリクエスト クラスを使用することを好みます。繰り返しますが、SRP です。
上記のコード スニペットで、コントローラーが「注文の配置」についてあまりにも多くのことを知っていることに注目してください。これには、Order オブジェクトの保存、電子メールの送信などが含まれますが、これらに限定されません。これは、1 つのクラスには多すぎるジョブです。小さな変更ごとに、開発者はコントローラーのコード全体を変更する必要があります。また、別のコントローラーでも注文を作成する必要がある場合に備えて、開発者はコードをコピーして貼り付けることがよくあります。コントローラーはプロセス全体を制御するだけでよく、プロセスのロジックをすべて実際に収容する必要はありません。
しかし、今日は、これらの巨大なコントローラーの作成をやめる日です。
まず、コントローラーからすべてのビジネス ロジックを抽出し、それを OrderService クラスに移動します。
これが完了すると、コントローラーは、プロセスの制御という本来の目的だけを実行するようになります。コントローラーは、ビュー、OrderService、および OrderRequest クラスのみを認識します。これは、コントローラーが要求を管理し、応答を送信するというジョブを実行するために必要な最小限の情報です。
この方法では、コントローラー コードを変更することはほとんどありません。ビュー、要求オブジェクト、サービスなどの他のコンポーネントは、ビジネス要件にリンクされているため、変更することができますが、コントローラーは変更できません。
これが SRP の目的であり、この原則を満たすコードを作成するための手法は多数あります。その一例が依存性注入です (これは、テスト可能なコードを作成する場合にも役立ちます)。
依存性注入
依存性注入なしで単一責任原則に基づく大規模プロジェクトを想像するのは困難です。OrderService クラスをもう一度見てみましょう。
このコードは動作しますが、理想的とは言えません。OrderService クラスの作成メソッドがどのように動作するかを理解するには、SMTP の複雑さを理解する必要があります。また、必要な場所に SMTP の使用を複製するには、コピー アンド ペーストしか方法がありません。しかし、少しリファクタリングすれば、状況は変わります。
すでにかなり良くなりました! しかし、OrderService クラスは、電子メールの送信についてまだ多くのことを知っています。電子メールを送信するには、まさに SmtpMailer クラスが必要です。将来的に変更したい場合はどうすればよいでしょうか? 開発環境で実際に送信するのではなく、送信する電子メールの内容を特別なログ ファイルに出力したい場合はどうすればよいでしょうか? OrderService クラスを単体テストしたい場合はどうすればよいでしょうか? インターフェイス IMailer を作成して、リファクタリングを続けましょう。
SmtpMailer はこのインターフェースを実装します。また、アプリケーションは IoC コンテナを使用するので、IMailer が SmtpMailer クラスによって実装されるように構成できます。OrderService は次のように変更できます。
これで、何かがうまくいきました。この機会に、もう 1 つ変更を加えました。OrderService は、すべての注文を保存するコンポーネントとやり取りするために、IOrderRepository インターフェイスに依存するようになりました。インターフェイスの実装方法や、インターフェイスを駆動するストレージ テクノロジについては、もう気にしなくなりました。OrderService クラスには、注文ビジネス ロジックを処理するコードのみが含まれるようになりました。
このように、テスターがメールの送信で何かが誤って動作していることを発見した場合、開発者はどこを調べればよいかを正確に把握できます。SmtpMailer クラスです。割引で何か問題があった場合も、開発者はどこを調べればよいかを把握できます。OrderService (または、SRP を心から理解している場合は、DiscountService) クラス コードです。
イベント駆動型アーキテクチャ
しかし、私はまだ OrderService.Create メソッドが気に入りません。
電子メールの送信は、メインの注文作成フローの一部ではありません。アプリが電子メールの送信に失敗しても、注文は正しく作成されます。また、注文が正常に行われた後に電子メールの受信をオプトアウトできる新しいオプションをユーザー設定領域に追加する必要がある状況を想像してください。これを OrderService クラスに組み込むには、依存関係 IUserParametersService を導入する必要があります。これにローカリゼーションを追加すると、さらに別の依存関係 ITranslator (ユーザーが選択した言語で正しい電子メール メッセージを生成する) が必要になります。これらのアクションのいくつかは不要です。特に、多くの依存関係を追加して、画面に収まらないコンストラクターを作成するという考えは不要です。私は Magento のコードベース (PHP で記述された人気の e コマース CMS) の 32 個の依存関係を持つクラスで、この良い例を見つけました。
時々、このロジックをどのように分離するかがわかりにくいことがあります。Magento のクラスはおそらくそのようなケースの 1 つに該当します。そのため、私はイベント駆動型の方法が好きです。
注文が作成されるたびに、OrderService クラスから直接電子メールが送信されるのではなく、特別なイベント クラス OrderCreated が作成され、イベントが生成されます。アプリケーションのどこかにイベント ハンドラーが構成されます。そのうちの 1 つがクライアントに電子メールを送信します。
OrderCreated クラスは、意図的に Serializable としてマークされています。このイベントをすぐに処理することも、キュー (Redis、ActiveMQ など) にシリアル化して保存し、Web リクエストを処理するプロセス/スレッドとは別のプロセス/スレッドで処理することもできます。この記事では、イベント駆動型アーキテクチャについて詳しく説明しています (OrderController 内のビジネス ロジックには注意を払わないでください)。
注文を作成するときに何が起こっているのか理解するのが難しくなったと主張する人もいるかもしれません。しかし、それは真実からかけ離れています。そう感じる場合は、IDE の機能を活用してください。IDE で OrderCreated クラスの使用箇所をすべて見つけることで、イベントに関連付けられたすべてのアクションを確認できます。
しかし、依存性注入はいつ使用し、イベント駆動型アプローチはいつ使用すればよいのでしょうか。この質問に答えるのは必ずしも簡単ではありませんが、役立つ可能性がある 1 つの簡単なルールは、アプリケーション内のすべての主要なアクティビティに依存性注入を使用し、すべての二次的なアクションにイベント駆動型アプローチを使用することです。たとえば、IOrderRepository を使用して OrderService クラス内で注文を作成するなどの処理で依存性注入を使用し、メインの注文作成フローの重要な部分ではない電子メールの送信を何らかのイベント ハンドラーに委任します。
結論
最初は非常に重いコントローラー、たった 1 つのクラスから始めましたが、最終的には複雑なクラスのコレクションになりました。これらの変更の利点は、例から明らかです。ただし、これらの例を改善する方法はまだたくさんあります。たとえば、OrderService.Create メソッドは、独自のクラス OrderCreator に移動できます。注文の作成は、単一責任原則に従うビジネス ロジックの独立した単位であるため、独自の依存関係を持つ独自のクラスを持つのは当然のことです。同様に、注文の削除と注文のキャンセルは、それぞれ独自のクラスに実装できます。
この記事の最初の例に似た、高度に結合したコードを書いた場合、要件に小さな変更を加えると、コードの他の部分に多くの変更が簡単に発生する可能性があります。SRP は、各クラスに独自のジョブがある、分離されたコードを開発者が書くのに役立ちます。このジョブの仕様が変更された場合、開発者はその特定のクラスのみを変更します。他のクラスは、もちろん最初から壊れていた場合を除き、以前と同じようにジョブを実行するため、変更によってアプリケーション全体が壊れる可能性は低くなります。
これらの手法を使用して事前にコードを開発し、単一責任の原則に従うことは困難な作業のように思えるかもしれませんが、プロジェクトが拡大し、開発が継続するにつれて、その努力は確実に報われるでしょう。