オフラインファースト的なGUIアプリケーションをScala.jsで書く話 / write stack - usecase

前回の記事では、Scala.jsで書かれたモデル層がどのようなクラス/オブジェクトをJSで書かれたUI層に公開し、UI層はそれらをどのようにして扱うのかというのを見てきました。今回からはScala.jsで書かれたモデル層のうち、コマンドから始まる一連の状態更新系(write stackと呼んでいます)について見ていきたいと思います。

write stackを構成するpackageとクラスたち

write stackを構成するクラスは以下の通りです

  • usecase パッケージ
    • Commandクラス
    • Serviceクラス
  • domain パッケージ
    • Domain Modelを表すクラスたち
  • infrastructure パッケージ
    • Repositoryクラス
    • Synchronizerクラス

このうち、今回はusecaseパッケージに注目してみましょう。

usecaseパッケージは、いわゆる「アプリケーション層」に相当するパッケージです。アプリケーション層の責務は、アプリケーションへの入力を受け取り、domaininfrastructureにメッセージを送り、ユースケースを実現する責務です。

逆に言うと、この層に「ユースケースを実現する」以外の関心が書かれてしまうと「ドメインモデル貧血症」を起こすことになりますので、この層は薄く実装されるべきです。

write stackでは、CommandクラスとServiceクラスがこのusecaseパッケージを構成しています。

Commandクラスの責務

Commandクラスの責務は、以下のふたつの責務を持っています。

  • ユーザー入力のvalidation
  • ユーザー入力をアプリケーション上意味の意味のある値に変換
  • Serviceのdispatch

わかりやすい例はAddTodoCommandでしょう。

trait Command {
  protected val addTodoService: Service

  private var _todoInput = ""
  private var _dueDateInput = ""

  def todoInput = _todoInput
  def todoInput_=(v: String) {
    _todoInput = v
  }

  def dueDateInput = _dueDateInput
  def dueDateInput_=(v: String) {
    _dueDateInput = v
  }

  def isTodoInputValid = _todoInput != ""
  def isDueDateInputValid = {
    val ret:Boolean = try {
      dueDate
      true
    } catch {
      case _:DateTimeParseException => false
    }
    ret
  }

  def isExecutable = isTodoInputValid && isDueDateInputValid

  def dueDate = {
    LocalDate.parse(dueDateInput)
  }

  def execute() = addTodoService.execute(this)
}

todoInputdueDateInputは、フォームの値が変更されたらUI側からここに値がぶっこまれてきます(前回見たとおりですね)。

is*Valid系のメソッドがユーザの入力値の妥当性を検証しています。この値をUI側から読み出すことで、エラーメッセージの表示などができるわけですね。

また、dueDateInputString型ですが、アプリケーション内ではStringではなくてLocalDate型で扱いたいですよね。その変換もこのクラスが行っています。

また、executeメソッドでは自身を引数にServiceをdispatchしています。

このaddTodoServiceが抽象メンバー(って正しい言葉なのかな、わかんない)であるところがちょっとしたポイントですね。Commandクラスが単体テストしやすいように他のクラスへの依存は抽象メンバーとして定義しておいてDIするわけです。DIについてよくわからない、という向きは手前味噌ですが要するに DI って何なのという話がわかりやすいと評判なのでおすすめです。

ちなみに、Scala.jsで定義されたクラスなどをJS側から触るためには、@JSExportの仲間たちのアノテーションを付けてやる必要がありますが、今回はjs_bridgeというパッケージに「JS側にexportするクラス」を置いています。見てみましょう

@JSExportTopLevel("AddTodoCommand")
class AddTodoCommand extends Command {
  protected val addTodoService = new ServiceImpl

  @JSExport
  override def todoInput = super.todoInput
  @JSExport
  override def todoInput_=(v: String) = super.todoInput_=(v)


  @JSExport
  override def dueDateInput = super.dueDateInput
  @JSExport
  override def dueDateInput_=(v: String) = super.dueDateInput_=(v)

  @JSExport
  override def isTodoInputValid = super.isTodoInputValid
  @JSExport
  override def isDueDateInputValid = super.isDueDateInputValid

  @JSExport
  override def execute() = super.execute()
}

さきほどはCommandをtrait(多重に継承できる抽象クラスみたいなもんです)として定義して、addTodoServiceは抽象メンバーでしたね。それを継承してJS側にExportするタイミングで、実際のServiceの実装を配線してDIしているのが見て取れるでしょう。

また、publicなメソッドであっても明示的に@JSExportしないとJS側からはそのメソッドをさわれないため、exportするメソッドは明示的にoverrideしています。しかしここには「実装」は一切書かれておらず、ロジックはすべてピュアScala.js(って言い方で通じるかな)であるtrait側に書かれていることに注意してください。

Serviceクラスの責務

では、CommandクラスによってdispatchされるServiceクラスは具体的にどのような仕事をしているでしょうか。「todoを追加する」というユースケースを実現するusecase.add_todoパッケージのServiceを見てみましょう。

trait Service {
  protected val repository: TodoRepository
  protected val synchronizer: TodoSynchronizer

  def execute(command: Command): Unit = {
    if ( ! command.isExecutable ) {
      return
    }

    // ...(1)
    val todo = Todo.open(
      id = repository.nextId(),
      body = command.todoInput,
      dueDate = command.dueDate
    )

    repository.store(todo) // ...(2)
    synchronizer.sync(todo) // ...(3) 
  }
}

domain層で定義されているTodoというドメインモデルを利用して、Todoを新しく作っています(1)。新しく作られたTodoを、アプリケーションの状態を保持する役目を持つrepositoryに保存しています(2)。参照系を構成するread stackの説明をするときに詳述しますが、read stackでこのrepositoryに保存されている状態を読み取ることで、UI側はアプリケーションの論理的な状態をUIとして描画する形です。

また、今回のアプリケーションでは「新しいTodoを作ったらなるべくすぐにサーバーに同期する」という仕様にしてあるので、「同期してくれる君」であるsynchronizerにtodoの同期を頼んで(3)、このクラスの責務はおしまいです。

このとき、サービスは「新しく作られたTodoがどのような状態やプロパティを持っているのか」や「repositoryはTodoをどこに保存しているのか」や「synchronizerはどうやってtodoをサーバーと同期しているのか」の詳細については一切意識していないことに気をつけてください。

次回詳述しますが、「新しくTodoが作られたときにそのTodoはどのような状態なのか」とか、そういうのはdomainパッケージに記述していきます。

このあたりの話の具体例をもう一つ出しましょう。「todoをdone状態にする」というユースケースを実現するusecase.make_todo_done.Service

trait Service {
  protected val repository: TodoRepository
  protected val synchronizer: TodoSynchronizer

  def execute(id: Int) = {
    repository.find(id).foreach {oldTodo =>
      val doneTodo = oldTodo.makeDone
      repository.store(doneTodo)
      synchronizer.sync(doneTodo)
    }
  }
}

repositoryから該当のtodoを引っ張ってきて、そのtodoに対してmakeDoneというメソッドを呼び出して「doneになったTodo」をrepositoryに保存して、同期してるだけです。「todoにどういうプロパティがあるのか」とか一切意識していません。

次回予告

次回は、usecaseパッケージが利用しているdomainパッケージとinfrastructureパッケージののうち、domainパッケージが持つ責務を具体的に見ていく予定です。

NDS(長岡開発者勉強会)の52回で「怖くないし役に立つ設計原則の話」というタイトルで喋ります

新潟県長岡市では定期的に「NDS」という勉強会が開催されています。わたしも新潟に住んでいるときには非常にお世話になった勉強会で、とても良い勉強会です。

その第52回(52回ですよ!?すごくないですか!?)が6/17に開催されるとのことなので、「怖くないし役に立つ設計原則の話」というタイトルで発表させていただくことにしました。

nagaoka.techtalk.jp

まだ内容固まっていないのですが、このトークでは、

  • デザインパターンとか知識としては知ってるんだけどなぜそれが有効なのかよくわからん
  • 「結局設計パターンとかって役に立たないし勘でやるものでしょ」
  • MVCとかMVVMとかMVPとか「わざわざ難しくしてる」としか思えない!!!
  • DRY原則なら知ってる

といった感じのひと(2010年くらいの俺だ……)を想定して、

  • われわれは(主語が大きい)なぜ設計パターンを役立たせることができないのか
  • 設計パターンには抽象度の高いものから低いものまで様々なものがある
  • SOLID原則を例にとって「役に立たせ方」を実践する

といった内容を発表したいと思っています。チャレンジング!どこまで成功するだろうか。

長岡にはおいしいお酒もおいしい料理もあるので、みなさんもぜひこの機会にNDSに参加してみてはいかがでしょうか。お待ちしております。

オフラインファースト的なGUIアプリケーションをScala.jsで書く話 / Vue.jsによるUI層とScala.jsによるモデル層のコミュニケーション

前回の記事では、Scala.jsをどのように利用したかについて概観を見てきました。

前回、UI層は素直にVue.jsの単一ファイルコンポーネントで書いて、モデル層はScala.jsで書く、というスタイルを取る、と述べましたが、今回はモデル層がどのようなインターフェイスをUI層に公開して、UI層がどのようにそれらを利用する設計としたのかについて見ていきます。

モデル層の公開するみっつの種類のクラス/オブジェクト

モデル層は、UI層に対して、「コマンド」「クエリ」「イベント」のみっつの種類のクラス/オブジェクトを公開します。まずは「コマンド」から見ていきましょう。

コマンドの責務

コマンドは、アプリケーションの状態を更新するような操作(たとえばTodoを追加する、Todoをdone状態にする、など)の窓口を提供します。

なにかを更新するような操作を行う場合、UI層は、

  • コマンドをインスタンス化し
  • コマンドを組み立てて
  • コマンドを実行

します。また、コマンドクラスは「このコマンドはvalidか?」を判別する責務も持ちます。これがUI側からどのように見えるのかがよくわかるのがTodoInput.vueです。JS部分を引用してみます。

    import {AddTodoCommand} from '../../../target/scala-2.12/scalajstodo-fastopt' //...(1)
    export default {
        beforeCreate(){
            this.addTodoCommand = new AddTodoCommand; //...(2)
        },
        data(){
            return {
                todoInput: "",
                dueDateInput: "",
                isTodoInputValid: true,
                isDueDateInputValid: true
            };
        },
        watch: {
            todoInput(v){ // ...(3)
                this.addTodoCommand.todoInput = v;  //...(4)
                this.isTodoInputValid = this.addTodoCommand.isTodoInputValid; //...(5)
            },
            dueDateInput(v){
                this.addTodoCommand.dueDateInput = v;
                this.isDueDateInputValid  = this.addTodoCommand.isDueDateInputValid;
            }
        },
        methods:{
            addTodo(){ //...(6)
                this.addTodoCommand.execute(); 
            }
        }
    }

まずは(1)の部分で、Scala.jsから公開されているAddTodoCommandというクラスをimportして、(2)でコマンドをインスタンス化しています。

フォームのinputに変化が起こると、データバインドされたtodoInputに変化が起こり、それをwatchしている関数(3)が呼び出されるので、その中でコマンドの値をセットています(4)。さらに、コマンドクラスは「この値がvalidな値かどうか」を知っているので、「この値はvalidですか?」というのをコマンドクラスにたずねて、その結果をUI側に書き戻しています(5)

フォームがsubmitされると、addTodoというメソッドが呼び出されます(6)。この中では、組み立て終わったコマンドを実際にexecuteしているだけです。この際、コマンドは投げっぱなしになっていることに注意してください。

コマンドクラスは「アプリケーションの状態を更新する」ための窓口であり、「更新されたあとの状態を取得する」ための窓口ではありません。「更新結果の取得」にはQueryを利用します。

クエリの責務

UI上にアプリケーションの現在の状態を描画するためには、現在の状態をなんとかして取得しなければなりません。その際に利用するのが、Queryです。これもUI側からの利用例を見ましょう。TodoList.vueです

    import {
        TodoQuery,
        /* 中略 */
    } from '../../../target/scala-2.12/scalajstodo-fastopt'
    
    export default {
        beforeCreate(){
            this.todoQuery= new TodoQuery; //...(1)
        },

        /* 中略 */

        data(){
            return {
                todos: this.todoQuery.all() //...(2)
            };
        }
    }

(1)インスタンス化したqueryを利用して、(2)でアプリケーションの状態(今回ならば「現在のtodo一覧」)を取得して、databind用の変数に書き戻しています。

ところで、このqueryは「いつ」実行すべきでしょうか。(2)の部分では、まず、UIの構築時に「初期データ」としてアプリケーションの状態を読み出しています。

しかし、よく考えるとそれだけではなくて、「アプリケーションの状態に変化があったとき(つまり今回ならtodoのリストに変化があったとき」にも、状態を読み出してUIを更新しなければなりません。そこで必要になるのが「イベント」です。

イベントの責務

イベントは、モデル側でなにか状態に変更が起こったのをUI側に伝えるためのオブジェクトです。これも例を見るのが一番でしょう。クエリと同じく、TodoList.vueです。

    import {
        /* 中略 */
        TodoRepositoryChanged //...(1)
    } from '../../../target/scala-2.12/scalajstodo-fastopt'
    export default {
        /* 中略 */
        created(){
            // (2)
            this.subscription = TodoRepositoryChanged.subscribe(() => {
                this.todos = this.todoQuery.all();
                console.log(this.todos);
            });
        },
        beforeDestroy(){
            this.subscription.unsubscribe();
        },
        /* 中略 */
    }

TodoRepositoryChangedというイベントをScala.js側からimportしてきています(1)。さらに、(2)でそのイベントが発火したらtodoを読み込み直すようにしています。UIがdestroyされたときにきちんとunsbscribeするのも忘れないようにしましょう(3)

UI層とモデル層のコミュニケーションまとめ

以上をまとめると、以下のようなフローでUI層とモデル層はコミュニケーションすることになります。

まず、初期データをQueryを通じて読み込みし、UI側でレンダリングします。

f:id:nkgt_chkonk:20170522150803p:plain

状態の更新を伴うような操作は、Commandを組み立てて実行します。

f:id:nkgt_chkonk:20170522150924p:plain

Commandの結果としてアプリケーションの状態が変化し、Eventが通知されます

f:id:nkgt_chkonk:20170522151017p:plain

Eventに応じて、最新の状態を読み出し、表示します。

f:id:nkgt_chkonk:20170522151058p:plain

このように、モデル層では「更新の窓口」と「読み出しの窓口」と「状態通知の窓口」のみっつを設けて、UI層ではそれらを適切にimportし利用することで、「UIの定義」と「アプリケーションのロジック」を分離することができます。いわゆる「プレゼンテーションとドメインの分離」ってやつですね。

次回予告

次回は、オフラインファーストなアプリケーションにおいて、モデル層をどのように設計していったかについて書きます。

オフラインファースト的なGUIアプリケーションをScala.jsで書く話 / Scala.jsについて

はじめに

オフラインファーストへの要求

近年、オフラインファーストというか、「オフラインのときにも普通につかえて、オンラインになったら同期する」みたいなことに対する要求が高まっているように感じます。

その場合は、ローカルにもきちんと永続データを持っておき、オンラインのときにバックエンドと通信をしながらバックエンドのデータと同期していく、というスタイルを考えるのが自然だと思います。

また、普通のJSアプリケーションであっても、「サーバーに投げる際に失敗したデータはローカルでメモリ上にもっておいてリトライしたい」などの要求もあるでしょう。

さらに、ここでモバイルアプリも視野に入っているとなると、どうしても「オッRealm Mobile Platformか!?」という感じが出てきますが、Realm Mobile Platformにロックインされるのと引き換えに開発の速を選ぶのか、それとも、というのは判断の別れるところでしょう。

そこで、一旦オフラインファーストなGUIアプリケーションをどのように設計するべきかの素振りを、ブラウザプラットフォーム上でしてみることにしました。これはわたしが単に慣れているプラットフォームだから、という理由です。

複雑なロジックは表現力の高い言語で書きたいという要求

オフラインファーストなアプリケーションを書いていく場合、「いつ同期するのか」「同期のストラテジはどうするのか」「同期中のデータはどう管理するのか」「同期に失敗した場合は?」「サーバーから降ってくる同期用データをどうハンドリングするか」など、かなり複雑なことをモデル層以下でハンドリングしなければならないということが予想されます。

また、そもそもモデル層はなるべくリッチで設計上の制約がない言語で書いて、ドメインの設計やアプリケーションの設計に集中したいという要求があります。

今回は、「どうせ素振りなのだから」ということでScala.jsに手を染めてみました。ちなみにリポジトリShinpeim/Scala.jsTodoExampleにあります。

本記事では、この素振りによって得られた知見をまとめます。全3,4回くらいになる予定です。

初回はScala.jsについてです。

Scala.jsについて

JSとの協調、懸念点

Scala.jsを使ってみるにあたって

  • JSのライブラリ資産ってScala.js側からは使えるの?
  • Scalaのライブラリ資産ってそのまま使えるの?

というところがまず懸念点として上がります。また、やはりJSが得意とする部分(たとえばVue.jsのシングルファイルコンポーネントでUIを組み立てていくこととか、Routingとか、そういうブラウザと密結合している部分)についてはJSで書けたほうが余分なハマりが少なそうという感じもあります。

そこで、今回は

  • index.js(エントリポイント)とUI定義であるVue.jsのシングルファイルコンポーネント(*.vueってやつ)だけJSで書く
  • そのほかはすべてScala.jsで書き、sbtがwatchしてJSにコンパイル
  • webpackがそのファイルもwatchして、がっちゃんする

というスタイルを取ることにしました。

このスタイルの懸念点としては、

  • Scala.jsが吐くファイルサイズがでかくなるけどそれをwebpackでがっちゃんって実用に耐えるの?
  • そもそもScala.jsで吐いたJSってES6 importとかCommonJS requireで扱えるの?

というあたりがあるので、そのあたりの検証も行いました。

以下、その懸念点に対する回答です。

JSのライブラリ資産ってScala側からは使えるの?

結論から言うと、使おうと思えば使えますが、「そのまま」は使えません。

  • すでにJSライブラリをScala.jsで動かすためのファサードが提供されている場合はそれ使う
  • 自分でファサード書く
  • 型検査なしにScala.js側からがんばってJSのオブジェクトなどを操作して使う

というみっつの選択肢があります。

詳しくは公式のドキュメント - JavaScript libraries for Scala.jsを参照してください。

ただ、今回はJSのほうが得意な部分は素直にJSで書く、というスタイルを選択しているため、jQueryだとかVue.jsだとかをScala.js側から触る必要はないので、JSのライブラリ資産をScala側から利用するシーンはありませんでした。

Scalaのライブラリってそのまま使える?

結論から言うと、「Scala.jsコンパチなライブラリならそのまま使える」ということのようです。

公式のドキュメント Compatible Scala librariesを御覧ください。ScalazとかCatsとかShaplessとかあって強い……

joda.timeを利用しようとしたらうまくfastOpt(最後にjs吐く部分の操作だと思って)できなくて、「どういうライブラリなら"Scala.jsコンパチ"って言えるんだ?」ってことはよくわかってなくて、まだ調べきれてません。

Scala.jsが吐くファイルサイズがでかくなるけどそれをwebpackでがっちゃんって実用に耐えるの?

  • でかいとwebpackがbundle.js吐くのに時間かかって開発のサイクルが遅くなったりしない?
    • 開発時はminifyなどをスキップする、Scala.jsが吐くjsファイルはバベったりローダー噛ませたりしないでそのままimportすると、現実的な速度でサイクル回せるなあという感じがする
  • で、最終的なサイズはどうなの、実用できそう?
    • 今回のリポジトリの規模で、minifyなしで4.08MB、minifyありで2.16MB。でかいライブラリに依存したりするとかなり厳しいかなという気がするが、まだちょっと未知数という感じがする。

そもそもScala.jsで吐いたJSってES6 importとかCommonJS requireで扱えるの?

扱える。see Export Scala.js APIs to JavaScript - Exports with modules

Scala.jsの書き味はどうだった?

最高の体験だった……。静的型付けによるIDEの賢いリファクタ……、堅牢で柔軟な言語仕様……、なんか矛盾があればsbtさんが怒ってくれる……、traitを利用したDI……。ほしいものがここにあった……ここは地上の楽園か……?

また、Scalaが表現力の高い言語なので、結果として設計やロジックに集中することができて、JSでがんばって書きながら設計してたときよりもより良い設計が導けた気がする。

次回予告というか今後語られる予定の話

  • Vue.jsのシングルファイルコンポーネントの世界とScala.jsの世界をつなぐ窓口をどのように設計したか(それはつまりプレゼンテーション層とドメイン層をつなぐ窓口の設計である)について
  • オフラインファーストを視野に入れた際のドメイン層の設計について

-> 書きました

オフラインファースト的なGUIアプリケーションをScala.jsで書く話 / Vue.jsによるUI層とScala.jsによるモデル層のコミュニケーション - 猫型の蓄音機は 1 分間に 45 回にゃあと鳴く

vue-routerのafterEachグローバルフックがhookされるタイミングとonReadyがhookされるタイミング

vue-routerにはonReadyというフックが用意されている

ドキュメントを読めばわかるように、これは when the router has completed the initial navigation, which means it has resolved all async enter hooks and async components that are associated with the initial route. 、つまり最初に表示するルートに紐付けられたbeforeEnter ナビゲーションガードやbeforeEachglobalナビゲーションガードが解決され、「どのrouteが実行されるべきか」が解決されたあとに呼ばれる。

これが便利になるのはSSRのときで、preloadしたいデータがあるような場合はbeforeEnter内で非同期読み込みし、その読み込みが終わったらnextするようにしておき、onReadyのタイミングでSSRすればよい、というような使いかたができるわけだ。

ところで、ナビゲーションガードが解決されたあとに呼ばれるhookはもうひとつある。それがafterEachグローバルhookだ。

このafterEachというhookとonReadyhookはどちらが先に呼び出されるのだろう。ドキュメントを読んでもよくわからなかったので、ソースを追った。

まず、onReadyで登録したhookを実際に呼び出しているのはここである。

  transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    const route = this.router.match(location, this.current)
    this.confirmTransition(route, () => {
      this.updateRoute(route)
      onComplete && onComplete(route)
      this.ensureURL()

      // fire ready cbs once
      if (!this.ready) {
        this.ready = true
        this.readyCbs.forEach(cb => { cb(route) })  // ココね!!!ここ!!!
      }
    }, err => {
      if (onAbort) {
        onAbort(err)
      }
      if (err && !this.ready) {
        this.ready = true
        this.readyErrorCbs.forEach(cb => { cb(err) })
      }
    })
  }

で、その前にコールされてるthis.updateRoute(route)の中身見ると

  updateRoute (route: Route) {
    const prev = this.current
    this.current = route
    this.cb && this.cb(route)
    this.router.afterHooks.forEach(hook => {
      hook && hook(route, prev)
    })
  }

ここでafterHooksを呼んでいる。というわけで、onReadyよりも先にafterEatchのhookが呼ばれるようになっていることが確認できた。

注意すべき点として、(あたりまえだが)afterEachは同期的に呼び出されているので、その中で非同期な操作を行った場合、その非同期操作の完了を待たずにonReadyhookが呼び出されるので、onReadyにhookしてSSRしても、afterEach内で行った非同期操作の結果はSSRされない。

VuexとPDS、immutable modelの所感

Twitterに書いた内容再掲。

最近Vuexについて考えてるのは、Vuex使ってPDS実現するとなると、状態はVuexのレイヤーで持つことになるから、モデル層はステートレスで作るのが相性良い(というか自然とそうなる)だろうな

その時にJSでイミュータブルに寄せて行く辛さがどれくらいキツいものなのかの知見が自分にはない

ある程度以上に複雑なアプリケーションでは、サーバーサイドからpushされる情報のためにコネクション握りっぱなしにする部分とかも出てくるであろうと思っていて、モデル層以下をイミュータブルに寄せて行った時にそういうのをどうハンドルすれば良いかも自分に知見がない

Vuex少し触ったところとても筋が良いと感じるんだけど、そのあたりみんなどうしているのか知りたいなあ

まあある程度の複雑さまでなら「Vuexの層に全部書く」でよいからそれでええやん、というのはありそう

イミュータブルモデルの良いところはテスタビリティがぐんぐんあがるよ!ってところだと思っていて、そのメリットは捨てがたいので知見貯めていきたい

画面ごとの複雑さが異なるアプリケーションにどう立ち向かうか

ツイッターで書いた内容です

SPAやってると画面ごとに複雑さが全然ちがって、単なる一覧表示してる画面の複雑さといろいろな操作ができる画面の複雑さは雲泥の差になる

こういうときに、「この画面はきちんとドメイン層作ったほうが見通しがよくなるけど、この画面もそうしちゃうとオーバーキルなんだよな〜」ということがある

そういうときにアプリケーション内で一貫したアーキテクチャでやっていくのか、この画面はXの層をスキップしますとするのか、こーたえーはーなーい(ABSTRACT TRUTH / NUMBERGIRL

ディレクトリ構成、名前空間構成に貴様の 意思 を込めろ」という煽りを思いついたので自由に使ってください

ひとつのアプリケーションの中に複数の画面があり、画面ごとに複雑さが異なるような場合、やはり画面ごとにアーキテクチャを変えるというのが正解な気がしてきた。その場合、たとえばパッケージ構造は次のようにする

- presentation
  - A
  - B
- model
  - transaction
    - A transaction
  - layered
    - usecase
      - B usecase
    - domain
    - infrastructure

その上で、domainとinfrastructureの下は画面構造に依存しない分割構造でパッケージを分ける

どうか。

しかし、これを実践するためには、意図をきちんと説明したら理解してくれるメンバーで開発を行う必要があり、弊社なら問題ないな