Skip to content

Instantly share code, notes, and snippets.

@azu
Last active September 17, 2020 15:51
Show Gist options
  • Save azu/54313f61b47da4ed1d8e3c2314d2bde5 to your computer and use it in GitHub Desktop.
Save azu/54313f61b47da4ed1d8e3c2314d2bde5 to your computer and use it in GitHub Desktop.
AlminとFluxやReduxの違い?

AlminとFB FluxやReduxの違い?

Almin

最低限のコンポーネントを並べると他のライブラリとあまり変わらないです。 ただレイヤーを意識して実装してみると、他のライブラリのチュートリアル通りの実装比較では実装量が増えると思います。 ただし、その場合も増えているのは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非依存)

Promiseをサポートしてる

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

-- https://stackoverflow.com/questions/26581587/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

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 UtilsStoresも同じような機能を持ちます。

Stores contain the application state and logic. Stores

FB FluxではこのStoreにロジックも実装することが想定されています。 AlminでのStoreではここにロジックを実装するのはさけて、あくまでロジックはdomainで、Storeは表示のためのものという役割分担をしています。

Middlewareのような仕組みがない

Almin(0.16時点)ではReduxのようなmiddlewareの仕組みはありません。

ただし、UseCaseを実行開始/実行済み/実行完了、エラーが発生、dispatchStoreの変更などといった必要なイベントは取ることが出来ます。

これらのイベントを使うことでalmin-loggerのようなロギングの仕組みやPerformance profileを取ることが出来ます。

Reduxなどのmiddlewareの特徴的な仕組みはdispatchされたPayloadを変形する点です。 イベントの受信は基本読み取り専用(発生済みのイベントなので変更が意味ない)で、Middlewareが必要になるのは書き込みするコマンドを扱い変形させたい場合にあると思います。

ただしMiddlewareはシステム全体に影響を与えるため、そのような横断的な存在が必要になるケースはログを取りたいなど一部に限られていると思います。

Alminの実装背景にはアプリケーションをレイヤーに分けて実装するレイヤードアーキテクチャがあります。 そのため横断的な存在はかなり例外的なものとなっていて、今のところMiddlwareがないと実装難易度が大きく変わるというケースがまだ見つかってないためMiddleware的な仕組みはまだありません。 (ログなどはイベントだけあれば実装できるけど、書き換えが必要になるケースがまだ分かってない)

逆にいうとreact-router-reduxredux-persistのような暗黙的にアプリケーション全体が連動するような仕組みを入れる方法が今はないです多分(工夫すればできるけど、やりやすくはないはず)

パフォーマンスプロファイル

ReactやVue、Angularなど有名なライブラリは開発者ツールでパフォーマンスプロファイルをサポートしています。AlminはViewライブラリではないので、これらのライブラリと組み合わせてつかうことを想定します

perf

Alminはビルトインでパフォーマンスプロファイルをサポートしていて、UseCaseの実行時間やStoreの更新にかかった時間などをブラウザ開発者ツールに表示できます。

const appContext = new Context({
    dispatcher: new Dispatcher(),
    store: yourStoreGroup,
    options: {
        strict: true,
        performanceProfile: true
    }
});

標準APIを利用しているので他のライブラリとも組み合わせてプロファイルをとれます。

複雑なアプリを書いているといつの間にか重たい箇所などがでてくるので、そういったものを色々な視点から見つけやすくしています。

クラスベース

Alminは意図的にクラスベースにしています。 これは分かりやすさ(とっつきやすさ)を優先しているので、場合によっては問題となることがあります。

もっとプリミティブな実装にするなら別ライブラリとして書くかなと思います(Reduxもあるし)。

TypeScript

Almin自体がTypeScriptで書かれているので標準でTypeScriptをサポートしています。

コミュニティ

使ってる人が少ないので情報源は多くない(書いてくれると参考になります)

基本的な考え方はDDD、CQRSなどの言語に依存しないものをベースにしているので、遠回りすると似たような話(他の言語が殆ど)が見つかる。

トレードオフ

  • 初手のファイル数が少し多めになりがち
    • 途中から楽になっていくイメージ
    • 自動生成を目的としてない構造(DDDがあんまりそういうのに向いた構造にならない)
      • 自動生成もできるだろうけど、本質的に解決しない感じがして放置してる
  • 巨大なStateをViewに渡す
    • MobXみたい?なContainerとStoreが1対1というデザインにはしにくい現状(分割するデザインは検討)

トレードオフをまとめたい

0.x

この辺解決したら1.0になるきがしている。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment