Patterns For Large-Scale JavaScript Application Architecture
クライアントサイドJavaScriptで複雑なアプリケーションを作るにおける議論した内容をまとめたものです。
同名のスライドをベースに、実際のものに近い開発ガイドや雑多な内容を追加しています。
作成するアプリケーションによって必要な構造は異なるため、この構成がよいということを主張するものではありませんが、 何か参考になるものがあれば幸いです。
140文字しか表示されない環境向けのサマリです。
JavaScriptで複雑なアプリケーションを作る構成と実践ガイド。 ドメインモデルをどのように考えて作っていくかについて。 Babel、React、Almin、PostCSSがベース。
- 難しいものを簡単に作れないため、難しいものは考えて作るしかない
- 考えて作るためには、議論できる言語化されたもの(コード)が必要
- 長期的にメンテナンスするならこの傾向はより必要
- ルールは明確に、でも最初から明確なワケではない
- 議論できるベースをどのように作っていくかについて
ここでは、ライブラリ抜きで数万LOC(lines of code)以上ぐらいを目安に考えている。
- Almin.js | JavaScriptアーキテクチャ
- Alminをどのように考えて実装していったかのスライド
- 構造化の考え方などについて
- 複雑なJavaScriptアプリケーションを考えながら作る話
- Fluxに慣れている人向けのスライド
- Fluxでドメインモデルを扱うにあたりどこが曖昧に感じるかという点をベースにしている
- CQRSを参考にAlminを実装するまでの話
- どのような設計思想で作られているかについて
- 参考資料
- その他の参考資料まとめ
- 書籍や記事、スライドなど
- アーキテクチャをめぐるたび | Web Scratch 別視点でのまとめ
具体的なコーディングルールなどの開発ガイドのドキュメントは次を読む。
- docs - 目次や全体像について
- azu/presentation-annotator
- 開発ガイドをできるだけ適用した参考実装
Create New Issue.
Issue作ってそこに書くか、Twitterとか適当に聞いてください。
以下は考え始めたときに「こういう情報を教えてくれる人がいれば助かったのにな」というのを書いたものです。
Inspired by https://github.com/tokuhirom/java-handbook
Alminはできるだけクラスで書けるようにした。 色々な言語のバックグラウンドをもつ人にとってクラスで書けたほうが直感的に理解ができるため。
Reduxのように関数を主軸した方が柔軟性やImmutabilityとの相性がいい。 しかし、プロジェクトに新しく入る人が読みやすいコードとはまた異なる印象のものができあがる。 また素のJavaScriptだとPayloadオブジェクトなどに型を付けにくいという問題がある。
JSDocでは、オブジェクトに対して型を付けるよりも、クラスのインスタンスとした方が型を扱いやすい。(JSDocのtypedefの使い勝手の問題も大きい)
これはTypeScriptやFlowなどを使えば解決し易いが、あくまでJavaScriptとして書いたときの理解を優先している。
問題が複雑化していくほど1つのモデルでは線形的に複雑度が上がっていく。 モデルを小さく分けていくことは、線形的に上昇する複雑度を軽減するためのパターン。
小さなプロジェクトなら分割の単位は大きくてもあまり問題は起きない。 プロジェクトが大きくなり、人が増えてきた場合に色々問題が起きやすい。 単純にファイルのコンフリクトする可能性が上昇する。
無意味に分ける必要はないが、分けられるなら積極的に分けた方がよい。 役割が分担され、複数人で並行的に開発がしやすくなる。
たとえば、コンポーネントは小さく作り、そのコンポーネントをレイアウトするコンポーネントを作って配置する。 UseCaseはUseCaseごとにファイルとして分け、むやみにUseCaseをまとめないようにするなど。
また、ファイルという単位ではなくレイヤーという大きな単位で分けることは、変更の影響範囲を限定しリファクタリングをしやすくする。
アーキテクチャで重要なものとしてレイヤー化があるが、優れたレイヤー化とは何か? レイヤーに変更を加えても他のレイヤーに影響しないこと
- use-case/ ディレクトリを見たときに、誰(アクター)が何をしたいのかが把握しやすい
- 1つファイルに複数のことが書かれていると読むときのノイズとなりやすい
参考: オブジェクト開発の神髄 P189
UseCaseはアクターから見た能動的な名前にし、受動的な名前を避ける。
- ✗「ゼミは学生に指定される」
- ◯「学生はゼミを指定する」
UseCaseの目的は、アクターがシステムをどうしたいかを理解するため。 なので、受動的よりも能動的な表現で理解した方が望ましいため。
この問題はDOMの状態をシステムへ反映するときによく考える必要がある。 システムが変更されたからDOMに反映されるではなく、DOMのイベントを起因としてシステムを変更するという形のとき、 UseCaseを受動的にしたくなってしまう。 そこを堪えて能動的な名前にした方がよいと気がしている。(混ざってしまうのを避けたい)
参考: オブジェクト開発の神髄 P189
UseCaseにはそのユースケースにおけるアクションの一連の流れを書く。 機能をUseCaseに書くのではなく、あくまでどのような流れなのかを書く。
実際の機能と呼ばれるロジックはドメインモデルに書き、 UseCaseはそのドメインモデルを使った流れを記述する場所です。
UseCaseの事前条件はできるだけクリアにしておく。 そうすることでUseCase自体はスッキリと実装できる。
そうでない場合は、UseCase内容として無駄なチェックが増えてしまい本質として何がしたいのかがわからなくなる。
たとえば、アプリの初期化が済めば必ず存在するデータをUseCaseで触るケースについて考えてみる。 このときに、そのデータがあるかの判定をUseCaseに含めるとif文の記述が増えてしまいます。 そのため、そのUseCaseの事前条件として、そのデータは存在しているとして書いたほうがシンプルになる。
もちろんそのUseCaseにそのような事前条件があるということは、コメントや仕組みとして書いた方がいいです。
ドメインへ。
すごい難しい。 そのドメインのしごと、ライフサイクル、複雑度合いなどを考慮してドメインを分ける。 チームメンバーと相談し、都度判断する。
逆にいえば、相談して作れない作りになっていたらおかしい。
ドメインはできる限りPlain Old JavaScript Object - ただのJavaScriptで書く。 これはドメインの独立性を維持し、コアドメインに集中するためでもある。
Dateやデータ構造的な問題でJavaScriptに足りない機能がある場合はこの限りではない。
フレームワーク独立
- The Clean Architecture | 8th Light
- クリーンアーキテクチャ(The Clean Architecture翻訳)
- 持続可能な開発を目指す ~ ドメイン・ユースケース駆動(クリーンアーキテクチャ) + 単方向に制限した処理 + FRP - Qiita
Alminというフレームワーク?を作って導入しているが、そのフレームワークはドメインに対して影響は与えない。 フレームワークとビジネスドメインは独立してないといけないため。 それをかんたんに推し量る目安として、ドメインはPlain Old JavaScript Objectで書けるというものがある(ライブラリ依存してない)
ドメインとStateを分けたことで、アプリケーションの状態(ドメイン)とViewの状態(State)を分けて考えられます。 この場合に、Stateは一時的な状態として扱い、信頼的できる唯一の情報としてはドメインを利用します。Stateをドメインから作成できるようにしておくと、Stateを作り直すことが簡単になります。
たとえば、history.pushState
でページ切り替えを行う際に、Stateをリセットしたいことが多いはずです。
逆に、Viewでしか利用しない状態などはStateだけで管理しておきます。 (アニメーションやメニューを開いているなどの状態)
この問題は実際のアプリケーションを作る際に置いて選択を迫られることが多いです。
<video>
は右クリックからUIの状態を変えられるが、システムの状態に齟齬が出たときにどちらを優先するか?- エラー表示の状態をStateで管理した際に、
pushState
でのページ切り替えをおこなった際にStateはリセットすべきなのか?
この問題は、パフォーマンスや体験などの問題からトレードオフが発生します。 多くの場合において、ドメインよりもStateの方が作り直すことは簡単です。 そのため、基本的にはドメインを信頼できる情報源として、StateはViewのための状態とした方がよいです。
React Componentもまずは、すべてのpropTypesをisRequired
な状態で書き始める。
optionalだと型が違った場合にエラーがでないので、typoもエラーにならない。
static propTypes = {
src: React.PropTypes.string.isRequired
};
ドメインなども同様にエラーとなるテストを書くところから始める。
開発中はできるだけデバッグログをだし、どこで動作がおかしくなったのかを追跡できるようにしているといい。 そのため、デフォルトは厳しくし、不要なら例外的に外すような作りにしていく必要がある。
- デバッグログが増えすぎると埋もれてしまい無視してしまうことがある。
- Lintを増やすと警告がでてるのを無視してしまうことがある。
- JSDocの型が嘘をついていて気づかないうちに不正な値が入ってる。
完全に機械的な処理は無理なのでレビューもする必要がある。
console
ログはconsole.groupCollapsed
でまとめる- Lintを通らないとgit pushできないgit hook scriptを使う
- JSDocの型を使いruntime assertする
など気づけるような仕組みを少しづつでも取り入れていく。
ドキュメントに書いてあっても、気づけないならそれは形骸化する可能性がある。
たとえば、React ContextはContainer Componentのみが使えるというルールにしている。(component.mdを参照) このルールを知らない人は、Project componentでもContextを使った方が楽なので使ってしまう。
そのため、eslint-plugin-no-allow-react-contextというESLintのルールを書いて、Contextを使える場所を限定している。
このようなルールは機械的に判断して、間違った書き方をしたらエラーとなるようにした方がいい。
たとえば、UseCaseクラスは次のルールで実装するというルールにもなっている。
- ファイル名と同名のUseCaseクラスをexportしている
ファイル名+Factory
のUseCaseのFactoryクラスをexportしている
このようなルールはできるだけ機械的にチェックする。 JavaScriptで静的なチェックが難しいなら、メタなテストとして実行してテストする。
こういうことができるようにレイヤーを分けているはずなので、機械的なチェックのしやすさもルールの基準とする。
'use strict';
import assert from 'assert';
import glob from 'glob';
import path from 'path';
import interopRequire from 'interop-require';
const srcDir = path.join(__dirname, '../src/');
const useCaseFiles = glob.sync(`${srcDir}/js/use-case/**/*UseCase.js`);
describe('MetaUseCase testing', () => {
useCaseFiles.forEach((filePath) => {
// UseCaseはファイル名と同じUseCaseクラスを定義している
const UseCaseName = path.basename(filePath, '.js');
// ES6 modules と CommonJSどちらでも読めるように
const UseCaseModule = interopRequire(filePath) || require(filePath);
describe(`${UseCaseName}`, () => {
it('UseCaseファイルはクラスをexportsしてる', () => {
assert(UseCaseModule, `UseCaseファイルはexportsしてる: ${filePath}`);
});
it('UseCaseファイルはファイル名と同名のUseCaseを持つ', () => {
const UseCase = UseCaseModule[UseCaseName];
assert(typeof UseCase === 'function', 'UseCaseクラスが存在する');
});
it(`UseCaseファイルは ${UseCaseName}Factory クラスを持つ`, () => {
const Factory = UseCaseModule[`${UseCaseName}Factory`];
assert(typeof Factory === 'function', 'Factoryクラスが存在する');
assert(typeof Factory.create === 'function', 'Factory.create()が実装されている');
});
});
});
});
ESLintなどのプラグインを書けば、静的にチェックできる部分も多いはずなので、 1時間以内に書けそうな感じならさっさと書いてしまう方がよい。
レビューで指摘する前に、機械的にチェックして落とした方が全体として良くなる。
JavaScriptはESLint、CSSはstylelintで簡単に独自のルールが作れる。 機械的にチェックできることを発見したらルールを書いてみると、その利益を受け取れる人は複数人いるため効果が分かりやすい。
一人が頑張れば受益者が多いという構図は物事を良い方向に向かわせる起爆剤にはなるので、違いを意識しておくと良いと思います。 -- @t_wada
DDDはCoC(convention over configuration)と相性が良くないという話もある。 それとは関係なく、CoCが増えるとプロジェクトに参加する人が覚えることは増える。 そのため、できるだけ覚えることは減らすようにしたほうがよい。
たとえば、babel-preset-es2015だけの変換だと、エラーを継承したカスタムエラーは作れない。
class CustomError extends Error {
}
エラーのようなオブジェクトを実装して使うというルールを入れるのもいいが、 覚えることが増えるのでツール側で解消できるなら、そちらのほうがよい。
ツールで解決した方がいいことは、せっかく標準化されていること対して現状では問題がおきた場合の話。 そのときに微妙な回避方法を取るよりは、ツール側で頑張ってみるということ。 (webpackでCSSもrequireしましょうとかではない)
Node.jsのassert
は積極的使っていきたい。
unassertを使えばproduction時に外すことが可能なので、開発中はassertをもっと使っていいはず。
これはECSSの考えの中に書いてあったことを参考にしている。 "賢く"とは、Sassなどでネストや関数などを使ってスクリプトのように書くことを言っている。 つまり、CSSはDRYではなくていいと考えている。この考え方はECSSの影響を受けている。
最終的にCSSはCSSと出力されるので、CSSのメタレイヤーで賢くやりすぎるのも問題があると考えている。 "賢く"やるより、必要がなくなったらすぐに消せるかを判断できるCSSの方が望ましいと考えている。
特にCSSでは例外が出てきやすい。 そのため、原則が守れないと崩壊してしまうルールよりは、例外を規定することで原則を守れるルールの方がよい。
SUIT CSSなどのルールを使っているのは、詳細度を一定にするためという面が多い。 詳細度を考えてCSSを書くのはとてもむずかしいので、普通に書いたら普通になるという状態を目指しておきたい。 そのために、特別でないものはすべて一定の詳細度にしていく。
Stateは必然的に詳細度が上がるので優先されるなどがちゃんと機能する状態を作るためにも詳細度は一定にする。
簡単にいえば、それぞれの要素には一意なクラスを付けてそれを使えば詳細度は大体安定する。 SUIT CSSはそういうことを決めた命名規則。
BEMなども同じような仕組みを持っている。
パターンは作り出すものじゃないという話をどっかで読んだ。
Alt: すべてのアプリケーションに同一のアーキテクチャは適応できない。
このパターンが最強!みたいなものは存在しない。
ショッピングカートを実装してみると必要性が分かる。
- voronianski/flux-comparison: Practical comparison of different Flux solutions
- almin/example/shopping-cart at master · almin/almin
- ショッピングカートをAlminで実装したもの
ユーザの入力に対して1F以内に反応を適用しないと体験が良くないケースはある。
例としてストップウォッチのようなものなどが該当する。
- 押してから1フレーム以内に表示が止まってほしい
Flux的なフローだと一周回してから反映するため、常に非同期に回す設計だとこの問題への対処が難しい。 そのため、同期的に一周回せるルートを用意しておくと、このような例外的なケースも同じ一方通行のデータフローで書けるはず。
StateはImmutableである方がよい。
ReactのshouldComponentUpdate
を信用して、shallowEqualで更新判定ができるように作っていたほうがいい。
パフォーマンスは更新判定をちゃんとやれば問題なく、StateのMutableに扱うとバグの原因なりやすいため。
- Container Componentが受け取る
- こうすることでpropTypesが
React.propTypes.instanceOf
のチェックだけでよくなる
import ViewState from "../path/to/store/ViewState";
export default class ContainerComponent extends React.Component {
static propTypes = {
view: React.PropTypes.instanceof(ViewState).isRequired
};
}
AlminだとこのStateクラスをパターン化したalmin-reduce-storeというライブラリがあります。
Project Componentやui-kitを使ってページをレイアウトするコンポーネント。
- React Contextを参照してよい
- 逆にここ以外でもReact Contextを参照するとどこでもグローバルに値を取り出せてしまう
- そうするとProject Componentなどと分けた意味がなくなるので禁止する
- https://github.com/azu/eslint-plugin-no-allow-react-context を使いReact Contextが利用できる場所を制限する
プロジェクト固有のUIコンポーネント。
UIがあるならステートレスなコンポーネントとしてUIから作るのもいい。 その後でUseCaseやドメイン、State作りを実際にアプリケーションを動かせばいい。
ドメインやUseCaseをどう書くべきか迷った場合は、まずUseCaseから考えてみるとよい。 まずは、アクターがシステムに対して何をしたいのかを書き出してみると、どのようなドメインがあって何をするべきかがでてくるはず。
Start from the Use Cases
The best place to start when trying to understand a new domain is by mapping out use cases. A use case lists the steps required to achieve a goal, including the interactions between users and systems. Work with business users to understand what users do with the current system, be it a paper‐based process or one that’s computerized. Be careful to listen for domain terminology, because this forms the start of your shared language for describing and communicating the problem domain. It’s also useful to read back the use case to the domain expert in your own understanding, so they can validate that you do understand the use case as they do. Remember: capture a process map of reality, understand the work ow as it is, and don’t try to jump to a solution too quickly before you truly understand and appreciate the problem. -- Patterns, Principles, and Practices of Domain-Driven Design
メッセージの翻訳は仕組み上色々漏れが生まれやすい。 漏れが生まれにくい仕組みにすることが重要。
AlminとReactを使ったアーキテクチャは、 サーバ側のようなデータフローを行えるようになってる。 なので、「この場合はどうするのがいいんだろ?」というときにサーバ側ではどうやってるかを考えるのも参考になる。
たとえばルーティングとかをクライアントサイドでやる場合に、サーバではどのようにやるのが一般的なのかを考えるなど。