ジンジャー研究室

長めのつぶやき。難しいことは書きません。

脱PolymerなWeb Componentsデザパタ

はじめに

今朝話題になっていたWeb Componentsの基本的な使い方・まとめ に触発されてみる。

すごい…!私にはこんな緻密な資料は書けないorz

だけど何か書きたい。書くぞ。

そろそろWeb Componentsしたい

Web開発に革命をもたらすと噂のWeb Componentsだが、そろそろプロダクションへの導入を検討したい。 2014年12月現在、Chrome 36+で全機能が使えるので、デモ画面や開発(テスト)ツールでは既にPolyfillなしで色々出来る。積極的に遊んでみたい。

とはいえ、現状と言えばWeb Componentsに関する情報は紹介記事がほとんどで、あまりプラクティカルな領域に踏み込めていない感がある。まあそんなものは実際に使い始めればわんさか出るので、別にその時になってからでも良いのだが、せっかくなので戦略の一つや二つ練っておきたい。

もうひとつ、Google製PolyfillライブラリのPolymerについて。 少し触ってみた感じでは特に不自由もなく素晴らしいライブラリだと思うのだが、知らず知らずのうちにロックインされてしまう微妙さがある。 それには多分いくつかの要因がある。

  • Web Componentsの記事を調べに行くと一緒にPolymerを紹介される
  • Template要素に十分な機能を持たせるライブラリがPolymerにしかない
  • webcomponents.js(旧platform.js)とpolymer.jsの機能の境目が曖昧
  • polymer-elementとして流通しているライブラリを使うと伝染する

そういうわけで、「なんだか知らないけどPolymer使っとけば安心でしょ」みたいな思考放棄の果てにjQuery的世界を再現する前になんとかしたい。 PolymerもjQueryも悪くないけど。

というわけで、実際使っていく上で考えうる限りの使い方のバリエーション、留意点などをざっと並べてみようと思う。

以下ベストプラクティス集じゃないので、念のため。

4つの仕様のうち使いたいものだけを慎重に選ぶ

Web Componentsは以下の4つの仕様からなる。

  • Custom Element
  • Shadow DOM
  • Template Element
  • HTML Imports

もちろんだが、これらは全てを統合して使ってもいいし、独立して使うことも出来る。 全てが統合された理想世界については、既にPolymerの方で語られているのでここでは特に触れない。

ここからはPolyfillをどこまで信用するかという話でもあるのだが、やはり心情的にあまり危険な橋を渡りたくないので、自分のアプリにとって必要最小限の機能を使っていきたい。 (Webサイトならいざ知らず、ブラウザのバージョンアップによるデグレードに対してパッチを当てるまでに時間がかかり、しかもそれが致命的になる場合もある)

HTML Importsを使わない

まず第一歩。 勝手な推測では大多数の人の目的はCustom Element(+Shadow DOM)なのでは、と思わなくもない。 そこで、HTML Importsを使わずに普通に.jsファイルで済ましてしまうことが可能だ。

Template Elementを使わない

また、Template要素を使う必要性も必ずしもないので、ここも好みのテンプレートに置き換え可能だ。従来の使い慣れたテンプレートエンジンや、多分Virtual DOMも使える。 ES6では複数行の文字列リテラルを宣言できるので、うるさいバックスラッシュともオサラバできる。

Shadow DOMを使わない

ここはちょっと何が正しいのか判断が付かない。というのは、個人的にCSSの干渉で「そこまで」困ったことが無いので、いまいち重要性がピンと来ないというのがある。 個人的にはそれよりも、「共通のスタイルが欲しい時にCSSコンポーネント内にインライン展開するのってどうなの?」という方が気になる。(←見解があれば教えてください)

さておき、カプセル化を気にしなければShadow DOMも削ることが出来る。(Shadow DOMのPolyfill難易度は高い) 以上のように色々そぎ落としていくとCustom Elementだけで事が済む。ミニマル主義ならこれで行きたい。

非動作時の代替手段を用意する

何らかの事情でWeb Componentsが動作しない場合に、デフォルトの動きを用意するといったことも可能だ。

  • ブラウザがWeb Componentsをサポートしていない
  • ブラウザのJavaScript機能がオフになっている
  • CDNが落ちている
  • バグ

記憶によればGitHub<time is="relative-time">要素がそのような実装になっていたと思う。 通常動作時は相対時間を表示し、動作しない場合は絶対時間が表示される。私GitHub賢い思う。

Custom Elementの生成を動的にカスタマイズする

Custom Elementの扱い方として常に紹介されるやり方は、「HTML Importなりscriptタグで読み込むなりすれば後は自由に使えるよ」というものだ。ただ、それだけでは済まない場合、もっと柔軟に扱うことが可能だ。

別の実装に切り替える

まず準備として、scriptの実行時点ではdocument.registerElement()せず、何らかの関数が呼ばれた時に登録するようにする。 これによって動的に決まるコンテクストにおいて、同じインターフェイスを持つ別の実装に切り替えることが可能になる。

if(debug) {
  registerDebuggableXxxElement();
} else {
  registerXxxElement();
}

コンテクストを渡す

パラメータを渡すことも可能。

registerXxxElement(userSettings);

要素名を変える

もちろん代わりの要素名を渡すことも可能。

registerXxxElement(userSettings, 'another-name');

フォーム部品の動作に気を使う

最早あるある現象なのだが、ハイユーザビリティを謳いながらブラウザの機能を殺していく、という皮肉なことが気をつけていないと起こりかねない。 Custom Elementだからと言って調子に乗ってオレオレ実装を始めてしまうと、元々普通に出来ていた機能をどんどん奪ってしまう。その傾向は特にフォーム部品に顕著に現れる。

  • 適切なタイミングでchangeイベントを発火する
  • <select>相当の要素の場合、valueへの代入が配下のoptionに影響する
  • 上の逆
  • フォーム送信時にシリアライズされる
  • フォーカスがあたる
  • :checked擬似クラスによる選択、スタイルの変更が出来る

正直、これらを全て正しく実装することを考えると、私ならしんどい。テストも書いたがそれでもしんどい。 しかも、:checked擬似クラスを再現する方法は用意すらされていない。 手軽に機能を継承するならis属性(is="xxx")を使うのが良いだろうか。この辺りの知見がもっと欲しい。

ここで機能を奪ってしまうことによるデメリットは以下のようなものが考えられる。

  • 他のライブラリとの親和性…たとえば:checkedなどを当たり前のように利用する
  • より大掛かりなシステムへの依存…たとえばmy-formのような特別な要素と密結合する
  • 特別な使い方を覚えるための学習コスト

調べてもあまり出てこないが、この辺の議論や見解も欲しい。

Template Elementを使って画面の初期化を遅らせる

例えば、ページ内に複数のタブがあった場合、隠れている部分の初期化は後回しにしたいという要求がある。

<div id="content-a">
  <template id="tmpl-a">
    ...
    <script src="init-a.js"></script>
  </template>
</div>
<div id="content-b" style="display:none;">
  <template id="tmpl-b">
    ...
    <script src="init-b.js"></script>
  </template>
</div>

表示されるべきコンテンツの初期化が、隠れているコンテンツの初期化にブロックされてしまうのは勿体無い。 そこで、上のように隠れている#tmpl-bのアクティベートのタイミングを遅らせることによって、初期化順序を最適化することが出来る。特にtemplate中のスクリプトが重い処理であるほど有効だ(手元での検証結果)。 もちろん銀の弾丸では無いが、武器のひとつにはなる。

Vulcanizeは早めに検討する

HTML Importsを多用するとどうしてもネットワークコストが高くなるため、JavaScriptと同様にHTMLもひとつに結合したい。 そんな時にPolymerチーム製のVulcanizeというツールが必要になる。

ただしこれには条件があって、静的にURLが宣言されている場合に使用が限られる

つまり、柔軟にURLをあれこれするフレームワークをがっつり構築した後でVulcanizeの導入を検討すると泣くことになる。(しかもエラーメッセージがはっちゃけていて分かりにくい) しかも現状HTML Importsの結合ツールはこれしか無いので、完全に詰んでしまう。結合の可能性は最初から念頭に置いておきたい。

HTML Importのモジュール管理

hrefを工夫する

HTML Importを活用して、例えば、foo.htmlbar.htmlというモジュールを独立に作成して提供したいとする。 ここで、foo.htmlbar.htmlに依存しており、アプリケーションからの利用イメージは次のようであるとする。

Application => foo.html => bar.html

この場合、foo.htmlからbar.htmlへのリンクをどうしたら良いだろうか。 Polymerは次のようにしている

<link rel="import" href="../bar/bar.html"></link>

何をしているかというと、モジュールがフラットに展開されたときに相対パスが通るように記述されている。

│
├── bower_components
│  ├── foo
│  │   └─ foo.html
│  └── bar
│      └─ bar.html

ここでひとつ問題になるのが、Bowerあるいは他のパッケージマネージャがフラットにモジュールを展開することに依存している。

│
├── bower_components
│  ├── foo
│  │   └─ foo.html -- requires bar@1.0.0
│  ├── baz
│  │   └─ baz.html -- requires bar@0.8.0
│  └── bar
│      └─ bar.html

そうすると、フラットにならない例外があった場合や別のパッケージマネージャを使いたい場合に、おそらく困った事態になる。 この辺りもコミュニティが盛り上がらないと議論が進まない気がしているし、自分もあんまり考えたくないからES6モジュールでの提供に逃げたい気持ちでいる。

菱形依存によるスクリプトの重複実行を避ける

こちらは既にちらほら話題に上がっているのだが、例えば、foo.htmlbar.htmlの両方がjQueryに依存していた場合。 具体的には次のように菱形(ダイヤモンド)の依存関係になっている場合、

Application => foo.html => jquery.js
Application => bar.html => jquery.js

普通に双方からscriptタグでjQueryを読み込むと、2度スクリプトが実行されてしまう。 これを避けるためには、jquery.jsをHTMLでラップすると良い。

Application => foo.html => jquery.html => jquery.js
Application => bar.html => jquery.html => jquery.js

こうすることで、jquery.htmlは2度目のアクセス時からは読み込み済みという扱いになり、内部で呼ばれているjquery.jsの実行も1回で済む。

その他

Resource Timing APIが、HTML Importを通じて呼び出されるサブサブリソースの情報を拾わない気がする…。

まとめ

私的見解をぶちまけてみた。まぁどれも結論は出ていない。

もうとにかく使い方のバリエーションが沢山あって、ベストプラクティス欲しい!という感想。(←Polymer使え)

Object.observe() のコールバック実行タイミング

ざっくり分かるスクリプト

下のスクリプトを実行すると、どのようにalertが表示されるでしょう。

var obj = {};
setTimeout(function() {
  alert('D');
});
Object.observe(obj, function() {
  alert(obj.a);
  obj.a = 3;
});
alert('A');
obj.a = 1;
alert('B');
obj.a = 2;
alert('C');

結果(反転) ⇒ A B C 2 3 D
Chrome 36で確認)

即時 > Object.observe > setTimeout
の優先度で実行されるようです。

Haskellでコードゴルフするためのメモ

明日プロダクションコードで使える無駄知識。

変数を1文字にする

基本ですね。

空白を入れない

なるべく隣接させる

  • 各種記号
$ . + - * / : ++ !! |
  • クォーテーションの類
'' "" `` () [] {}
  • 数字の次
2in

改行しない

;で改行できます

型を書かない

型推論の恩恵に預かりましょう。

@を活用する

引数を同時に展開できます。

f a@(_,y)=y+g a

括弧の代わりに$を使う

基本ですね。

map f(g a)
map f$g a

カリー化

これも基本です。

f a=g a
f=g

もっとカリー化

ポイントフリーしていきましょう。

f a=g(h a)
f a=g.h a
f=g.h

何度も使う関数を別名にする

長い名前の関数ほど有効。

a=length

関数をpublicにする

whereの中に居る必要の無いものは外に出します。

mapよりもリスト内包表記

多くの場合、リスト内包表記の方が短くなります。

let-inよりもwhere

1文字分だけ稼げます。

f a=let x=g a in x
f a=x where x=g a

concatよりも>>=id

1文字分だけ稼げます。

concat["a","b"]
["a","b"]>>=id

1文字の結合にconsを使う

Stringは[Char]なので。

" "++a
' ':a

ガード節を使う

if-then-elseより短くなる。
ガード節は関数のトップレベルでも使用できる。
otherwiseの代わりにTrueが使える。

f a=if a>5 then 1 else 0
f a|a>5=1|True=0

Polymerで個人のホームページを構築してみた

Web ComponentsでWebサイトの作り方が変わる

ブログやSNSの登場以来ずっと放置していた自分のサイトを復活させてみた。(祝!)
一応このブログは技術ブログのような顔をしているので、コンテンツの中身についてはあまり言及しない代わりにアーキテクチャについて少し喋ろうかなと思う。


で、今回はPolymerというWeb Componentsのpolyfill+αなライブラリを使ってみたので感想を書いてみた。
Polymerというライブラリ自体の性能とか導入可能性について言いたいわけではなくて、あくまでWeb Componentsを使うとこんな感じでサイト構築できるんじゃないかというところ。

完成したページ


http://jinjor.herokuapp.com/

World Maker
世界の創造主を名乗って自作の音楽を公開。かれこれ11周年くらい。

Polymerを使ったカスタム要素の実例

Ploymer主にを使用した場所。

  1. 音楽プレイヤー
  2. 音量ツマミ
  3. 全体のレイアウト

以降、順番に見ていく。

音楽プレイヤー

YouTubeっぽい自作のプレイヤー(上のサムネイル画像の下の方)をカスタム要素化。
Web Componentsの一番オーソドックスな使い方だと思う。今まではこういったコンポーネント部品を共通化する方法がなくサーバサイドのロジックでこしらえていたのだが、それをクライアントサイドで独立して出来るようになったというのがポイント。
つまり、サーバサイドで動的なデータを扱う必要がなければ、静的なファイルを配信するだけで良い。実際に今回のサイト構築はgrunt-contrib-connectというモックサーバだけで事足りた。

以下は、カスタム要素の呼び出し元のコード。

<link rel="import" href="x-midi-viewer.html"></link>

...

<x-midi-viewer audio="music/2014/sakura.mp3" smf="music/2014/sakura.mid" delay="1500"></x-midi-viewer>

音楽ファイルとしてMP3ファイルとMIDI(SMF)ファイルを用意する。delayは両者のタイミングのずれを補正するもの。カスタム要素は事前にインポートしておく。
呼び出し先は長いので省略。

音量ツマミ

f:id:jinjor:20140224032516p:plain

もう少し凝った使い方として、子コンポーネントとモデル(≒可変なデータオブジェクト)を共有するといった事が出来る。
DOMに慣れていると「子コンポーネントからクリックイベントが飛んできたら目的のオブジェクトを更新して…」というアプローチになりがちだが、ここでは更新して欲しいオブジェクトを直接渡してしまうことにした。

呼び出し元。

<x-volume-controller gain="{{sequencer.gainNode}}"></x-volume-controller>

ここでgainNodeというのは、Web Audio APIに定義されているGainNodeを実装したインスタンスで、これを渡してやることでカスタム要素に音量の制御を委譲している。

呼び出し先の一部を抜粋。

<polymer-element name="x-volume-controller" attributes="gain"><!-- ★ -->
  <template>
    <style>
      ...
    </style>
    <button class="volume_icon" data-value="{{loudness}}"></button>
    <button class="volume_slider" on-mousedown="{{startMoving}}" on-mousemove="{{move}}">
      <div class="volume_slider_foreground" style="left:{{left}}px"></div>
    </button>
  </template>
  <script>
    Polymer('x-volume-controller', {
      ready: function() {
        ...
      },
      move: function(e){
        var self = this;
        var x = e.pageX - e.currentTarget.offsetLeft;
        if(self.moving){
          var left = Math.max(Math.min(x, 46),0);
          self.left = left - (firefox ? 3 : 0);
          self.gain.gain.value = left / 46;// ☆
          self.updateLoudness();
        }
      },
      ...
    });
  </script>
</polymer-element>

<template>と対応する<script>を合わせて記述する。
Polymer関数の第2引数は所謂ViewModelになっていて、このオブジェクトの状態がテンプレートに自動手反映される。また、テンプレートはイベントと関数との関連付けも同時に担う。
スライダーを動かしたときに音量調節する処理をmoveメソッドに記述している。★で受け取ったGainNodeを☆で操作。所々怪しげなコードが書いてあるが気にしたら負け。readyは初期化時に呼ばれる。

全体のレイアウト

メインフレームの外(=メニュー部分)を共通化したい場合の新しい選択肢。つまりこう。

  • インラインフレーム(一部を再描画)
  • サーバサイドで共通化(全体を再描画)
  • Ajaxで中身を書き換える(一部を再描画)
  • クライアントサイドで共通化(全体を再描画)← New

汎用部品だけではなく、アプリケーションの責務分離や共通化のためにWeb Componentsを使うのもありなのではないかと思う。

呼び出し元のコード。

  <body>
    <x-layout>
      <div>
      <h3>リニューアルしました</h3>
        ...
      </div>
    </x-layout>
  </body>

今までと違うのは、カスタム要素にコンテンツを丸ごと渡している点。

呼び出し先のコード。

<link rel="import" href="x-social.html">
<polymer-element name="x-layout" noscript>
  <template>
    <style>
      ...
    </style>
    <div id="container"><div>
      <x-social></x-social>
      <header>
        <a href="/"><h1>World Maker</h1></a>
        <p>音楽とプログラミングを主食にするジンジャーのホームページです</p>
      </header>
      <nav>
        <ul>
          ...
        </ul>
      </nav>
      <div id="main-contents">
        <div>
          <content><!-- ★ここに挿入される -->
          </content>
        </div>
      </div>
    </div></div>
  </template>
</polymer-element>

渡されたコンテンツは<content></content>の位置に挿入される。(複数の場合など、更に細かく指定することも出来る。)

ハマったところ

現時点で未解決の課題。単なる調査不足とも言う。

ページの構築が遅い

動的にロードしているのである程度は仕方ないのだが、従来のAjax同様に最初にページが表示されてからガクガクっと画面が動く。
今回は対策方法が見つからなかったので、とりあえず放置。結構簡単なページでもこれなのでレイアウト用に使うのはあまり向いていなかったりするのだろうか。

メタ情報などを共通化できなかった

これもレイアウト関連。Open Graphを使う際に<meta>タグを挿入する必要があったのだが、<head>から離れていたためにカスタム要素で上手く共通化することが出来なかった。

カスタム要素内のスタイルを外部ファイルに切り出せない

カスタム要素の<style>内に記述する必要がある。これがPolymer独自の制限なのかどうかは分かっていない。

カスタム要素内のid等の扱い

ハマったというより勉強不足できちんと理解していないので不安を募らせているところ。
例えばカスタム要素内のidは、次のようにして取り出すことが出来る。

<div id="screen"></div>
...
<script>
Polymer('x-midi-viewer', {
      ready: function() {
        var el = this.$.screen;
...
</script>

このidは外界のそれとは区別されるので、document#getElementByIdで取得することが出来ない。
で、今回はその辺の細かい仕様はなんとなくの理解で済ませていたのだが、大規模開発でコーナーケースにあたっても混乱無く出来るか自信は無い。あとjQueryなど既存のライブラリが不自由なく使えるかどうか。

Web Componentsと微妙に仕様が異なる

例えば、ViewModelのバインディングなどはPolymer独自の仕様。どこに差異があるのかをある程度把握しておかないと、すんなり移行というわけには行かなそう。
また、要素構築時のコールバックの一部が動かないなどの不具合があった。

最後に

いくつか不安要素はあったものの、現時点でもある程度のものは普通に作れるので期待して良いと思う。

関連する去年の記事。もし興味があれば。
GUIをツリーにする話(前篇) - ジンジャー研究室
GUIをツリーにする話(後篇) - ジンジャー研究室
GUIをツリーにする話(新篇) - ジンジャー研究室


以上!

JavaScriptでバグが消えるまで

動的型付け言語は実行時にエラーが出るから嫌だよね、と良く言うけれども、実際にどんなエラーがどのくらい出ているのかを遊びがてら試してみた。

作ったものと条件(ざっくり)

  • HTML中でSVGを一定時間アニメーションさせる
  • ライブラリとしてRaphael.jsとBackbone.jsを使う
  • HTML+JSで130行程度
  • エディタ機能はシンタックスハイライトのみ

これを一度も実行せずに最後まで書き切った後、そこそこ想定したとおりに動くようになるまでに、修正を何回入れれば良いかを試してみた。

結果(エラーの起きた順)

Uncaught SyntaxError: Unexpected token ) 

どこかからコピったら、関数の終わりに閉じ括弧が付いていた。

Uncaught SyntaxError: Unexpected token ; 

どこかからコピったら、オブジェクトのプロパティ(関数)の終わりにセミコロンが付いていた。

Uncaught TypeError: object is not a function 

newを余分に付けた結果、コンストラクタだと思っていたものがインスタンスだった。

Uncaught TypeError: Cannot read property 'width' of undefined

initializeの時点ではインスタンスにoptionsが設定されていないというBackbone.jsの仕様理解不足。

Uncaught TypeError: Cannot read property 'sequencer' of undefined

コンストラクタに変数を渡すのを忘れていた。

Uncaught TypeError: Cannot call method 'add' of undefined

Backbone.Modelのプロパティを取得するのにgetを呼ぶ必要があるのを忘れていた。

Uncaught TypeError: Cannot read property 'width' of undefined

initializeの時点ではインスタンスにoptionsが設定されていないというBackbone.jsの仕様理解不足。

Uncaught TypeError: Cannot read property 'time' of undefined

Backbone.Modelのプロパティを取得するのにgetを呼ぶ必要があるのを忘れていた。

Uncaught TypeError: Cannot set property 'undefined' of undefined

Backbone.jsの仕様理解不足。クライアントサイドで自動付与されるIDはidではなくcid。

Uncaught TypeError: Cannot set property 'c6' of undefined

インスタンスのプロパティ(ハッシュ)の初期化忘れ。

(何も表示されず)

Backbone.Modelのプロパティを取得するのにgetを呼ぶ必要があるのを忘れていた。

(何も表示されず)

Modelのコンストラクタに渡す変数の型が間違っていた。

(図形が大きすぎる)

数値が間違っていた。

(図形が2つある)

メソッドを誤って別の場所で2度呼んでいた。

(図形の表示位置がおかしい)

すぐに解決できなかったのでここで終了。

統計

おおよそ次のような結果になった。

エラー回数(全15回)
  • シンタックスエラー: 2回
  • 型エラー: 8回
  • ぬるぽ相当: 2回
  • ロジックのミス: 3回
時間配分
  • 書いていた時間: 約2時間30分
  • 直していた時間: 約30分

直していた時間のうち半分が型エラーだとすると、新規開発で既に10%くらい型エラーでロスしている計算になる。
そしてもちろん、これが大規模&チーム開発になると物凄い勢いでロスが生じるのであろうと想像する。

まとめ

所詮遊びなので、条件が適当だとか分かりきったことを言っていじめないで欲しい。

上司を説得して新技術をなんとか導入する方法

頭の固い上司(プロマネ)が新しい技術を導入することに対して消極的だったとする。曰く「一体それに何のメリットがあるんだ。リスクを考えたのか。やたらめったら怪しげなものを投入するんじゃない!」
しかしあなたはこの上司をなんとか説得して、素晴らしい技術を現場に導入したい。

そんな時どうするか。ここに自分なりの意見を書こうと思う。

事前準備編

普段から信頼を得ておく

これが一番重要だ。「この人が言うならそうなんだろう」と思わせたらスムーズに事が運ぶだろう。しかし当たり前だが一朝一夕に出来ることではない。

別の場所で実績を作っておく

これも説得力があるが、簡単ではない。

「メリット・デメリットを考えて冷静に判断できる人」を普段から演出しておく

「この人はメリットばかりを見てデメリットも含めてちゃんと考えていないんじゃないか」という疑念を払拭するために、キャラを作っておく。

説得編

必要性を相手にとって身近な具体例で説明する

現段階で上司はその技術の必要性を認識していない。別に困ってないからわざわざアクションを起こす必要が無いと思っている。そこで「例えばこの間こういう事態になったじゃないですか」と、なるべく具体的な例で説明する。ここで理論的な説明(例えば「○○率が上がると××の確率はこの割合で上昇する」など)をしてはいけない。「そんなのは机上の空論で現実を踏まえていない」と思われて逆効果だ。

「相手にとって身近」というのも重要だ。全く興味の無いメリットを説かれてもピンと来ないし、むしろその人にとっては重要度が低いので「こいつはこんな些細な事のために全体のことをろくに考えずに…」と思われてしまう。

道具の特徴を網羅的に述べない

目的の技術の長所をひたすら並べても響かない。道具を使いたいだけじゃないかという印象を与えてしまう。
相手にとって一番響くポイントに限定すると良い。

相手の使っている技術を否定しない

オブジェクト指向なんて糞じゃん」等と言わない。

デメリットを指摘されたらちゃんと聴く

「でもこれこういうデメリットがあるじゃない?」と必ず言われるが、黙って言うことを聴こう。間違っても話を途中でさえぎって「いやその場合にはこう出来るし、そもそもそんなことを言い始めたら…」と早口で反論してはいけない。
とりあえず一度は頷いて「確かにそれはそうですねー」等と相槌を打っておく。こちらが分からないことがあれば質問しても良いし、部分的に正しいことを言われるはずなので素直にそこは賛同しておく。「彼は自分の意見も真摯に受け止めてまともに考えてくれている」感を演出しよう。

怖くないことをアピールする

見たことのないもの、触れた事のないものは誰だって怖いものだ。
そこで実際にそれが動作している画面なりコードなりを見せることで、何もないよりも安心感を持たせることが可能だ。見せるもの、例えばコードは、なるべく直感的に理解できそうなものをチョイスし、習得コストの低さをアピールする。「これならできそうだ」感が重要だ。

モヒカン臭を抑える

「お前には扱えるかもしれないが…」と思われてしまうことは避けなければいけない。
既にモヒカン臭を獲得してしまった人は、無臭な人に使い方を教えてみて、その人でも普通に扱えていることを説得材料にすると良い。

楽しさ・かっこよさ・幸せさを演出する

理屈ではなく感覚に訴える。
例えばGitの導入なら「Git使っている人は楽しそうだなー。カッコイイなー」と思わせる。「この前書いたコードなんだけど見てよ、GitHubに置いてるんだけどさ」等という普段からのさりげないアピールも有効だ。
気付いた頃には上司も「俺も使えるようになっちゃったもんね」とか言い出すはずだ。

「みんなやってるよ」はさり気なく

もしメジャーな技術であるに関わらず上司がそれを知らないのだとしたら、さり気なく「世の中ではこんなに使われている・実績がある」を教えると良い。インパクトのあるチャートも重要だ。
しかしあまりここを強く主張すると流されやすい人の烙印を押されてしまう。「だから何だ、ウチは関係ない」とならないように注意しよう。

予測できない部分については「やってみないと分からない」で押し切る

ある程度まで議論が進んだところで「それで本当にいけるのかなぁ?」と言い出すとは思うが、これ以上は議論しても答えが出ない事は相手も承知のはずなので「ちょっとそれはやってみないと分からないですねー」等と言っておけば、「まぁノウハウを溜める意味でも一度やってみるか」と言ってOKしてくれるだろう。

番外編

こっそり導入してみる

害がなければこっそり導入するのも手だ。「テストですか?Jenkinsで毎日回ってますが何か?」

最後に

色んな勉強会のLTでこういう発表があるので、あるあるネタなんだろうということで書いてみた。

こうしてみると、新技術の導入はほとんど心理戦だということが分かる。
まぁ、こんな回りくどい手を使わなくても、偉い人ポジションを獲得すれば鶴の一声なのだろうが。

GUIをツリーにする話(新篇)

English

前置き

この記事は以下の記事からの続きである。

GUIをツリーにする話(前篇) - ジンジャー研究室
GUIをツリーにする話(後篇) - ジンジャー研究室

前回の話をざっくりまとめると、

の2つがあった場合、モデルの受け渡しを不要にするために共有したい。
また、片方のViewによってModelが更新されたとき、もう片方のViewにもそれが反映されて欲しい。

f:id:jinjor:20131210030223p:plain

「モデルを監視する」を定義通りに、Observerを用いて実装するのにBackbone.jsが適している。しかし、記述方法としてはAngularJSのdirectiveがおいしい。

<div ng-app="app">
    <div ng-controller="Ctrl">
         <list values="members"></list>
         <sum values="members"></sum>
    </div>
</div>

これをベースとして、今回の記事では実装・設計上必要となる更にいくつかの概念について触れることにする。

プロパティのメモ化

ツリーの話からはいったん離れて、もう少し深く掘り下げると見えてくることがある。
ModelからViewへの距離が遠くなるほど再計算が多く走るという点である。

これだけでは何を言っているか分からない。
そこで、お題をもう少し複雑にするために仕様を追加してみよう。

  • それぞれの行について、全体に対する割合を併せて表示する。

すると<list>directiveの詳細は次のようになる。

var add = function(a, b) {return a + b};
var sumOf = function(values){
    return values.reduce(add, 0);
}

angular.module('app', [])
.directive('list', function() {
    return {
        restrict: 'E',
        template: '<div><button ng-click="add()">add</button>\
<ul><li ng-repeat="v in values">{{v}} ({{ratioOf(v)}}%)</li></ul></div>',
        scope: { values: '=' },
        link: function($scope) {
            $scope.add = function(){
                var value = $scope.values.length + 1;
                $scope.values.push(value);
            },
            $scope.ratioOf = function(value){
                // sumOfが何度も呼ばれてしまう
                return (value / sumOf($scope.values) * 100).toFixed(1);
            }
        }
    };
})
.directive('sum', function() { ... })
.controller('Ctrl', function($scope) {
    $scope.members = [1, 2, 3];
});

ここで顕在化するのは、関数ratioOfはループ中(ng-repeat)で何度も呼ばれているので、1回で済むはずの関数sumOfがn回呼ばれているという問題である。
宣言的に書けたのは良いが、これはHaskellでもなんでもないので前の計算を覚えてくれているはずもなく、愚直に再計算してしまう。

この問題は、次のように少々ぎこちない方法を使って解決することが出来る。

var sum;
$scope.$watch('values.length', function() {
    sum = sumOf($scope.values);
});
$scope.ratioOf = function(value){
    return (value / sum * 100).toFixed(1);
}

デモ(JSFiddle)

やっていることとしては、「values.lengthという値が変わったとき*1、sumの値を更新しなさい」とAngularに教えている。
似たような仕組みはEmber.jsにもあって、Modelに対してpropertyを設定することが出来る。Backbone.jsの場合でもModel(Collection)の変更時に同じ計算を走らせることが出来る。

ぎこちないと言ったのは宣言的に書けないという点で、さらに一般化して、Model <- Model <- Model ... <- Model <- View のように連なった時、常に依存関係と計算量を考慮してプログラミングするのはきつくなってくる。


このように、「ある値を変更した時にそれに依存した値を自動的に再計算する」ことを目的とした、Reactive Programmingというパラダイムがある。*2
MVC」は問題をModelとViewという2つの領域に分ける概念だが、実際にはViewのための中間Modelが居たり、Viewの後ろに更にスタイル記述が居たりするので、実際にはもっと沢山の階層が存在する。そう考えた時、Reactive Programmingの概念を導入すると、もっとすっきり解決することが多いのではないかと思われる。*3

f:id:jinjor:20131212200810p:plain

Reactive Programmingは一般的な概念だが、GUIやゲームへの適用を試みている言語のひとつにElmがある。サンプルを見れば、どれだけ簡潔に書けるのかが一目で分かるはずだ。

アプリケーション設計

コンポーネントツリーの話から大分離れていたので、ここで話を戻すことにする。
ひとつのModelを子供のView達が共有する話だった。これを更に組み上げていくと全体としては次のような図になることが想像できる。

f:id:jinjor:20131212193037p:plain

ここで、Modelとなるオブジェクトは常にグローバルから渡されているわけではなく、コンポーネントにとっての関心を扱う固有のModelが子供に共有されているというケースが考えられる。つまり、親からは見えず、子供には共有されている。恐らくこのようなアプリケーションの組み方が洗練されてくる頃には、このスコープの概念に何か名前が付くのではないだろうか。

グローバル変数が欲しくなったら

さて、この調子でどんどんアプリケーションが巨大化していくことを妄想すると、次のようなことが発生するのではないだろうか。

十分にあり得る。状態を共有しないのであれば単にユーティリティを使えばいいのだが、例えばメニューバーやステータスバーの類、それから画面全体の状態を扱うものに関して上記のケースが当てはまる。その場合、ツリーのルートから引数に渡していくのは面倒になる。面倒でも渡せばいいのだが、しっかりしたインターフェイスを持たないと仕様変更の際に打撃を受ける(動的型付けでは特に)。そのノードに至る全ての経路を洗い出す必要が出てくるからだ。
そういうわけで、「このオブジェクトだけは特別にグローバルに置いておくから好きにしていいよ」という設計的な判断があったとする。図にすると次のようになる。

f:id:jinjor:20131212195418p:plain

判断の是非はさておき、このようにする場合は、アプリケーションレイヤーとユーティリティレイヤーをしっかり意識する必要がある。アプリケーションレイヤーでは疎結合を犠牲にして実装の簡便さを提供する。一方で、ユーティリティレイヤーに属するコンポーネントは外界に依存しないことを心に誓う。

ただし、一度グローバルへの直接参照を許可する設計にすると後戻りが出来ないのが注意点としてある。
例えば、「ロガーは統一して使えばいいだろう」と思っていても、後になって「このコンポーネント以下のログは出さない」のような変更があると対処できなくなる。

Web ComponentsとModel Driven Views

さて、概念図ばかりなので「じゃあどういう風に書けるのか?」が気になる。

AngularJSでは、上記の例のようにdirectiveを登録する必要があった。しかしそもそもAngularJSというフレームワークにガッツリ依存しているし、こういうことをしたい場合に標準の仕組みが欲しい。ということで、Web Componentsの導入を検討したい。
詳細な解説は他の記事に譲るとして、要は「カスタムタグ(コンポーネント)が書ける!」という事実だけで十分である。例はリンク先を見て欲しい。テンプレートと対応するスクリプトをペアで記述する。

ところで、Web ComponentsはMVCフレームワークではなく、単にDOMの延長である。だからAngularJSのような(双方向)データバインディング機構を期待することはできない。

ちなみに、そのような機能を備えたテンプレートは、MDV(Model Driven Views)と呼ばれている。
Web Componentsを実装していると謡うライブラリ(PolymerやDart Web UIなど)が増えてきているが、必ずしもこの機能を実装しているとは限らない点には注意が必要である。(逆に言うとその辺に注意しながら動向を追っていると面白い!)

最後に

3回に亘って「GUIをツリーにする」ことに関しての私見をグダグダと語ってきた。現時点で持っている理想とそれに至る経緯や観点、それから潜在する問題点と現時点での動向を一通り嘗めたつもりである。

仮説として理想は持ったので、あとは誰かが実装するのを待つか自分で実装するかという話になるのだが、これがなかなか難しくて既に挫折気味である。
仮に完成したとして、それをプロダクトに導入できるかと言ったら(当然ながら)別問題で、特に「モデルを共有するコンポーネント」というシロモノは馴染みがないためにメンタルモデルを共有しにくい。だから何とか伝えようと頑張ってみたのだが、果たしてどうだろう。

また、最後に少しWeb Componentsについて触れたが、MDVが無くても十分面白い技術なので積極的に使っていきたい。


以上。長文お疲れ様でした。

*1:valueでは何故か感知しなかった

*2:特にデータの流れを抽象化して関数的に表現するものをFRP(Functional Reactive Programming)と呼ぶ。JavaScriptライブラリではBacon.jsが有名。

*3:それでも計算量を最適にするのは簡単ではない。例えば評価のタイミングにはpull型とpush型があり、状況に応じて使い分ける必要がある。