Slide 1

Slide 1 text

フロントエンドの複雑さに立ち向かう 〜 DDD と Clean Architecture を携えて 〜 さくらのテックランチ Vol.6 ダーシノ / @bc_rikko

Slide 2

Slide 2 text

ダーシノ(@bc_rikko) シニアフロントエンド エンジニア 代表作 NES.css ファミコン風CSSフレームワーク GitHub ⭐️ 20,000+ さくらのINFRAWARS インフラエンジニア育成型 サーバー防衛シミュレーションゲーム

Slide 3

Slide 3 text

話さないこと コンポーネント設計 加速するコンポーネント設計入門: https://speakerdeck.com/bcrikko/component-design-as-an-accelerator CSS設計 OOCSS、BEM、SMACSS、FLOCSS、RSCSSを比較して設計思想をみつける: https://kuroeveryday.blogspot.com/2017/03/css-structure-and-rules.html SPA/MPAやCSR/SSRなどフレームワークが絡む領域 Webサーバ、CDN、BFFなどを含むインフラ領域のアーキテクチャ

Slide 4

Slide 4 text

なぜフロントエンドは複雑になるのか? コア部分のトレンドがだいたい5年周期で替わる(※1) 秘伝のタレ化したコードは Smart UI(利口なUI)(※2) になりがち リッチなUIはビジネスロジックと同じくらい実装がいる (BFFを使わない場合) 複数のバックエンドの差異を吸収する NOTE: ※1: 超ざっくりフロントエンド年表 … jQuery(2006) → Backbone.js/AngularJS(2010) → → React(2013)/Vue.js(2014) → React16 関数コンポーネント(2018)/Vue3 CompositionAPI(2020) ※2: Smart UI(利口なUI)… UI(View層)にビジネスロジックが含まれている状態。DDD ではアンチパターン。 ただし、小さなアプリケーションの場合には開発効率の面で有効な場合もある。

Slide 5

Slide 5 text

さくらのクラウドUIで感じた課題 10年以上の歴史と趣があるコードで、Smart UI 化している 手続き型で実装されており、コードから仕様が読み解きづらい 独自UIライブラリが剥がせないほどビジネスロジックと結合している テストが書きづらいため、デグレしやすい 同じ概念が場所によって異なる命名になっている 複数のバックエンドの差異がフロントエンドを侵食している ハチャメチャが押し寄せてくる Pascal → ラスカル → あらいぐま →

Slide 6

Slide 6 text

どうすれば開発しやすくなるか 関心・責務ごとにレイヤーが分かれていること フレームワークやライブラリが捨てやすいこと 依存が少なくテストが書きやすいこと 一貫した共通の言語でやりとりができること

Slide 7

Slide 7 text

注目した設計思想・哲学 ドメイン駆動設計 (Domain-Driven Design) エリック・エヴァンスのドメイン駆動設計 著: Eric Evans https://www.shoeisha.co.jp/book/detail/9784798121963 Clean Architecture Clean Architecture 達人に学ぶソフトウェアの構造と設計 著: Robert C.Martin https://www.kadokawa.co.jp/product/301806000678/

Slide 8

Slide 8 text

ドメイン駆動設計 の 良いところ と イマイチなところ 良いところ 共通言語を作り、全員で共有する ビジネスとコードを対応付ける ビジネス視点で責務を分割できる コミュニケーションの重要性を 説いている ドメインモデルを成長させる前提 イマイチなところ 書籍本文が難解でチーム全員で 共通認識を持つのが難しい 20年前のJava全盛期に書かれた ため内容が古い DDD用語と別文脈のキーワードが コンフリクトしていて誤解しやすい

Slide 9

Slide 9 text

Clean Architecture の 良いところ と イマイチなところ 良いところ 関心・責務ごとにレイヤーをわける レイヤー間の依存方向を制限する 具体的な決定を遅延させる イマイチなところ 書籍を読まずに例の同心円図を 見ると誤解が発生しやすい フレームワークの制約があるため キレイにハマらない

Slide 10

Slide 10 text

実践する前に… 読書会・勉強会を主催し、メンバー全員のスキルの底上げを図った ※ネット上の記事は方法論に関するものが多く誤解しかねないので、哲学・思想の部分を 理解するために原典の読書会や勉強会、実践を行った DDD読書会(25回) DDDモデリング勉強会(2回) OOP・SOLID原則勉強会(2回) 実践 Clean Architecture 勉強会(複数回) ペアプロ・モブプロ(30回以上)

Slide 11

Slide 11 text

実際にクラウドUIで半年間開発してみた

Slide 12

Slide 12 text

Domain層 システムのコア部分でビジネスロジックを扱う Entity システム上、ユニークになるオブジェクト ※フレームワークの制約によりイミュータブルオブジェクトになっている ValueObject 一意に識別できなくてもよいオブジェクト 値の制約や何らかのふるまいを持たせたいときに使う Service 複数のドメインオブジェクトを扱って処理を行う ※クラスベースだとメンバ変数の変更検知がされないためEntityをイミュータブルとして扱っている

Slide 13

Slide 13 text

Domain層サンプルコード class Task extends Entity { #title: string ... isDone(): boolean { ... } isExpired(): boolean { ... } /** * @deprecated * 本来ならEntityには変更系のメソッドも生えるのだが、 * JSフレームワークはメンバ変数の変更を検知できないので使えない */ changeTitle(newTitle: string) { ... } }

Slide 14

Slide 14 text

Presentation層 UIを扱う、Presentation内はMVCアーキテクチャ View HTML(JSX)/CSS を書くところ ロジックは ViewModel に任せる ViewModel Vue.js の Computed/Methods、React の カスタムHooks UIに関するロジック(データ加工やUI制御など) ViewController Presentation層とApplication層を繋ぐ薄いレイヤー

Slide 15

Slide 15 text

Presentation層サンプルコード const task = await controller.findByID(query.id) const { formattedTitle } = useTask(task)

{ formattedTitle }

{ task.isDone() ? controller.delete(task)}>Delete : controller.update(task, input)}>Update } // ViewModel function useTask(task: Task) { const formattedTitle = useMemo(() => { const status = task.isExpired() ? 'Warning' : 'Progress' const title = task.title() return `${status}: ${title}` }, [...]) return { formattedTitle } }

Slide 16

Slide 16 text

Application層 ユースケースを扱う UseCase システムを使う上でのユースケースをまとめる 例: タスクを登録する、タスクを完了する Service Domain外部の関心事を実装するステートレスな軽量サービス

Slide 17

Slide 17 text

Application層サンプルコード class TaskInteractor implements ITaskUsecase { async findByID(id: TaskID) { const task = await repository.findByID(id) return task } async delete(task: Task) { await repository.delete(task) } async update(task: Task, input: FormInput) { // 本来なら書き換えた Task をそのまま Repository に渡せば良いのだが、 // メンバ変数の書き換えはJSフレームワークで検知できないため DTO で代替する const dto = new TaskDTO(task, input) const newTask = await repository.save(dto) return newTask } }

Slide 18

Slide 18 text

Infrastructure層 API通信、データの永続化などを扱う Repository データの取得や更新、永続化を行う 実データへのアクセス方法を隠蔽し、呼び出し側はFetchを使うか、 Web Storageを使うか、In-Memory Cacheを使うか、などを意識 しなくていい APIClient さくらのクラウドAPIを扱いやすくするためのクライアント

Slide 19

Slide 19 text

Infrastructure層サンプルコード class TaskRepository implements ITaskRepository { #cache: Tasks[] async findByID(id: TaskID) { const isCached = this.#cache.some(a => a.id === id) if (isCached) { return this.#cache.find(a => a.id === id) } const res = await apiClient.get(`/task/${id}`, { ... }) const task = new Task(res) return task } async delete(task: Task) { const res = await apiClient.delete(`/task/${task.id}`) } async update(dto: TaskDTO) { const body = dto.createBody() const res = await apiClient.put(`/task/${dto.id}`, { body }) const newTask = new Task(res) return newTask } }

Slide 20

Slide 20 text

よかったところ 責務ごとにレイヤーが分かれているので秩序が生まれた フレームワーク・ライブラリ依存部分が局所化でき捨てやすくなった 依存が少なくテストが書きやすくなった メンバー全員が共通認識を持ちやすくなった 言葉の定義を厳密に扱うようになった

Slide 21

Slide 21 text

改善の余地あり 前提知識(DDDやClean Architecture、OOPなど)が増えたため プロジェクトの参入障壁が上がった 「前提知識を持っていたのでレガシーコードと比べて仕様が 理解しやすかった」 という感想もあった(11月入社の方) フレームワークの制約でアーキテクチャを厳密に適用できない Class-Based OOP と JSフレームワーク の相性が悪い データ指向プログラミングのほうが相性いいかも(未検証 冗長的なコードが増え、エディタ内で迷子になる コードジャンプしても思い通りの場所に飛ばない 似たような名前のファイルが増えて探しづらい

Slide 22

Slide 22 text

これから 複雑さに立ち向かうためには継続的なリファクタリングや知識の蒸留が 必要不可欠なので運用がはじまってからが本番 アーキテクチャは生き物なので、今後どのように成長するかわからない 責務の分解、知識の配置場所などは個人の思想が強く現れるため 俯瞰的な人力レビューが必須で、レビュー工数増加の対策をしたい 守破離の「守」の段階で愚直な実装が多いので、知見が溜まったら改善したい

Slide 23

Slide 23 text

参考リンク ドメイン駆動設計の用語と解説(DDD入門ガイド) | Black Everyday Company https://kuroeveryday.blogspot.com/2019/02/learn-domain-driven-design- quickly.html オブジェクトストレージ開発におけるDDD (ドメイン駆動設計) | さくらのナ レッジ https://knowledge.sakura.ad.jp/32024/

Slide 24

Slide 24 text

参考文献 エリック・エヴァンスのドメイン駆動設計 - Eric Evans https://www.shoeisha.co.jp/book/detail/9784798121963 Clean Architecture 達人に学ぶソフトウェアの構造と設計 - Robert C.Martin https://www.kadokawa.co.jp/product/301806000678/