0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

JavaScriptでクリーンアーキテクチャはどうすればいいのか(initializer編)

Last updated at Posted at 2018-09-21

#initializerとは

エントリーポイントが保持すべきは機能は次のものだ:

  • Initializer ― 必要なオブジェクトのインスタンスを生成する。UsecaseにInterface Adapterを渡しつつ生成することで、依存性を注入するDIコンテナとして働く。それ以外の初期化はしない。オブジェクト指向でない言語なら要らないかもしれない。
  • Starter ― Initializerがオブジェクトを生成した後に、能動的にユースケースを開始する。他のオブジェクトの準備を整えてユースケースを開始する場合に必要となる。例:websocketサーバとの接続

エントリーポイントの周りでは最低限のブート処理だけするべきで、あとは各モジュールに任せるべきだ。


なぜ今回initializerか?

イベントハンドラの問題(再掲)

  • セレクトボックス中のリストは効率的に変えられるようになったけど、変更されたかどうかのイベントが取れない

    • 今のままではイベントハンドラがNodeListオブジェクトに付いてないから
    • idに紐付いたNodeListオブジェクトにonchangeも与えないといけない
    • それ相当のメソッドを追加する必要がある
    • それこそがPresenterの本領発揮するところであった
  • イベントハンドラは生成箇所によって、参照する変数が意図しないものになっているせいで容易にエラーとなる

  • イベントハンドラ内で変数参照に問題が出ないように、生成する記述箇所を検討する必要がある

  • => initialierシナリオの検討課題は、どこで何を生成すべきか


我が勤務実績一覧アプリでは

  • Repositoryは会社固有の定数のため引数あり
    • 会計期の開始日/月次の締日/休日/残業計算、等
  • Presenterは特に何も, Usecaseのためだけ
  • Usecaseは一つなので迷わない
  • ContollerからUsecase#initialize()を呼ぶことに
    • Viewを構築
    • Viewに初期値を
    • 準備したViewをPresenterでViewControllerに
  • 依存関係の逆転は次回
window.addEventListener("DOMContentLoaded", function(evt){

  const repository   = new LaborRepository(COMPANY);
  const presenter    = new Presenter();
  const filterUC     = new filterUsecase(repository, presenter);
  const viewController = new Controller(filterUC);
  
  viewController.onload(evt);
})

class Controller {
  constructor(usecase){
    this.oport = usecase
  }
  onload(evt){
    this.oport.initialize()
  }
}

class FilterUsecase {
  constructor(repository, presenter){
    this.store = repository
    this.oport = {}
  }
  initialize(){
    this.oport.departmentfilter = Presenter.create("select", "departments", "departmentfilter")
    this.oport.labortypefilter = Presenter.create("select", "labortypes", "labortypefilter")
    this.oport.memberfilter  = Presenter.create("select", "members", "memberfilter")
    // これで画面上にセレクトボックスが空の状態で出現する

    this.cond = {}
    const 勤務実績表 = this.repository.q("勤務実績")
    const departments = this.filterByDepartment(勤務実績表)
    const labotypes = this.filterByLabortype(勤務実績表)
    const members   = this.filterByMember(勤務実績表)
    this.oport.departmentfilter.update(departsments);
    this.oport.labortypefilter.update(labortypes);
    this.oport.memberfilter.update(members);
    // これで画面上のセレクトボックスには初期値が入った
  }
  filterByDepartment(table){//Usecase中ではpresenterが受け取れる配列データ構造に変換
    //この場合はTable -> Arrayへ; "所属"の列だけを抜き出して配列へ
    //Table: {tbody: [{"氏名": "Aさん", "個別月": "2018/09", ...}, {"氏名", "Bさん"}, ...]} 
    //Array: ["所属", "A部", "B部", ...]
    return table.select("所属").uniq()
      .left_join(this.repository.from("所属部門").sortby("表示順")
      .select("所属").toArray()   
  }
  ...
}

#準備したViewをPresenterでViewControllerに
##準備したViewをPresenterでViewControllerに
###準備したViewをPresenterでViewControllerに

  • 大事なので三回言いました。

#Controller = イベントハンドラを持つオブジェクト

  • ViewController = マンマシンインターフェースに関するイベントハンドラを持つ
    • onclick, onmouseover, ...
    • だからUI部品にで使われるよ
  • 人間と関わらないイベントハンドラって?
    • onloadとか、ontimeoutとか
  • NodeListはViewなのViewControllerなのControllerなの?
    • イベントハンドラ完全未設定ならView
    • されたらViewControllerとして動く
    • (私調べ)

#ViewをViewControllerにする

  • 二箇所で生成が可能。どちらが適切か?
  1. Usecase->Presenter->View
  • Usecase中でイベントハンドラを定義して、Presenterに依頼
  1. Controller->Usecase->Presenter
  • Controller中でイベントハンドラを定義してUsecaseに任す

Usecase->Presenter->View

  • Usecase中で定義
class FilterUsecase {
  initialize(){
    const component = {
       type: "select",
       name: "departments",
    id: "departmentfilter",
       onchange: function(evt){
         const i = evt.target.selectedIndex;
         const department = evt.target.options[i].text
         this.changeDepartment(i, department)
       }//このthisはFilterUsecase中だから、
        // 以下のchangeMemberを呼び出せる
    }
    const deptPresenter = Presenter.create(component.type, component.name, component.id)
    deptPresenter.addEventListener("change", component.onchange.bind(this));
    this.oport[component.id] = deptPresenter
  }

  changeDepartment(i, dept){
    this.cond = {department: (i < 1) ? null : dept}
    const "勤務実績表" = this.repository.q("勤務実績", this.cond)
    const members  = this.filterMember("勤務実績表")
    this.oport.memberfilter.update(members)
  }
}
class Presenter {
  addEventListener(kindof, handler){ 
    this.viewController.addEventListener(kindof, handler)
  }
}

#Controller->Usecase->Presenter

  • Controller中で定義
    • viewControllerたちをまとめて引き渡す
    • usecase変数の値が有効(こちらの方が自然?)
class Controller {
  constructor(usecase){
    this.oport = usecase;
    this.viewControllers = {
      departmentfilter: {
        type: "select",
        name: "departments",
        onchange : function(evt){
          var sender = evt.target
          var i = sender.selectedIndex;
          var department = sender.options[i].text;
          usecase.changeDepartment(i, department);
        }
      },{
      labortypefilter: {
      }, ...
    }
  }

  onload(evt){ //viewControllerたちをまとめて引き渡す                                                                                                             
    this.oport.initialize(this.viewControllers)                                                                                       
  }                                                                                                                           
}

class FilterUsecase {
  initialize(viewControllers){
    for( var {id: component} of viewControllers){
      const presenter = Presenter.create(component.type, component.name, id);
      presenter.addEventListener("change", component.onchange);
      this.oport[id] = presenter
    }
  }

  changeDepartment(i, dept){
    this.cond = {department: (i < 1) ? null : dept}
    const "勤務実績表" = this.repository.q("勤務実績", this.cond)
    const members  = this.filterMember("勤務実績表")
    this.oport.memberfilter.update(members)
  }

}
class Presenter {
  addEventListener(kindof, handler){ 
    this.viewController.addEventListener(kindof, handler)
  }
}

まとめ

  • initializerでは、
    1. Viewを構築
    2. Viewに初期値を
    3. ViewをPresenterでViewControllerに
  • ViewContollerの定義は、Controller上でやるとスマートに感じる

余談

  • こうしておくとイベントハンドラ内で、Componentクラスのメソッドも使えるようになる(はず)
  • 動作未確認
  • Vue.jsっぽい(使ったことないけど)
    this.viewControllers = {
      departmentfilter: new Component({
        type: "select",
        name: "departments",
        data: null,
        methods: {
          onchange : function(evt){
            var sender = evt.target
            var i = sender.selectedIndex;
            var department = sender.options[i].text;
            usecase.changeDepartment(i, department);
          }
        }
      })
    }

Repository編へ続く

全記事

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?