LoginSignup
1

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-09-21
1 / 6

Presenterとは?

  • View=Webページ(html)一歩手前の「Interface Adapter」
    • Viewを動的に書き換える 役割
    • つまり、引数の入力値を最終的にDOMへ
  • Viewへの操作は基本、idに紐付いたNodeListオブジェクト
    • idさえあれば、プログラム中どこからでもViewへ
    • ある意味、従来のMVC構造より自由
    • むしろ自由だからルール通り書かないと泥沼
  • => idを与えてインスタンス化、dataを与えて更新すればいいのだろうか?

idを与えてインスタンス化、dataを与えて更新

  • idごとにPresenterがあるのが適切か(粒度)
    • htmlで静的に決まるid(今回はこっち)
    • データから動的にプログラムで生成されるid
    • 二つの場合がありそう
  • Presenterは、
    • idを与えてインスタンス化し永続化する方向性
    • idを引数で与えて都度、生成/消滅という方向性
    • 二つの場合がありそう
    • 今回はインスタンス化の方向性で検討する
  • 与えるデータをどのUI部品に紐付けるか
    • type: select, radio, checkbox, ..., table, chart, ... etc.
    • typeに応じて、生成と更新のDOMは 異なる

生成と更新に対して、メソッド名を共通するクラスとする

めんどくさい

  • Javascriptのような動的な言語の特徴を抹殺するような展開
class Presenter {
  constructor(id){
    this.oport = document.getElementById(id);
  }
}
class AttachPresenter extends Presenter {
  adapt(type){ 
    if (type == "select")
      return document.createElement("select");
    if (type == "radio")
      return null
    if (type == "checkbox")
      return null
    ...
  }
  update(type){
    const dom = this.adapt(type)
    if (dom != null)
      this.oport.appendChild(dom)
  }
}
class UpdatePresenter extends Presenter {
  adapt(data, type){
   if (type)
    return ... //具象クラスで実装
  }
  update(data, type){
    // 1. 元ノードの、要素だけ消す
    const dom  = this.adapt(data)
    const targetNode = this.oport.children[0];
    while(targetNode.childNodes.length > 0)
      targetNode.removeChild(targetNode.firstChild);
    // 2. dataに応じた要素だけ再度加える
    while(dom.children.length>0){
      targetNode.insertBefore(dom.lastElementChild, targetNode.firstElementChild);
    }
  }
}

うまくないと感じる要因

  • インスタンスが一個のクラスとか、いちいちかきとうないわ
  • 同一のidを持つViewに対するコードが別クラスで分断している
    • 引数に常にtypeを与えて条件分岐しないといけない
    • =>ナンセンス
  • 分断を回避するには?=>先にtypeを与えておく
const departmentView   = new ViewBuilder(type)
departmentView.attach("departmentfilter")
departmentView.update(initialData)
departmentView.update(data)
  • こうだ。

PresenterがやろうとしていたことをViewに寄せてみる

Viewがidを持つ!

  • こうやろ!
class ViewBuilder {
  constructor(type){
    this.views = {
      "select" : SelectView,
      "radio"  : RadioView,
       ...     
    }  
    return this.views[type]
  }
  addViewBuilder(id, klass){
     this.view[id] = klass
  }
}
class SelectView {
  constructor(name){
    this.node = document.createElement("select", "")
    this.node.setAttribute("name", name)
  }
  attach(id){
    this.id = id   
    this.oport = document.getElementById(id)
    this.oport.appendChild(this.node)
    return this
  }
  adapt(data){
    return data.map(function(v){
      const option = document.createElement("option")
      option.appendChild(document.createTextNode(v))
      return option
    }
  }
  update(data){
    const dom = this.adapt(data)
    // 1. 元ノードの、要素だけ消す
    const targetNode = this.oport.children[0]
    while(targetNode.childNodes.length > 0)
      targetNode.removeChild(targetNode.firstChild)
    // 2. dataに応じた要素だけ再度加える
    while(dom.length>0){
      targetNode.insertBefore(dom.pop, targetNode.firstElementChild)
    }    
  }
}

PresenterはViewを持つ

  • 中身が空になってしまった、、、
class Presenter {
  constructor(view){
    this[view.id] = {oport: view}
  }
  update(id, data){
    this[id].oport.update(data)
  }
}
  • これでは存在意味がないので、Viewの名前をPresenterに置き換える
  • もともとPresenterはDOM操作するのが役割のつもりであった

View->Presenterに置き換え

class Presenter { 
  constructor(text){
    this.viewController = (text == null) ? null : document.createTextNode(text)
  }
  static create(type, name, id){ 
    const presenter = PresenterBuilder(type);
    return (new presenter(name)).attach(id)
  }
  attach(id){ // 巻き上げ
    this.id = id
    this.oport = document.getElementById(id)
    this.oport.appendChild(this.viewController)
    return this
  adapt(data){
    return [document.createTextNode(data)];
  }
  update(data){// 巻き上げ
    const dom = this.adapt(data);
    // 1. 元ノードの、要素だけ消す
    const targetNode = this.oport.children[0];
    while(targetNode.childNodes.length > 0)
      targetNode.removeChild(targetNode.firstChild);
    // 2. dataに応じた要素だけ再度加える
    while(dom.length>0){
      targetNode.insertBefore(dom.pop, targetNode.firstElementChild);
    }    
  }
}

class SelectPresenter {
  constructor(name){
    this.viewController = document.createElement("select", "")
    this.viewController.setAttribute("name", name)
  }
  adapt(data){
    return data.map(function(v){
      const option = document.createElement("option")
      option.appendChild(document.createTextNode(v))
      return option
    }
  }
}

class RadioPresenter {  
  constructor(name){ //生成用
    ...
  }
  adapt(data){ //更新用
    ...
  }
}

class CheckboxPresenter { //同上
  ... 
}

class PresenterBuilder {
  constructor(type){
    this.presenters = {
      "select" : SelectPresenter,
      "radio"  : RadioPresenter,
      ""
    }  
    return this.presenters[type]
  }
  addPresenterBuilder(id, klass){
     this.presenters[id] = klass
  }
}

よしうまく整理できた!

まとめ

  • PresenterはView一歩手前の「Interface Adapter」
    • Viewを動的に書き換える 役割
    • つまり、引数の入力値を最終的にDOM(NodeListオブジェクト)へ
  • Viewとはidに紐付いたNodeListオブジェクト
    • idさえあれば、プログラム中どこからでもView(ViewControllerとも)へ
    • ある意味、従来のMVC構造より自由
    • むしろ自由だからルール通り書かないと泥沼
  • => idを与えてインスタンス化、dataを与えて更新
  • typeを与えてインスタンス化しNodeListオブジェクトを生成, idを与えてViewと為す
    • (再掲)Viewとはidに紐付いたNodeListオブジェクト
  • Usecase->Presenter->Viewの流れ
    • Presenterが切り替え可能に
    • UI部品例えば、ラジオボタンやチェックボックスに変更が容易に

しかし、重大な課題が残る

  • セレクトボックス中のリストは効率的に変えられるようになったけど、変更されたかどうかのイベントが取れない
    • 今のままではイベントハンドラがNodeListオブジェクトに付いてないから
    • idに紐付いたNodeListオブジェクトにonchangeも与えないといけない
    • それ相当のメソッドを追加する必要がある
    • それこそがPresenterの本領発揮するところであった

initializer編へ続く

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
1