戻る
トップページに戻る
Logo
Gadget Blossom 略してガジェブロ
SOLID原則を振り返る

SOLID原則を振り返る


当たり前でも時間が経てば忘れるものが結構ありますね。
渡しの場合は設計原則は定期的に見返さないと定着しません。
今回は数ある原則や考え方などまとめました。

設計原則以外の記載もありますが、「まぁ大事だよね」程度に抑えていただければ幸いです。

SOLID原則

SOLID原則とは、オブジェクト指向プログラミングにおける5つの設計原則の頭文字を取ったものです。

  • Single Responsibility Principle (単一責任原則)
    クラスは1つの責務のみを持つべき

  • Open/Closed Principle (開放/閉鎖原則)
    拡張に対して開いており、修正に対して閉じているべき

  • Liskov Substitution Principle (リスコフの置換原則)
    親クラスと子クラスは互いに置き換え可能であるべき

  • Interface Segregation Principle (インターフェース分離原則)
    1つのインターフェースに複数の責務を持たせるべきではない

  • Dependency Inversion Principle (依存性逆転原則)
    上位モジュールは下位モジュールに依存せず、抽象に依存すべき

単一責任原則 (Single Responsibility Principle - SRP)

あるクラスを変更する理由は1つだけであるべきです。
つまり、「クラスは1つの責務のみを持ち、その責務に関連する変更のみを行うべきである」ということです。

悪い例

class User {
  private name: string;
  private email: string;

  constructor(name: string, email: string) {
    this.name = name;
    this.email = email;
  }

  getName(): string {
    return this.name;
  }

  getEmail(): string {
    return this.email;
  }
  // ❌ 責任が2つある: ユーザー情報の管理とメール送信
  sendEmail(subject: string, body: string): void {
    // メール送信処理 (ここでは詳細を省略)
    console.log(`Sending email to ${this.email}: ${subject} - ${body}`);
  }
}

const user = new User("John Doe", "johndoe@example.com");
user.sendEmail("Welcome!", "Thank you for registering.");

良い例

class User {
  // ... (name, email, getName, getEmail は同じ)
}

class EmailService {
  // メール送信の責任を持つクラス
  sendEmail(user: User, subject: string, body: string): void {
    // メール送信処理 (ここでは詳細を省略)
    console.log(`Sending email to ${user.getEmail()}: ${subject} - ${body}`);
  }
}

const user = new User("John Doe", "johndoe@example.com");
const emailService = new EmailService();
emailService.sendEmail(user, "Welcome!", "Thank you for registering.");

この例では、メール送信の責任をEmailServiceクラスに分離しました。
これによりUserクラスはユーザー情報の管理という単一の責任を持つようになり、SRPに準拠した設計になりました。

開放・閉鎖原則 (Open/Closed Principle - OCP)

ソフトウェアのエンティティ(クラス、モジュール、関数など)は、拡張に対して開いていなければならず、変更に対して閉じていなければならない。
つまり、「新しい機能を追加する際には、既存のコードを変更せずに、新しいコードを追加することによって実現できるべきである」ということです。

悪い例

class DiscountCalculator {
  calculateDiscount(productType: string, price: number): number {
    if (productType === "book") {
      return price * 0.1; // 書籍は10%割引
    } else if (productType === "electronics") {
      return price * 0.05; // 電子機器は5%割引
    } else {
      return 0; // その他は割引なし
    }
  }
}

// 新しい商品種別 (衣料品) の割引を追加したい場合、DiscountCalculatorクラスを変更する必要がある

良い例

interface DiscountRule {
  applyDiscount(price: number): number;
}

class BookDiscountRule implements DiscountRule {
  applyDiscount(price: number): number {
    return price * 0.1; // 書籍は10%割引
  }
}

class ElectronicsDiscountRule implements DiscountRule {
  applyDiscount(price: number): number {
    return price * 0.05; // 電子機器は5%割引
  }
}

class DiscountCalculator {
  constructor(private discountRules: DiscountRule[]) {}

  calculateDiscount(productType: string, price: number): number {
    const rule = this.discountRules.find((rule) => rule.supports(productType));
    return rule ? rule.applyDiscount(price) : 0;
  }
}

// 新しい商品種別 (衣料品) の割引を追加する場合、ClothingDiscountRuleクラスを新規作成すれば良い
class ClothingDiscountRule implements DiscountRule {
  applyDiscount(price: number): number {
    return price * 0.15; // 衣料品は15%割引
  }
}

この例では、DiscountRuleインターフェースを導入し、各割引ルールクラスがapplyDiscountメソッドを実装しています。
DiscountCalculatorクラスは、DiscountRuleインターフェースを実装したオブジェクトのリストを受け取り、商品種別に応じた割引ルールを適用して割引額を計算します。
これにより、新しい割引ルールを追加する場合でも、DiscountCalculatorクラスを変更する必要はありません。

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

派生型(サブクラス)は、その基本型(スーパークラス)と置換可能でなければならない。
つまり、子クラスは親クラスの振る舞いを変更することなく、拡張または特殊化できるべきである、ということです。

悪い例

class Rectangle {
  constructor(
    protected width: number,
    protected height: number,
  ) {}

  getArea(): number {
    return this.width * this.height;
  }
}

class Square extends Rectangle {
  // 正方形は長方形の一種ではないため、LSPに違反
  constructor(side: number) {
    super(side, side);
  }

  setWidth(width: number): void {
    this.width = width;
    this.height = width; // 正方形の制約に反する
  }

  setHeight(height: number): void {
    this.width = height;
    this.height = height; // 正方形の制約に反する
  }
}

良い例

interface Shape {
  getArea(): number;
}

class Rectangle implements Shape {
  constructor(
    protected width: number,
    protected height: number,
  ) {}

  getArea(): number {
    return this.width * this.height;
  }
}

class Square implements Shape {
  constructor(private side: number) {}

  getArea(): number {
    return this.side * this.side;
  }
}

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

クライアントは、利用しないメソッドを持つインターフェースを強制的に実装させられるべきではない。

悪い例

interface Worker {
  work(): void;
  eat(): void;
  sleep(): void;
}

class Human implements Worker {
  work(): void {
    console.log("Human is working.");
  }

  eat(): void {
    console.log("Human is eating.");
  }

  sleep(): void {
    console.log("Human is sleeping.");
  }
}

class Robot implements Worker {
  // Robotはsleepしないため、ISPに違反
  work(): void {
    console.log("Robot is working.");
  }

  eat(): void {
    console.log("Robot is recharging."); // eatの代わりにrecharge
  }

  sleep(): void {
    // 実装する必要がないメソッド
    throw new Error("Robots don't sleep.");
  }
}

良い例

interface Workable {
  work(): void;
}

interface Eatable {
  eat(): void;
}

interface Sleepable {
  sleep(): void;
}

class Human implements Workable, Eatable, Sleepable {
  // ... (各メソッドの実装)
}

class Robot implements Workable, Eatable {
  // RobotはSleepableを実装しない
  work(): void {
    console.log("Robot is working.");
  }

  eat(): void {
    console.log("Robot is recharging.");
  }
}

依存性逆転原則 (Dependency Inversion Principle - DIP)

高レベルのモジュールは、低レベルのモジュールに依存してはならない。
どちらも抽象(インターフェース)に依存すべきである。

抽象は詳細に依存してはならない。詳細は抽象に依存すべきである。

悪い例

class ProductService {
  private productRepository: MySQLProductRepository; // 具象クラスに依存

  constructor() {
    this.productRepository = new MySQLProductRepository();
  }

  getProductById(id: number): Product {
    return this.productRepository.findById(id);
  }
}

この例では、ProductServiceクラスがMySQLProductRepositoryクラスに直接依存しています。
もしデータベースをPostgreSQLに変更する場合、ProductServiceクラスも修正する必要があります。

良い例

interface ProductRepository {
  findById(id: number): Product;
}

class MySQLProductRepository implements ProductRepository {
  // ...
}

class PostgreSQLProductRepository implements ProductRepository {
  // ...
}

class ProductService {
  constructor(private productRepository: ProductRepository) {} // 抽象に依存

  getProductById(id: number): Product {
    return this.productRepository.findById(id);
  }
}

// データベースの種類に応じて、適切なリポジトリを注入する
const productService = new ProductService(new MySQLProductRepository()); // MySQLの場合
// const productService = new ProductService(new PostgreSQLProductRepository()); // PostgreSQLの場合

さて、如何だったでしょうか。
過去に業務で作ったクラスやシステムを思い返すとゾッとなることもちらほら。

不定期で見返すことで、より良い設計ができるようになるかもしれませんね。