最低限のコンポーネントを並べると他のライブラリとあまり変わらないです。 ただレイヤーを意識して実装してみると、他のライブラリのチュートリアル通りの実装比較では実装量が増えると思います。 ただし、その場合も増えているのはAlminに依存した部分ではなく、自分で実装しないといけないDomainやInfra(Repository)などといったレイヤーになります。 これは責務をレイヤーで分離する考え方から来ているので、他のライブラリでも同じようなレイヤーを実装すると同じようにコード量が増えると思います。(登場人物が多く見える)
Almin | Flux | Redux |
---|---|---|
Dispatcher | Dispatcher | store.dispatch |
Context | Container | Middleware/React Redux |
UseCase | ActionCreator | Actions |
Store | Store | Store |
StoreGroup | Container | combineReducers |
(State – Almin非依存) | Store | Reducer |
(Domain – Almin非依存) | ||
(Repository – Almin非依存) |
Alminでは、Promiseはファーストクラスなオブジェクトです。
// Hello World in a single file
import { Context, Store, UseCase } from "almin";
const context = new Context({
store: new Store();
});
// UseCase
class HelloUseCase extends UseCase {
// Promiseを返せば非同期
execute(name) {
return new Promise((resolve) => {
// イベントを投げる
this.dispatch({
type: "Hello",
name: name
});
// resolveしたタイミングでUseCaseは完了
resolve()
});
}
}
// UseCaseを実行
context.useCase(new HelloUseCase()).execute(name).then(() => {
console.log("成功")
}).catch(error => {
console.error("失敗", error)
});
FluxではActionCreator、ReduxではActionに相当するものとして、AlminではUseCaseがあります。
UseCaseを継承したクラスのexecute
メソッドに処理内容を実装します。
この時execute
メソッドがPromiseを返せばそのUseCaseは非同期処理として扱われます。
ただし、UseCaseを実行した返り値は外からは取れない(自動的にPromise<void>
となる)ので、外から分かるのはUseCaseが成功した/失敗したというシンプルな形になります。
これはUseCaseを実行するView側がUseCaseに強く依存してしまうのを避けるための制約です。(UseCaseはコマンドであるので、コマンド実行の正否は分かるけど、その結果まで取って使うのは大体依存関係がおかしくなるというところからきている。Viewべったりになる)
Fluxライブラリは同時にdispatch
すると次のようなエラーが発生します。
Flux Dispatch.dispatch(…): Cannot dispatch in the middle of a dispatch
これは、非同期処理をするActionを順番に実行することがかなり難しくなる制約です。
(この制約を回避するためにwaitFor
がありますが、使いにくいことでも有名です)
AlminではそれぞれのUseCaseは必ずPromiseを返す(UseCase#execute
の実装でPromiseを返してない場合も自動的にPromiseを返す)ので、UseCaseA -> UseCaseBを順番に実行するということが単純に書けます。
const run = async () => {
await context.useCase(new UseCaseA()).execute();
await context.useCase(new UseCaseB()).execute();
};
run()
AlminでのUseCaseはやりたいことのコントロールフローを書く場所です。 そのため1UseCase = 1ファイルを基本としていて、実際のロジックはDomainに書くことでUseCaseは処理の流れだけを書くことに集中させることを目的にしています。
そのため変にプリミティブな制限はせずにPromiseはPromiseとして扱い、さらにUseCaseをネストするNesting UseCase(ユースケースを再利用するというよりは、代替コースを表現する)などもサポートしています。
Storeは各ライブラリによって意味が違います。 Modelという言葉の意味がアーキテクチャによって異なるのと似たような現象だと思います。
大体のライブラリで共通しているのは「表示のための情報を保存している」という機能を持っていることだと思います。 AlminのStoreは表示のための情報を変換/保存する層です。 実際のアプリケーションの情報はdomain/repositoryにありますが、そのアプリケーションの情報から表示向けの情報に変換して、無駄な更新しなくても良いようにキャッシュする層という扱いです。
Store-StateというようにStoreというクラスがStateオブジェクトを保存する関係にして実装するパターンをチュートリアルなどでは扱っています。 最近hatebupwaなどを書いてみて、Stateはクラスじゃなくてただのオブジェクトでも良さそうかなと思っています(getterのようなものが扱えないけど、更新方法や型的に扱いやすい)
AlminではいわゆるSingle source of truthはdomain/repositoryなので、Storeはあくまで表示向け(ReactやVueなど)の情報を持ちます。 (domainの情報を信頼すればいいように作るため、Storeがもつ状態が壊れてもdomainからStoreの状態は復元できる) なので、AlminのStoreはReduxでのreselector + reducerの層と考えると大体似たようなものと言えるかもしれません。
Flux UtilsのStoresも同じような機能を持ちます。
Stores contain the application state and logic. Stores
FB FluxではこのStoreにロジックも実装することが想定されています。 AlminでのStoreではここにロジックを実装するのはさけて、あくまでロジックはdomainで、Storeは表示のためのものという役割分担をしています。
- 複雑なJavaScriptアプリケーションを考えながら作る話
- FluxのStoreの比較を書いている
Almin(0.16時点)ではReduxのようなmiddlewareの仕組みはありません。
ただし、UseCaseを実行開始/実行済み/実行完了、エラーが発生、dispatch
、Store
の変更などといった必要なイベントは取ることが出来ます。
これらのイベントを使うことでalmin-loggerのようなロギングの仕組みやPerformance profileを取ることが出来ます。
Reduxなどのmiddlewareの特徴的な仕組みはdispatch
されたPayloadを変形する点です。
イベントの受信は基本読み取り専用(発生済みのイベントなので変更が意味ない)で、Middlewareが必要になるのは書き込みするコマンドを扱い変形させたい場合にあると思います。
ただしMiddlewareはシステム全体に影響を与えるため、そのような横断的な存在が必要になるケースはログを取りたいなど一部に限られていると思います。
Alminの実装背景にはアプリケーションをレイヤーに分けて実装するレイヤードアーキテクチャがあります。 そのため横断的な存在はかなり例外的なものとなっていて、今のところMiddlwareがないと実装難易度が大きく変わるというケースがまだ見つかってないためMiddleware的な仕組みはまだありません。 (ログなどはイベントだけあれば実装できるけど、書き換えが必要になるケースがまだ分かってない)
- feature: plugin/middleware · Issue #2 · almin/almin
- @almin/usecase-busというコマンドに介入できる仕組みも作っていますがレイヤーが増えるだけなのでまだメリットが見えてない
逆にいうとreact-router-reduxやredux-persistのような暗黙的にアプリケーション全体が連動するような仕組みを入れる方法が今はないです多分(工夫すればできるけど、やりやすくはないはず)
ReactやVue、Angularなど有名なライブラリは開発者ツールでパフォーマンスプロファイルをサポートしています。AlminはViewライブラリではないので、これらのライブラリと組み合わせてつかうことを想定します
Alminはビルトインでパフォーマンスプロファイルをサポートしていて、UseCaseの実行時間やStoreの更新にかかった時間などをブラウザ開発者ツールに表示できます。
const appContext = new Context({
dispatcher: new Dispatcher(),
store: yourStoreGroup,
options: {
strict: true,
performanceProfile: true
}
});
標準APIを利用しているので他のライブラリとも組み合わせてプロファイルをとれます。
複雑なアプリを書いているといつの間にか重たい箇所などがでてくるので、そういったものを色々な視点から見つけやすくしています。
Alminは意図的にクラスベースにしています。 これは分かりやすさ(とっつきやすさ)を優先しているので、場合によっては問題となることがあります。
もっとプリミティブな実装にするなら別ライブラリとして書くかなと思います(Reduxもあるし)。
Almin自体がTypeScriptで書かれているので標準でTypeScriptをサポートしています。
使ってる人が少ないので情報源は多くない(書いてくれると参考になります)
基本的な考え方はDDD、CQRSなどの言語に依存しないものをベースにしているので、遠回りすると似たような話(他の言語が殆ど)が見つかる。
- .NETのエンタープライズアプリケーションアーキテクチャ第2版 .NETを例にしたアプリケーション設計原則 | ディノ エスポシト, アンドレア サルタレロ, クイープ, 日本マイクロソフト(監訳) | コンピュータ・IT | Kindleストア | Amazon
- Patterns, Principles, and Practices of Domain-Driven Design: Scott Millett, Nick Tune: 0787721845461: Amazon.com: Books
- 現場で役立つシステム設計の原則 〜変更を楽で安全にするオブジェクト指向の実践技法:書籍案内|技術評論社
- 初手のファイル数が少し多めになりがち
- 途中から楽になっていくイメージ
- 自動生成を目的としてない構造(DDDがあんまりそういうのに向いた構造にならない)
- 自動生成もできるだろうけど、本質的に解決しない感じがして放置してる
- 巨大なStateをViewに渡す
- MobXみたい?なContainerとStoreが1対1というデザインにはしにくい現状(分割するデザインは検討)
トレードオフをまとめたい
この辺解決したら1.0になるきがしている。