自分なりにSOLIDの原則を理解する

7 min読了の目安(約7000字TECH技術記事

概要

SOLIDの原則について自分なりに可能な限り簡潔にまとめてみました。

S(Single Responsibility Principle): 単一責任の原則
O(Open/Closed principle): 開放閉鎖の原則
L(Liskov substitution principle): リスコフの置換原則
I(Interface segregation principle): インターフェース分離の原則
D(Dependency inversion principle): 依存性逆転の原則

S: 単一責任の原則

概要

モジュールは一つのアクターに対して責務を負うべきである。

単一責任原則に違反した具体例

下記は3つのメソッドがそれぞれ別のアクターに対する責務を負っており、SRPに違反している。
つまり、アクターの異なるコードは分割すべきである。

class Employee {
    // 経理部門が規定する(報告先はCFO)
    calculatePay() {}

    // 人事部門が規定する(報告先はCOO)
    reportHours() {}

    // DB管理者が規定する(報告先はCTO)
    save() {}
}

解決策

共有データは一箇所にまとめて、アクターの異なる関数を別のクラスに移動する。

// 共有データ
class Employee {
    id: number
    name: string 
    salary: number

    constructor(id: number, name: string, salary: number) {
        this.id = id
        this.name = name
        this.salary = salary
    }
}

// 経理部門が規定する(報告先はCFO)
class PayCalculator {
    employeeData: Employee

    constructor(employee: Employee) {
        this.employeeData = employee
    }

    calculatePay() {}
}

// 人事部門が規定する(報告先はCOO)
class HourReporter {
    employeeData: Employee

    constructor(employee: Employee) {
        this.employeeData = employee
    }

    reportHours() {}
}

// DB管理者が規定する(報告先はCTO)
class EmployeeSaver {
    employeeData: Employee

    constructor(employee: Employee) {
        this.employeeData = employee
    }

    save() {}  
}

参考

O: 開放閉鎖の原則

概要

クラス・モジュール・関数は拡張に対して開かれて、修正に対して閉じられていなければならない。(=変更の影響を受けずにシステムを拡張しやすくする)
システムをコンポーネントに分割して、コンポーネントの依存関係を階層構造にする。(= 上位コンポーネントが下位コンポーネントの変更の影響を受けないようにする)

開放閉鎖の原則に違反した具体例

employeeInfonamesデータ構造を変更した場合、printEmployeeInfo の実装も変更する必要がある。

interface EmployeeInfo {
    description: string
    names: string[]
}

const printEmployeeInfo = (employeeInfo: EmployeeInfo) => {
	console.log(employeeInfo.description)
  employeeInfo.names.forEach((name) => {
    console.log(name);
  })
}

const employeeInfo = {
		description: "従業員情報",
    names: ["Taro", "Jiro", "Saburo"]
}

printEmployeeInfo(employeeInfo)
//=> Taro Jiro Saburo

解決策

employeeInfonames のイテレーション方法を持たせる。
printEmployeeInfoEmployeeInfo インターフェースを満たしたオブジェクトに対しては自身の実装を変更せずに拡張可能となった。

interface EmployeeInfo {
    description: string
    names: string[]
    printNames: () => void
}

const printEmployeeInfo = (employeeInfo: EmployeeInfo) => {
    console.log(employeeInfo.description)
    employeeInfo.printNames()
}

const employeeInfo = {
    description: "従業員情報",
    names: ["Taro", "Jiro", "Saburo"],
    printNames: function() {
        this.names.forEach((name: string) => {
          console.log(name)
        })
    },
}

printEmployeeInfo(employeeInfo)
//=> Taro Jiro Saburo

参考

L: リスコフの置換原則

概要

派生クラスはその元となったベースクラスと置換が可能でなければならない。

  • 派生クラスでオーバーライドされたメソッドはベースクラスのメソッドと同じ数・型の引数ととらなければならない
  • 派生クラスでオーバーライドされたメソッドの返り値の型はベースクラスのメソッドの返り値の型と同じでなければならない
  • 派生クラスでオーバーライドされたメソッドの例外はベースクラスのメソッドの例外と同じ型でなければならない

リスコフの置換原則に違反した具体例

下記のコードではDogはLSPに沿っているといえる。
しかし、SlothAnimalと置き換えることができないので、LSPに違反しているといえる。

class Animal { 
    run(speed: number) {
        return `running at ${speed} km/h`
    }
}

// OK
class Dog extends Animal {
    bark() {}

    run(speed: number) {
        return `running at ${speed} km/h`
    }
}

// LSPに違反
class Sloth extends Animal {
    run() {
         return new Error("Sorry, I'm too lazy to run");
    }
}

参考

I: インターフェース分離の原則

概念

クライアントは自身が使用しないメソッドへの依存を強制してはいけない。(= 不必要な依存関係ををなくす)

インターフェース分離の原則に違反した具体例

下記コードでDogAnimal interfaceを満たしており、問題ないといえる。
しかし、Lizard ではcryに対して処理がなく、Animalに不必要に依存しており、インターフェース分離の原則に違反しているといえる。

interface Animal {
	  run: () => void
    eat: () => void
    cry: () => void
}

// OK 
class Dog implements Animal {
    run() {
        console.log("RUN")
    }

    eat() {
        console.log("EAT")
    }

    cry() {
        console.log("CRY")
    }
}

// Cryに対して処理がなく、Animalに不必要に依存している
class Lizard implements Animal {
    run() {
        console.log("RUN")
    }

    eat() {
        console.log("EAT")
    }

    cry() {
        // Don't call this method
    }
}

解決策

下記のように共通部分だけを取り出して、より細かいインターフェースへ分離することで、不必要な依存をなくす事が可能。

interface Animal {
	  run: () => void
    eat: () => void
}

// 個別のInterface
interface Mammal extends Animal {
    cry: () => void
}

// 個別のInterface
interface Reptile extends Animal {}

class Dog implements Mammal {
    run() {
        console.log("RUN")
    }

    eat() {
        console.log("EAT")
    }

    cry() {
        console.log("CRY")
    }
}

class Lizard implements Reptile {
    run() {
        console.log("RUN")
    }

    eat() {
        console.log("EAT")
    }
}

参考

D: 依存性逆転の原則

概念

上位モジュールは下位モジュールに依存してはならず、両方とも抽象に依存すべきである。(= 下位モジュールの変更に上位モジュールが影響を受けないようにする)

抽象(Interfaces/Abstractionクラス)は実装の詳細(Class)に依存してはらず、実装の詳細が抽象に依存すべきである。

依存性逆転の原則に違反した具体例

下記のコードではWhetherProvider(上位モジュール)CustomHTTPClient(下位モジュール) に依存した状態であり、DIPの原則に反していると言える。

DataProvider --(依存)--> DataFetchClient
import DataFetchClient from "FetchHTTPClient"

class DataProvider {
  httpClient: typeof DataFetchClient

  constructor(httpClient = DataFetchClient) {
    this.httpClient = httpClient
  }

  getData() {
    return this.httpClient.get("")
  }
}

解決策

モジュールを抽象(Interface)に依存させるようにする。

interface HttpClient {
	get(arg: string): Promise<HttpClient>
}

class DataProvider {
  httpClient: HttpClient

  constructor(httpClient: HttpClient) {
    this.httpClient = httpClient
  }

  getData() {
    return this.httpClient.get("URL")
  }
}

参考

参考