お知らせ

現在サイトのリニューアル作業中のため、全体的にページの表示が乱れています。

個人的に考えてるNext.jsのアーキテクチャ

投稿日:
開発::設計ライブラリ::Next.js

Next.jsの全体設計を考えるときに疎結合性やテスト容易性を達成するときに考えているアーキテクチャについて簡単に書いてみる。

Page Router向けに作っていて、API Routesについては考慮していない。過去にこれに近い設計で開発していたことがあったが、単体テストによるデグレードや不具合、仕様漏れの検出はよくできていたと思う。今回書いたものは過去に考案し、開発していたもののブラッシュアップになる。

アーキテクチャ図

基本的に各レイヤー間はTypeScriptのtypeで仕切り、依存性を逆転させることで、テスト容易性や疎結合性を重視している。

next-js-arch.png

ディレクトリ構成

アーキテクチャ図にないlibrariesが登場するが、これは汎用的な共通処理だ。

src
  ├─adaptors
  │  └─User
  │     ├─index.spec.ts
  │     └─index.ts
  ├─components
  │  └─form
  │      └─TextInput
  │         ├─index.spec.ts
  │         └─index.ts
  ├─libraries
  │  └─HttpClient
  │     ├─index.spec.ts
  │     └─index.ts
  ├─pages
  │  └─hoge
  │     ├─ServerSideProps.ts
  │     ├─ServerSideController.spec.ts
  │     ├─ServerSideController.ts
  │     └─index.page.ts
  └─usecaes
      └─hoge
         ├─controller.spec.ts
         ├─controller.ts
         ├─state.spec.ts
         ├─state.ts
         ├─style.scss
         ├─usecase.tsx
         ├─view.spec.tsx
         └─view.tsx

登場する各要素について

Page

画面本体。UsecaseとgetServerSidePropsを配置し、その橋渡しを行うだけの存在。

Usecase

StateとController、Viewを橋渡しする存在。

画面全体を別物にすり替える場合もここで行う。

useEffect()はここに書くが、中のロジックはController側に書く。

一見するとファサードであり、pageにべた書きしてもいいような内容だが、コードを書く時のコロケーションの観点から敢えて分離している。

また、画面のページレイアウトが全く別物になるなど劇的な変化がある場合は、この階層で分岐制御(ユースケース別の切り替え)する。

State

useState()で作った状態を定義する場所。それ以外は何もしない。

Controller

イベントハンドラによる処理を配置する場所。APIコールもここから行う。

状態については、typeを経由してState側で宣言した状態を注入して利用する。

状態を外部から注入するため、状態変化時のテストがしやすい。また画面からロジックをはがしているため、ロジック単体のテストが可能。

Adaptor

APIを呼ぶだけの存在。データの加工や例外ハンドリングは呼び元で行う。

APIを呼ぶだけの責務とすることで、複数のコンポーネントから呼ばれたときに同じAPIを呼ぶコードが重複したり、呼び出し元によってデータ加工手法を分けるなどの煩雑な実装を回避するのが目的。

テストとしては引数や戻り値、呼び出し方法が実装時から変わっていないかを見る観点のみあれば回帰テストとして機能する。

View

ほぼ純粋なJSXを書く場所。ロジックは原則として書かない。booleanを使ったDOMの切り替えは記述してよい。

制御はUsecaseでStateを合成したControllerで行う。

表示非表示の分岐のみにすることでtesting-libraryを利用したJSXの表示切替を単体テストとして実装できる。

UI Component

TextInputみたいな細かいパーツや、再利用されるフォームUIなど、UI系の共通部品。

基本的に状態は持たないが、無限ループが起きず、再利用されない状態(親に渡す必要がなく、自分自身に閉じた状態)については持ってよいと考えている。例えば、OK/CancelのあるモーダルでOKが押された時だけ呼び元に返す状態は持ってよい。

View, Controller

ページコンポーネント向けの内容に準ずる。Usecaseを持つほど大規模なコンポーネントはないと思うので、Usecaseなしで繋ぎ合わせてよいと考えている。

ServerSideProps

getServerSideProps()の中身。Page側では以下のようにして呼び出す想定。

import { execServerSideProps } from './ServerSideProps';

export const getServerSideProps = (async () => {
  execServerSideProps();
});

ServerSideController

ServerSidePropsの中で利用するロジック。APIを呼ぶ場合はAdaptorとも繋がる。

利点

  • 各レイヤーやコンポーネントでの単体テストが容易
  • MVC的な構造のため理解しやすい
  • SOLID原則で得られる利益を享受しやすい

欠点

  • ボイラープレートコードが増える
    • とはいえ、DDDよりは少ない
  • Modelに相当するものがないため、ControllerがFatになる。またModel処理の共通化ができない
    • Modelをどこに配置すべきかを検討できていない
  • typeに破壊的変更が起きると数珠繋ぎに修正が必要になり、コストが重い
    • その代わり型で各コンポーネントの関係性がわかる利点もある

あとがき

構想自体は4年前に考えたものだがアウトプットができていなかった。まだ煮詰まっていない上に考慮出来ていない部分もあるが、AppRouterの登場からだいぶ経ち、陳腐化してきそうだったので、取り敢えず吐き出した。