🙆‍♀️

S.O.L.I.D設計原則: 理解しやすく保守可能なコードを書くためのガイド

2024/07/27に公開

👤対象者

  • プログラミング初心者から中級者
  • コードの保守性やスケーラビリティに関心がある人
  • レガシーコードの改善を目指す開発者

この記事は、S.O.L.I.D原則の重要性や利点を学び、日常のプログラミングに適用しようとする幅広い開発者に向けています。

背景

この記事では、Bob C. Martin(アンクルボブ)が提唱する5つの設計原則「S.O.L.I.D」について解説します。これらの原則は、保守可能でスケーラブルで理解しやすいコードを書くための指針です。

S.O.L.I.D 原則とは?

S.O.L.I.Dは、5つの設計原則の頭文字をとったもので、以下のような問題を解決します。

  • レガシーコードの扱い
  • 変更が必要な部分を見つけるために何度もコードを読み直すこと
  • メソッドが何をしているのか理解するのが難しいこと
  • 名前が同じ複数のメソッドに飛び回ること
  • 小さなバグを修正するのに多くの時間を費やすこと

単一責任原則(Single Responsibility Principle)

名前から明らかなように、単一の責任を持つべきです。
クラスは一つのことにだけ責任を持つべきであり、つまりクラスは一つの理由でしか変更されないべきです。

まず、すべてのものが一つの場所にあるため、メンテナンスが快適でない悪い実践を見てみましょう。バリデーション、リクエスト取得、描画がすべて一つの場所にあります。

class Result {
  checkResult(int rollno) {
    bool isRollnoValidate = isRollnovalidate();
    if (isRollnoValidate) {
      ResultModel resultModel = searchResult();
      showResult(resultModel);
    } else {
      return "Invalid rollno";
    }
  }

  isRollnovalidate() {
    return true;
  }

  // get request
  searchResult() {
    // return result;
  }

  // painting
  showResult(ResultModel model) {
    // show result in pleasant way
  }
}

class ResultModel {}

単一責任原則を適用した後、開発者は望む機能に集中できます。

class Result {
  checkResult(int rollno) {
    bool isRollnoValidate = Validate().isRollnovalidate();
    if (isRollnoValidate) {
      ResultModel resultModel = NetworkApi().searchResult();
      Printing().showResult(resultModel);
    } else {
      return "Invalid rollno";
    }
  }
}

class Validate {
  // this is responsible for validate
  isRollnovalidate() {
    return true;
  }
}

class ResultModel {}

class Printing {
  // this class is responsible for printing
  showResult(ResultModel model) {
    // show result in pleasant way
  }
}

class NetworkApi {
  // this class is responsible for fetching result
  searchResult() {
    return ResultModel();
  }
}

オープン・クローズド原則(Open-Closed Principle)

エンティティは拡張に対して、オープンであり、修正に対してクローズドであるべきです。
良い実践として、新しい機能を追加する際に既存のコードを修正する必要がないようにするべきです。

まず、悪い実践を見てみましょう。機械部門の結果機能を追加する必要がある場合、既存のコードを編集しなければなりません。

class Result {
  mechanicalCheckResult() {
    // some code 
  }

  civilCheckResult() {
    // some code 
  }
}

オープン・クローズド原則を適用した後、新しい機能を追加するためのクラスを作成します。

abstract class Result {
  checkResult();
}

class ComputerScience implements Result {
  
  checkResult() {
    // some code
  }
}

class Civil implements Result {
  
  checkResult() {
    // some code
  }
}

class Mechanical implements Result {
  
  checkResult() {
    // some code
  }
}

リスコフの置換原則(Liskov Substitution Principle)

抽象の観点から、デザインの良さを評価するものです。

アーキテクチャは、サブクラスがコードの論理的な正確さを維持することを保証します。基本的に、継承よりもコンポジション(インターフェースを使用する)を優先します。

まず、悪い実践を見てみましょう。

abstract class Result {
  checkResult();

  codingTestResult();
}

class MechanicalBranch extends Result {
  
  checkResult() {
    //  some code
  }

  /*
  * Here it  is logically incorrect
  * */
  
  codingTestResult() {
    //  some code
  }
}

class ComputerScienceBranch extends Result {
  
  checkResult() {
    //  some codet
  }

  
  codingTestResult() {
    //  some code
  }
}

リスコフの置換原則を適用した後

abstract class OfflineResult {
  checkResult();
}

abstract class CodingResult {
  codingTestResult();
}

class MechanicalBranch implements OfflineResult {
  
  checkResult() {
    //  some code
  }
}

class ComputerScienceBranch implements OfflineResult, CodingResult {
  
  checkResult() {
    // some code
  }

  
  codingTestResult() {
    // some code
  }
}

インターフェース分離原則(Interface Segregation Principle)

クライアントは使用しないメソッドに依存してはならないと述べています。

基本的に、クライアントは呼び出しているメソッド以上のものに依存してはならないのです。

悪い実践:

abstract class Result {
  checkResult();

  codingTestResult();
}

class MechanicalBranch implements Result {
  
  checkResult() {
    //  some code
  }

  /*
  * Here we exposed client with the method which none of his              *  business
  * */
  
  codingTestResult() {
    //  some code
  }
}

class ComputerScienceBranch implements Result {
  
  checkResult() {
    //  some codet
  }

  
  codingTestResult() {
    //  some code
  }
}

インターフェース分離原則を適用した後:

abstract class OfflineResult {
  checkResult();
}

abstract class CodingResult {
  codingTestResult();
}

class MechanicalBranch implements OfflineResult {
  
  checkResult() {
    //  some code
  }
}

class ComputerScienceBranch implements OfflineResult, CodingResult {
  
  checkResult() {
    // some code
  }

  
  codingTestResult() {
    // some code
  }
}

依存関係逆転の原則(Dependency Inversion Principle)

抽象は詳細(具体的な実装)に依存してはならない。抽象に依存すべきです。

基本的に、実装(背景や低レベルのコード)を変更しても、高レベルのコード(実際に操作するクラス)に影響を与えないようにすべきです。

抽象クラスやインターフェースを拡張することは良いが、逆(抽象メソッドがないもの)は悪い実践です。

抽象に依存することで、実装から独立する自由が得られます。これを掘り下げてみましょう。

abstract class Payment {
  payment();
}

class PaymentViaCreditCard implements Payment {
  
  payment() {
    // some code
  }
}

class PaymentViaDebitCard implements Payment {
  
  payment() {
    // some code
  }
}

class PaymentViaBhimUPI implements Payment {
  
  payment() {
    // some code
  }
}

class Checkout {
  // our checkout class knows nothing about how payment works 
  // its knows pay.payment() is paying method 
  checkOut(Payment pay) {
    pay.payment();
  }
}

結論

S.O.L.I.Dの原則は原則であり、ルールではありません。目標はコードを保守可能で、拡張しやすくすることです。SRPやS.O.L.I.Dのためにコードを過度に分割することは避けましょう。S.O.L.I.Dを達成しようとするのではなく、S.O.L.I.Dを使って保守性を達成しましょう。これはツールであり、目標ではありません。

参考文献

Bob C. Martin (アンクルボブ)によるS.O.L.I.D原則
https://scrapbox.io/iki-iki/SOLID原則

Discussion