🧊

JavaScriptの実装で、最近悩んでるコト

たとえば、こういう要件があったとします。

  • 複数の選択肢から、nコを選んで実行する
  • nコ以上選んだ場合は、後から選んだものを優先する
  • リセットボタンがある
  • 決定ボタンを押すと、選んだ内容が表示される

みたいな。

で、こういうのを実装するにあたり、どういう実装にしようかなーと思うわけです。

とりあえず画面をつくる

以下から2つ選んでね
<ul>
  <li class="js-choice">くう</li>
  <li class="js-choice">ねる</li>
  <li class="js-choice">あそぶ</li>
</ul>

<div>
  <button class="js-reset">選びなおす</button>
  <button class="js-submit">決定</button>
</div>

<div>やりたいこと:<span class="js-result">ここに2つ</span></div>

こんな感じ。

今回は、とくにスタイルとかは考えずにやっていきます。

実装パターンA

上から順にただ処理を書いていったパターン。

(function(global) {
  'use strict';

  // Private functions.
  var klass = function(c) { return global.document.getElementsByClassName(c); };

  // Data.
  var myChoices = [],
      MAX_CHOICES = 2;

  // Dom elements.
  var choices = klass('js-choice'),
      submitBtn = klass('js-submit')[0],
      resetBtn = klass('js-reset')[0],
      result = klass('js-result')[0];

  // Handlers.
  var save = function(e) {
    var t = e.target.textContent;
    myChoices.push(t);
    (myChoices.length > MAX_CHOICES) && myChoices.shift();
  },

  submit = function() {
    result.textContent = myChoices.join(',');
  },

  reset = function() {
    myChoices = [];
    result.textContent = '';
  };

  // Event handlers.
  submitBtn.addEventListener('click', submit, false);
  resetBtn.addEventListener('click', reset, false);
  
  Array.prototype.forEach.apply(choices, [function(e) {
    e.addEventListener('click', save, false);
  }]);

}(this.self || global))

このパターンのいいところ

  • とにかくコードが短い
  • 要件が小さいだけあって、サクッと書ける
  • 見たらわかるので、知識のない人にも引き継ぎやすい

このパターンのわるいところ

  • 仕様変更によわい

実装パターンB

どこぞのライブラリにインスパイアされた、なんちゃってMVCパターン。

(function(global) {
  'use strict';

  // Private functions.
  var klass = function(c) { return global.document.getElementsByClassName(c); };

  // Data.
  var myChoicesCollection = function(c) { this.init(c); };
  myChoicesCollection.prototype = {
    init: function() {
      this.data = [];
      this.MAX_CHOICES = 2;
    },
    
    push: function(d) {
      this.data.push(d);
      (this.data.length > this.MAX_CHOICES) && this.data.shift();
    },
    
    get: function() {
      return this.data.join(',');
    },
    
    reset: function() {
      this.data = [];
    }
  };

  // Views
  var ChoiceView = function(c) { this.init(c); };
  ChoiceView.prototype = {
    init: function(c) {
      this.c = c;
      this.el = klass('js-choice');
      this.bindEvents();
    },

    bindEvents: function() {
      var that = this;
      Array.prototype.forEach.apply(that.el, [function(e) {
        e.addEventListener('click', that.pushOne.bind(that), false);
      }]);
    },
    
    pushOne: function(e) {
      var that = this;
      that.c.myChoicesCollection.push(e.target.textContent);
    }
  };

  var ResultView = function(c) { this.init(c); };
  ResultView.prototype = {
    init: function(c) {
      this.c = c;
      this.el = klass('js-result')[0];
    },

    render: function() {
      this.el.textContent = this.c.myChoicesCollection.get();
    }
  };

  var BtnView = function(c) { this.init(c); };
  BtnView.prototype = {
    init: function(c) {
      this.c = c;
      this.el = null;
      this.submitBtn = klass('js-submit')[0];
      this.resetBtn = klass('js-reset')[0];
      this.bindEvents();
    },

    bindEvents: function() {
      var that = this;

      that.submitBtn.addEventListener('click', that.submit.bind(that), false);
      that.resetBtn.addEventListener('click', that.reset.bind(that), false);
    },
    
    submit: function() {
      this.c.resultView.render();
    },
    
    reset: function() {
      this.c.myChoicesCollection.reset();
      this.c.resultView.render();
    }
  };

  // Main app.
  var App = function() { this.init(); };
  App.prototype = {
    init: function() {
      this.myChoicesCollection = new myChoicesCollection(this);
      this.choiceView = new ChoiceView(this);
      this.resultView = new ResultView(this);
      this.btnView = new BtnView(this);
    }
  };
  new App();

}(this.self || global))

ガーッと書き終わって気付いた細かいところはあるけども、まぁそっとしておこう。

このパターンのいいところ

  • 仕様変更にそこそこ強い

このパターンのわるいところ

  • とにかくコードが長い
  • サクッと書けない
  • 周りからはその程度、「スグできるっしょ!」って思われてるので、工数がもらえない
  • 知識のない人にも引き継ぐにしては、ちょっとオレオレ感が半端ない

悩んでるコト

まぁサンプルのコードを見てもらった上で、こういう要件の場合に、

  • どっちのパターンを使うべきか。
  • また、その判断基準はどういうもので、どのタイミングで判断すべきか。

ってのに悩んでます、と。

最初に聞いてる要件だと、パターンAで全然いけそうやと思ってたけど、
なんか日を追うごとに要件が増えていって、コレはもうあかん!ってなってパターンBに、
しかし書き直す時間はないっていうのが最近多いんですよねー。

主要な機能に関しては保守性重視の後者、小さい機能に関しては前者ってしてますが、
そのせいで中途半端な規模の機能がきたときに悩む。

プロジェクトの規模とか、案件のメンバーとかそういう諸条件によるとしか言えないのはまぁわかるとしても。
単純に羅列して機能が5コ以上あったらもうDOMベースのJavaScriptはキツいなーとか、あるのかしら・・。

うーん、やはり設計力が試される感じなのか。