単一責任の原則:優れたコードのレシピ

公開: 2022-03-11

私たちが優れたコードと見なすものに関係なく、それは常に1つの単純な品質を必要とします。コードは保守可能でなければなりません。 適切なインデント、きちんとした変数名、100%のテストカバレッジなどは、これまでのところしか理解できません。 保守が不可能で、要件の変化に比較的容易に適応できないコードは、廃止されるのを待っているコードです。 プロトタイプ、概念実証、または最小限の実行可能な製品を構築しようとするときに、優れたコードを作成する必要はないかもしれませんが、それ以外の場合は常に、保守可能なコードを作成する必要があります。 これは、ソフトウェアエンジニアリングと設計の基本的な品質と見なされるべきものです。

単一責任の原則:優れたコードのレシピ

この記事では、単一責任の原則とそれを中心に展開するいくつかの手法によって、コードにこれほどの品質を与える方法について説明します。 優れたコードを書くことは芸術ですが、いくつかの原則は、堅牢で保守可能なソフトウェアを作成するために必要な方向性を開発作業に与えるのに常に役立ちます。

モデルがすべて

新しいMVC(MVP、MVVM、またはその他のM **)フレームワークに関するほとんどすべての本には、不正なコードの例が散らばっています。 これらの例は、フレームワークが提供するものを示しています。 しかし、彼らはまた、初心者に悪いアドバイスを提供することになります。 「モデルにこのORMXがあり、ビューにエンジンYをテンプレート化して、すべてを管理するコントローラーがあるとしましょう」のような例は、巨大なコントローラーに他なりません。

これらの本を擁護していますが、例は、それらのフレームワークを簡単に使い始めることができることを示すことを目的としています。 それらはソフトウェア設計を教えることを意図していません。 しかし、これらの例に従う読者は、数年後になって初めて、プロジェクトにモノリシックなコードのチャンクを含めることがいかに逆効果であるかを理解しています。

モデルはアプリの心臓部です。

モデルはアプリの心臓部です。 モデルを他のアプリケーションロジックから分離している場合、アプリケーションがどれほど複雑になっても、メンテナンスははるかに簡単になります。 複雑なアプリケーションの場合でも、モデルを適切に実装すると、非常に表現力豊かなコードが得られます。 そしてそれを達成するために、モデルが意図されたとおりに動作することを確認することから始め、その周りに構築されたアプリが何をするかを気にしないでください。 さらに、基盤となるデータストレージレイヤーが何であるかには関係ありません。アプリはSQLデータベースに依存していますか、それともすべてをテキストファイルに保存していますか?

この記事を続けると、関心の分離が非常に優れたコードであることがわかります。

単一責任の原則

おそらく、SOLIDの原則について聞いたことがあるでしょう。単一責任、オープンクローズ、リスコフの置換、インターフェースの分離、依存関係の逆転です。 最初の文字Sは、単一責任原則(SRP)を表しており、その重要性を誇張することはできません。 私はそれが良いコードのための必要十分条件であるとさえ主張するでしょう。 実際、ひどく書かれたコードでは、常に複数の責任を持つクラスを見つけることができます-数千行のコードを含むform1.csまたはindex.phpは、私たち全員にとって珍しいことではありませんおそらくそれを見たか、やったことがあるでしょう。

C#(ASP.NETMVCおよびEntityFramework)の例を見てみましょう。 C#開発者でなくても、OOPの経験があれば、簡単にフォローできます。

 public class OrderController { ... public ActionResult CreateForm() { /* * View data preparations */ return View(); } [HttpPost] public ActionResult Create(OrderCreateRequest request) { if (!ModelState.IsValid) { /* * View data preparations */ return View(); } using (var context = new DataContext()) { var order = new Order(); // Create order from request context.Orders.Add(order); // Reserve ordered goods …(Huge logic here)... context.SaveChanges(); //Send email with order details for customer } return RedirectToAction("Index"); } ... (many more methods like Create here) }

これは通常のOrderControllerクラスであり、そのCreateメソッドが示されています。 このようなコントローラーでは、Orderクラス自体が要求パラメーターとして使用される場合がよくあります。 しかし、私は特別なリクエストクラスを使用することを好みます。 繰り返しますが、SRP!

1つのコントローラーに対してジョブが多すぎます

上記のコードスニペットで、コントローラーが「注文」についてあまりにも多くのことを知っていることに注意してください。これには、Orderオブジェクトの保存、電子メールの送信などが含まれますが、これらに限定されません。これは、単一のクラスにとって単純に多すぎるジョブです。 少し変更するたびに、開発者はコントローラーのコード全体を変更する必要があります。 また、別のコントローラーも注文を作成する必要がある場合に備えて、多くの場合、開発者はコードをコピーして貼り付けることになります。 コントローラーはプロセス全体を制御するだけで、プロセスのロジックのすべてのビットを実際に格納する必要はありません。

しかし、今日はこれらの巨大なコントローラーの作成をやめる日です!

まず、コントローラからすべてのビジネスロジックを抽出し、それをOrderServiceクラスに移動しましょう。

 public class OrderService { public void Create(OrderCreateRequest request) { // all actions for order creating here } } public class OrderController { public OrderController() { this.service = new OrderService(); } [HttpPost] public ActionResult Create(OrderCreateRequest request) { if (!ModelState.IsValid) { /* * View data preparations */ return View(); } this.service.Create(request); return RedirectToAction("Index"); }

これが完了すると、コントローラーは意図したこと、つまりプロセスの制御のみを実行するようになります。 ビュー、OrderService、およびOrde​​rRequestクラスについてのみ認識します。これは、要求の管理と応答の送信というジョブを実行するために必要な最小限の情報セットです。

この方法では、コントローラーコードを変更することはめったにありません。 ビュー、リクエストオブジェクト、サービスなどの他のコンポーネントは、ビジネス要件にリンクされているため変更できますが、コントローラーにはリンクされていません。

これがSRPの目的であり、この原則を満たすコードを作成するための多くの手法があります。 この一例は、依存性注入(テスト可能なコードの記述にも役立つもの)です。

依存性注入

依存性注入のない単一責任原則に基づく大規模なプロジェクトを想像するのは難しいです。 OrderServiceクラスをもう一度見てみましょう。

 public class OrderService { public void Create(...) { // Creating the order(and let's forget about reserving here, it's not important for following examples) // Sending an email to client with order details var smtp = new SMTP(); // Setting smtp.Host, UserName, Password and other parameters smtp.Send(); } }

このコードは機能しますが、理想的ではありません。 createメソッドOrderServiceクラスがどのように機能するかを理解するために、SMTPの複雑さを理解する必要があります。 また、このSMTPの使用を必要な場所に複製するには、コピーアンドペーストが唯一の方法です。 しかし、少しリファクタリングすると、それが変わる可能性があります。

 public class OrderService { private SmtpMailer mailer; public OrderService() { this.mailer = new SmtpMailer(); } public void Create(...) { // Creating the order // Sending an email to client with order details this.mailer.Send(...); } } public class SmtpMailer { public void Send(string to, string subject, string body) { // SMTP stuff will be only here } }

すでにはるかに良いです! ただし、OrderServiceクラスは、電子メールの送信について多くのことを知っています。 電子メールを送信するには、正確にSmtpMailerクラスが必要です。 将来変更したい場合はどうなりますか? 開発環境で実際に送信するのではなく、送信された電子メールの内容を特別なログファイルに印刷したい場合はどうなりますか? OrderServiceクラスの単体テストを行う場合はどうなりますか? インターフェイスIMailerを作成して、リファクタリングを続けましょう。

 public interface IMailer { void Send(string to, string subject, string body); }

SmtpMailerはこのインターフェースを実装します。 また、アプリケーションはIoCコンテナーを使用し、IMailerがSmtpMailerクラスによって実装されるように構成できます。 OrderServiceは、次のように変更できます。

 public sealed class OrderService: IOrderService { private IOrderRepository repository; private IMailer mailer; public OrderService(IOrderRepository repository, IMailer mailer) { this.repository = repository; this.mailer = mailer; } public void Create(...) { var order = new Order(); // fill the Order entity using the full power of our Business Logic(discounts, promotions, etc.) this.repository.Save(order); this.mailer.Send(<orders user email>, <subject>, <body with order details>); } }

今、私たちはどこかに到達しています! 私はこの機会にまた別の変更を加えました。 OrderServiceは、すべての注文を格納するコンポーネントと対話するためにIOrderRepositoryインターフェースに依存するようになりました。 そのインターフェースがどのように実装されているか、そしてどのストレージテクノロジーがそれを支えているかについてはもはや気にしません。 現在、OrderServiceクラスには、注文ビジネスロジックを処理するコードのみが含まれています。

このように、テスターが電子メールの送信で正しく動作していないものを見つけた場合、開発者はどこを見ればよいかを正確に知っています:SmtpMailerクラス。 割引に問題があった場合、開発者はここでもどこを見ればよいかを知っています。OrderService(またはSRPを心から採用している場合は、DiscountServiceの可能性があります)クラスコード。

イベント駆動型アーキテクチャ

しかし、私はまだOrderService.Createメソッドが好きではありません。

 public void Create(...) { var order = new Order(); ... this.repository.Save(order); this.mailer.Send(<orders user email>, <subject>, <body with order details>); }

メールの送信は、メインの注文作成フローの一部ではありません。 アプリがメールの送信に失敗した場合でも、注文は正しく作成されます。 また、ユーザー設定領域に新しいオプションを追加して、注文が正常に行われた後に電子メールの受信をオプトアウトできるようにする必要がある状況を想像してみてください。 これをOrderServiceクラスに組み込むには、依存関係IUserParametersServiceを導入する必要があります。 ローカリゼーションをミックスに追加すると、さらに別の依存関係であるITranslatorがあります(ユーザーが選択した言語で正しい電子メールメッセージを生成するため)。 これらのアクションのいくつかは不要です。特に、これらの多くの依存関係を追加して、画面に収まらないコンストラクターで終わるという考えは不要です。 32の依存関係を持つクラスのMagentoのコードベース(PHPで記述された人気のあるeコマースCMS)で、この良い例を見つけました!

画面に収まらないコンストラクター

このロジックを分離する方法を理解するのが難しい場合があり、Magentoのクラスはおそらくそれらのケースの1つの犠牲者です。 それが私がイベント駆動型の方法が好きな理由です:

 namespace <base namespace>.Events { [Serializable] public class OrderCreated { private readonly Order order; public OrderCreated(Order order) { this.order = order; } public Order GetOrder() { return this.order; } } }

注文が作成されるたびに、OrderServiceクラスから直接電子メールを送信する代わりに、特別なイベントクラスOrderCreatedが作成され、イベントが生成されます。 アプリケーションのどこかでイベントハンドラーが構成されます。 そのうちの1つがクライアントにメールを送信します。

 namespace <base namespace>.EventHandlers { public class OrderCreatedEmailSender : IEventHandler<OrderCreated> { public OrderCreatedEmailSender(IMailer, IUserParametersService, ITranslator) { // this class depend on all stuff which it need to send an email. } public void Handle(OrderCreated event) { this.mailer.Send(...); } } }

クラスOrderCreatedは、意図的にSerializableとしてマークされています。 このイベントをすぐに処理することも、シリアル化されたキュー(Redis、ActiveMQなど)に格納して、Webリクエストを処理するプロセス/スレッドとは別のプロセス/スレッドで処理することもできます。 この記事では、作成者がイベント駆動型アーキテクチャとは何かを詳しく説明します(OrderController内のビジネスロジックには注意を払わないでください)。

注文を作成するときに何が起こっているのかを理解するのが今では難しいと主張する人もいるかもしれません。 しかし、それは真実から遠く離れることはできません。 そのように感じたら、IDEの機能を利用してください。 IDEでOrderCreatedクラスのすべての使用法を見つけることにより、イベントに関連付けられているすべてのアクションを確認できます。

しかし、いつ依存性注入を使用する必要があり、いつイベント駆動型アプローチを使用する必要がありますか? この質問に答えるのは必ずしも簡単ではありませんが、アプリケーション内のすべての主要なアクティビティに依存性注入を使用し、すべての二次アクションにイベント駆動型アプローチを使用するのに役立つ簡単なルールが1つあります。 たとえば、IOrderRepositoryを使用してOrderServiceクラス内で注文を作成したり、メインの注文作成フローの重要な部分ではない電子メールの送信をイベントハンドラーに委任したりするなど、DependecyInjectionを使用します。

結論

最初は非常に重いコントローラー、たった1つのクラスから始めて、最終的には手の込んだクラスのコレクションになりました。 これらの変更の利点は、例から非常に明らかです。 ただし、これらの例を改善する方法はまだたくさんあります。 たとえば、OrderService.Createメソッドを独自のクラスOrderCreatorに移動できます。 注文の作成は、単一責任の原則に従った独立したビジネスロジックの単位であるため、独自の依存関係を持つ独自のクラスを持つのは当然のことです。 同様に、注文の削除と注文のキャンセルは、それぞれ独自のクラスで実装できます。

この記事の最初の例に似た、高度に結合されたコードを書いたとき、要件に小さな変更を加えると、コードの他の部分に多くの変更が簡単に生じる可能性があります。 SRPは、開発者が分離されたコードを作成するのに役立ちます。各クラスには独自の役割があります。 このジョブの仕様が変更された場合、開発者はその特定のクラスにのみ変更を加えます。 もちろん、そもそも壊れていない限り、他のクラスは以前と同じように仕事をしているはずなので、この変更によってアプリケーション全体が壊れることはほとんどありません。

これらの手法を使用してコードを事前に開発し、単一責任の原則に従うことは困難な作業のように思えるかもしれませんが、プロジェクトが成長し、開発が継続するにつれて、努力は確実に報われるでしょう。