🤔

SOLID原則を学ぶ

2022/01/10に公開

Robert.C.Martin氏、いわゆるアンクル・ボブ (ボブおじさん Uncle Bob) の本。
「Clean Architecture」を読んで、SOLID原則(5つの原則)について、学んだので(自身の整理のためにも)以下に記載する。

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

単一責任の原則(SRP)

「モジュールを変更する理由はたったひとつだけであるべきである」と語られているが、この変更する理由というのが「ユーザーやステークホルダー」であるべきとのこと。
本書では、この「ユーザーやステークホルダー」のことをアクターと呼んでいる。
つまり、言い換えると「モジュールはひとりのアクターに対して債務を負うべきである。」となる。

たとえば、社員クラスがあったとして、そのクラスは様々なアクターで使われるだろう。
その時、モージュルとデータが分離されていない場合、アクター間の修正によりデータの不整合が起きる可能性がある。そのため、モージュルとデータをちゃんと分離する。(本書ではFacadeパターンを用いるのが良いと伝えている)

class Employee {
    name() { // NG: データは別に持つべき
      return 'Kodak'
    }

    department(dept: DepartmentInterface) { // OK: データは別クラスで持つ
      return dept.name()
    }
}

interface DepartmentInterface {
  name(): string
}

class SystemEngineer implements DepartmentInterface {
  name() {
    return 'SE'
  }
}

オープン・クローズドの原則(OCP)

「ソフトウェアの構成要素は拡張に対して開いていて、修正に対して閉じていなければならない」と語られている。つまり、「ソフトウェアの振る舞いは、既存の成果物を変更せずに拡張できるようにすべきである」ということ。

class Printer {
  print(printer: PrinterInterface) {
    return printer.print()
  }
}

interface PrinterInterface {
  print(): void;
}

// 機能追加したい場合は、以下「Printer」クラスを増やすだけ。
class DefaultPrinter implements PrinterInterface {
  print() {
    // ...
  }
}

class HtmlPrinter implements PrinterInterface {
  print() {
    // ...
  }
}

class TextPrinter implements PrinterInterface {
  print() {
    // ...
  }
}

リスコフの置換原則(LSP)

「S型のオブジェクトo1に対応する各々に、T型のオブジェクトo2が1つ存在し、Tを使って定義されたプログラムPに対して、o2の代わりにo1を使ってもPの振る舞いが変わらない場合、SはTの派生型」と語られている。

。。。わかり辛い。

NGパターンの代表例として、以下のような正方形クラスが長方形クラスを継承しているようなパターンはNGとされている。
なぜなら、長方形は幅と高さを独立して変更できるのに対して、正方形は両方同時に変更する必要がある。これをユーザーが長方形と信じるとおかしなことになるから。

// rectangleには長方形 or 正方形が入る。
// 長方形は幅と高さを独立して変更できるのに対して、正方形は両方同時に変更する必要がある。
// user関数からみるとこれが判断できない。。。
function user(rectangle: Rectangle){
	// ...
}

class Rectangle {
  constructor(protected height: number, protected width: number) {
  }
  setH(set: number) {
    this.height = set;
  }
  setW(set: number) {
    this.width = set;
  }
}

class Square extends Rectangle {
  constructor(height: number, width: number) {
    super(height, width)
  }
  setSide(set: number) {
    this.height = set;
    this.width = set;
  }
}

user(new Rectangle(1,2))
user(new Square(2,2)) // 注意: 継承しているのでSquareでも呼び出せる

こういった場合、インターフェースを使って事前に設計を綺麗にしておく(たとえば、長方形なのか正方形なのかがわかるメソッドを定義しておくなど・・・)

インターフェイス分離の原則(ISP)

一言で言えば「使っていないメソッドを持つクラスに依存してはならない」ということ。
きちんとインターフェースをグループごとに分けて適切に使う。

interface AnimalInterface {
  sleep(): void
  eat(): void
  fly(): void
}

class Human implements AnimalInterface
{
  sleep() {}
  eat() {}
  fly() {} // Humanクラスでは、fly()は使用しない
}

依存関係逆転の原則(DIP)

「ソースコードの依存関係が(具象ではなく)抽象だけを参照しているもの。それが、最も柔軟なシステムである。」つまり、クラスを呼び出す時は抽象を使えということ。

class Animal {
  createPenguin() {
    const penguin = new Penguin() // AnimalクラスからPenguinクラス(具象)へ依存
    penguin.swim()
  }
}

// 上の方法ではなく、抽象を使う
class Animal {
  createPenguin(penguin: PenguinInterface) { // Animal → PenguinInterface ← Penguin へと依存関係を変える。
    penguin.swim()
  }
}

上のは一例だが、本書においても、例でデザインパターンの1つ「Abstract Factory」を使う説明の記述があった。つまり、クラスを呼び出す時だけでなく、継承においても変化しやすい具象クラスではなく抽象を使うべきだということ。

Discussion