Try T.M Engineer Blog

多摩市で生息するエンジニアが「アウトプットする事は大事だ」と思って始めたブログ

「Clean Architecture 」を読んだ感想

いたるところで有名な Robert.C.Martin氏、いわゆるアンクル・ボブ (ボブおじさん Uncle Bob) の本。Cleanシリーズの3作目「Clean Architecture」を読んだので、その感想です。

Clean Architecture

f:id:special-moucom:20220109015713j:plain

でた、ドーナッツっ!!

よくエンジニア記事で見かける図ですが、「Clean Architecture」を読むことで、なんとなくこの図の意味が理解できたと思うので(私の)解釈を残しておく。

前提

この図の各層と内側に向かう→は、依存関係のルールを表したもの。

円の外側は仕組み。内側は方針を表す。

依存関係のルールとしてソースコードの依存性は、内側(上位レベルの方針)だけに向かっていかなければならない。」(内側に向かう→がそれを表している)

つまり、円の内側は外側について何も知らない。外側で宣言されたデータフォーマットは内側で使用してはならない。ということ。

エンティティ層(Entities)

最重要のビジネスルール部分のレイヤー。エンティティといえば、データベースにもクラス等の表現を用いて使われるものがありますが、それとは別。

もっと抽象的なもので、特定のフレームワークやアプリケーションに依存しないオブジェクトを表す。

ユースケース層(Use Cases)

エンティティの外側。アプリケーション固有のビジネスルール部分のレイヤー。

システムすべてのユースケースが含まれており、ユースケース間で依存関係は持ってはならない。

インターフェースアダプター層(Interface Adapters)

ユースケースの外側。エンティティ層やユースケース層、後述のフレームワーク&ドライバー層のフォーマットにデータを変換するレイヤー。

MVCやMVPアーキテクチャのコントローラー、ビュー、プレゼンターなどはこのレイヤーに位置する。(モデルは、コントローラーからユースケースユースケースからコントローラー、プレゼンター、ビューへ戻されるデータ構造にすぎない。と書かれている)

フレームワーク&ドライバー層(Frameworks & Drivers)

一番外側。フレームワークやデータベース、ツールで構成されるレイヤー。(Web、UIなども含むので、Webから入力するページ等を想像して問題ないと思う)

なお、このレイヤーではコードをあまり書かないようにする。

右下の図(境界線の超え方)

境界線の超え方が書かれている。インターフェースアダプター層のコントローラーとプレゼンターが、ユースケース層へデータを渡す例が書かれている。

また、ソースコードの依存関係にも触れていて、ユースケースからプレゼンターを呼び出す必要があった場合、ユースケースからプレゼンターを直接呼び出すのはNGなので、プレゼンターがインターフェースを実装することを表している。ここの実装は以下SOLID原則に従っている。

SOLID原則

「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」を使う説明の記述があった。つまり、クラスを呼び出す時だけでなく、継承においても変化しやすい具象クラスではなく抽象を使うべきだということ。

本書を読んで

前半は、goto文辛いよ話、オブジェクト指向の誕生、そして関数型プログラミングへと歴史の話。

以前、私が読んだ「何故オブジェクト指向でなぜつくるのか」に近しい内容が書かれていた。

kodak.hatenablog.com

そこから、SOLID原則の話とコンポーネント(デプロイ単位)の話・・・で、ようやくクリーンアーキテクチャの話。

ページ数も結構あるので、最初から読んでいると「おいおい、あのドーナッツいつ出てくるんだ?」と何度も思いました。

Clean Architectureは、抽象的な話なので「うーん、わかるような・・・わからないような・・・」ずっと本書とにらめっこしていました。

未だ「完璧に理解した」とは言えませんが、エンジニアBlogや記事等で語られているSOLID原則やClean Architectureについて、話に付いていけるくらいは理解できたつもりです。

付録では、ボブおじさんの職歴について触れていましたね。「ネクタイを強制する職場で、パフォーマンスが出なくてクビになった」とか「あのグラディ・ブーチと仕事ができるのかっ。やる!やる!」みないな話もあって、共感するものがありました。

本書は、読んでいて面白かったし、学びになることも多くありました。

ただ、誰もにでもオススメできるような書籍ではないとは思います。というのも、やはり時代を感じる部分は多々あるからです。本書が発売したのが2018年(英書は2017年)ですし、ボブおじさんが働いていた時代の話はもっと後の話です。なので、現代的なアプリの話はありません。

エンジニアBlogや記事等で語られているSOLID原則やClean Architectureについて、素直に知っておきたいなぁという方にオススメです。