IT戦記

プログラミング、起業などについて書いているプログラマーのブログです😚

サイボウズで学んだこと

はじめに

2010 年 9 月 15 日を持ちまして、サイボウズ・ラボを退職いたしたました。
報告も兼ねて、久しぶりにブログを書いてみたいと思います。

(写真はゆうすけべーさんです)

この会社に入って、たくさんの学びと思い出がありました。

その一つ一つをまとめていければ、素晴らしい記事になるのかもしれませんが、僕は文章が苦手です。
ですので、うまく退職のエントリを書き上げることができません。
言葉にできない。そんな感じです。
なので、このエントリはサイボウズ・ラボやサイボウズ本社の仲間たちへのありがとうの気持ちをこめて、自分らしく最後まで JavaScript のことを書きたいと思います。

サイボウズでの最後の仕事

僕にとって、サイボウズでの最後の仕事は「JavaScript で新しいユーザーインタフェースを作ること」でした。
そして、その中で始めて複数人による大規模な JavaScript の開発というのを行いました。
大規模と言っても、ソースコード一万〜二万行程度、プログラマー 3 人くらいの規模なので、「大規模」というと違和感のある人も多いかもしれません。
ただ、僕の中では人生で一番大規模な JavaScript の開発であったことは間違いありませんでした。
このような機会は、なかなかないと思いますのでこの開発で学んだことを書いていきたいと思います。

大規模 JavaScript 開発の中で学んだこと

大規模な JavaScript 開発で様々なことを学びました。

大まかにいうとこの三点でしょうか。

JavaScript という自由度の高いプログラミング言語の中でどうやってコードを共有していくか

JavaScript は非常に柔軟で自由度の高いプログラミング言語です。
たとえば、 jQuery で書く JavaScriptPrototype.js で書くJavaScript のコードが全然違うものに見えるように様々な指向の書き方をすることができたりもします。
そのような状況ではコードがぐっちゃぐちゃになりやすいです。
なので、その対策として以下のことをやりました。

  • 基本ライブラリの統一
  • 名前の規則を作る
  • ファイル間の依存解決をライブラリに任せる(自分で script 要素を書かない)
  • 概念を「絵に描いて、説明できる」ということを重視する
  • 複雑な処理は、共通の書き方にする

まあ、あたり前といえばあたり前のことばかりですね。

基本ライブラリの統一

JavaScript では、外部ライブラリというと「基本ライブラリ」「応用ライブラリ」の二つに分類されます。

  • 基本ライブラリ
    • 特定の機能というよりは、プログラミング自体を簡単にするためのライブラリ
    • 基本的に「基本ライブラリ」はページに一つだけ
    • 基本ライブラリ同士は、一緒に使うと DOM 2 Event 周りの API で不具合を起こすことがある
    • 例えば jQueryPrototype.jsGoogle Closure 、 Dojo toolkit などなど
  • 応用ライブラリ
    • ある特定の機能を持ったライブラリ
    • シンプルにその機能だけを実装したものが多く、有名どころの基本ライブラリとは衝突しないように作られていることが多い
    • 例えば ScriptaculousJavaScript User Agent Identfier 、 JavaScript-XPath などなど

サイトで一つの「基本ライブラリ」をちゃんと決めて開発しようね。ということです。
まあ、当たり前っちゃ当たり前っですね。
今回は「Google Closure」を使いました。
理由は「機能豊富で、やりたかったことが出来たから」です。
でもやっぱり、人によっては「あー、 jQuery 使いたいよー><」「俺は、やっぱり Base2 だぜー」とかってなるので、そこはがまん、が、がまんで><

名前の規則を作る

あまり、やりすぎると窮屈にはなるのですが、やっぱり複数人でやるときは必要なんだと思いました。
たとえば、

// クラス名はクラスはアッパーキャメルケースで書く
// 名前空間は小文字
goog.provide('cybozu.ui.Component');
goog.require('cybozu.mixin');
goog.require('cybozu.assert');
goog.require('goog.event.EventHandler');

// 省略可能な仮引数には、最初に opt_ を付ける
cybozu.ui.Component = function(element, opt_name) {
    this.eventHandler_ = new goog.event.EventHandler(this);

    this.element_ = element;
    this.name_ = opt_name || '';
    this.child_ = new cybozu.ui.Component(element.firstChild, 'child');

    // イベント名はロワーケース
    this.eventHandler_.listen(child, 'dragentertochild', this.handleDragEnterToChild, false);
    this.eventHandler_.listen(child, 'dragexittochild', this.handleDragexitToChild, false);
};

cybozu.mixin(cybozu.ui.Component.prototype, {

    // 将来変更する可能性のある名前(プライベートな名前)には最後に _ を付ける。
    status_: 'none',

    hoge_: 'fuga',

    element_: null,

    name_: null,

    // 変数名、メソッド名、プロパティ名はロワーキャメルケース
    evenetHandler_: null,

    getHoge: function() {
        return this.hoge_;
    },

    // イベントなどのコールバック(イベントハンドラ)として使われる名前には、最初に handle を付ける
    handleDragEnterToChild: function(evt) {
        this.enterChildOver();
    },

    handleDragExitToChild: function(evt) {
        this.exitChildOver();
    },

    // ユーザーインタフェースの部品の状態が変化した場合に呼ばれるメソッド名には、最初に enter を付ける。
    enterChildOver: function() {
        DEBUG && cybozu.assert(this.status_ === 'none');

        this.status_ = 'childover';
        this.element_.className = 'childover';
    },

    // 逆に状態をニュートラルな状態に戻す場合は、最初に exit を付ける
    enterChildOver: function() {
        DEBUG && cybozu.assert(this.status_ === 'childover');

        this.element_.className = 'none';
        this.status_ = 'none';
    },

    // 状態の確認は isIn を付ける
    isInChildOver: function() {
        return this.status_ === 'childover'
    }
});

のコメントに書いてあるような名前付けでやります。

ファイル間の依存解決をライブラリに任せる(自分で script 要素を書かない)

JavaScript の処理系は、 PHP の require_once のようにファイル間の依存関係を解決する方法を持っていません。
もし、依存解決をやりたければ、外部のツールや JavaScript 自身に任せる必要があります。
あまり JavaScript を使っていないページの場合、とくに依存解決などはしなくても自分自身で読み込む JavaScript を手書きすることで解決できますが、大規模な開発だとそういうわけにもいきません。
ファイル間の依存解決の方法は、以下の 3 とおりあります。

方法 開発時 リリース時 特徴
同期 XMLHttpRequest と eval × eval なので遅い。eval する箇所のスコープの影響を受ける。依存関係は、実行時に計算される。依存関係の変更があった場合などに何もしなくていいので、開発が楽
動的 script 要素挿入 事前にファイル間の依存関係の情報を JavaScript で読める形のデータにしておく必要がある。依存関係が変わる度に、依存関係データを更新する必要がある。
依存するすべてのファイルを一つに結合 × 無駄なコストが一切ないので早い。コードを少しでも変更したら再結合する必要がある

開発時には「同期 XMLHttpRequest と eval」か「動的 script 要素挿入」が出来て、リリース時には「依存するすべてのファイルを一つに結合」が出来ることが望ましいですね。
ちなみに Google Closure では「動的 script 要素挿入」と「依存するすべてのファイルを一つに結合」が出来ます。
Dojo Toolkit は「同期 XMLHttpRequest と eval」と「依存するすべてのファイルを一つに結合」が出来るんだったと思います、たしか。

概念を「絵に描いて、説明できる」ということを重視する

JavaScript では、様々な書き方が出来るのですが、今回はクラス指向で書いて、 prototype の書き換えや無名関数も基本的には使わないというルールでやりました。
なぜ、そのようにしたかというと「概念を絵に描いて、説明できる」ということが重要だと考えたからです。
クラス指向だと UML っぽい書き方もしやすいですしね。
あと、「様々な状態にあえて名前を付ける」ということもやりました。これも、状態遷移図や状態マトリックスを描きやすくするためです。
あと、「単純なコールバックよりも、イベントを使う」ということもやりました。これも、「イベント名」という名前を付けてシーケンス図が描きやすくするためです。
あとは、親と子をあえて明確にしたり、とか、イベントを listen する対象を親と子だけに限定したり(兄弟への listen は親を経由する)とかですかね。

複雑な処理は、共通の書き方にする

これは、まあ当たり前のことっちゃ当たり前のことなんですが、まあ有り体に言えば「JavaScript でよく出てくるようなデザインパターンは使おうね。」ということです。
たとえば Deferred なんかがいい例かもしれません。非同期処理のエラー処理を書くときに便利ですね。

デバッグをどのようにやっていくか

デバッグは、特に大規模開発には関係ないかもしれませんが。みんなでノウハウを共有しておくことは重要です。
特に今回の開発で役にたったデバッグノウハウをまとめておきます。

アサーションを使う

ユーザーインタフェースのようにテストしずらいコードでは、アサーションを使うとコードの質を効率よく改善できます。
アサーションというのは、「このコードを実行してるということは、事前にこういう状態になっているはず」ということを確認するためのコードを埋め込むことをいいます。
例えば、以下のような assert 関数を作り、

function assert(condition, opt_message) {
    if (!condition) {

        if (window.console) {

            // メッセージの表示
            console.log('Assertion Failure');
            if (opt_message) console.log('Message: ' + opt_message);

            // スタックトレースの表示
            if (console.trace) console.trace();
            if (Error().stack) console.log(Error().stack);
        }

        // デバッガーを起動し、ブレークする
        debugger;
    }
}

こうしておくと、前提条件が崩れた時点でデバッガー起動し、ブレークするので、不具合を調べるのが楽になります。
また、この関数を呼び出す際に以下のように、グローバルなフラグかなんかを使ってリリース時は呼ばないようにしておくといいでしょう。

var DEBUG = true; // 開発時は true 、リリース時は false にする

Component.prototype.handleMouseOver = function(evt) {
    // 開発時だけアサーションする
    DEBUG && assert(this.element_.className === 'out');
    this.element_.className = 'over';
}

様々な条件がある場合は、様々な条件をまとめて一つの名前をつけて、その名前を「状態名」とします。
そして、その条件が満たされたときに「状態名」を変更し、その条件を必要なときに「状態名」を確認するようにします。

Component.prototype.enterDragOver = function() {
    // 状態名の確認
    DEBUG && assert(this.status_ === 'dragging');

    // 状態名を設定
    this.status_ = 'dragover';

    // 状態 dragover が満たすべき様々な条件を設定
    this.element_.style.background = 'red';
    this.element_.style.border= 'blue 1px solid';
    this.element_.style.boxSizing = 'border-box';
};
エラーメッセージを見たら、即座にブレークポイントの条件を考える

alert デバッグ、 console.log デバッグ、 debugger デバッグなどのソースコードを直接書き換えるデバッグはやめましょう。
で、もっぱら「条件付ブレークポイント」を使います。
条件付ブレークポイントは、 FirebugWebkit で出来る以下のようなやつです。

JavaScript でよく見るエラーから、「ブレークポイント」の条件を推測します。

熟練された JSer はエラー名からブレークポイントの条件を即座に答えられる。

たとえば、

hoge.js の 121 行目で「TypeError: Cannot read property 'firstChild' of null」というエラーが出たら

hoge.js の 121 行目を見る
そして、 firstChild というプロパティを読んでいる変数を探す。
で、以下のようになっていたとする。

121: return nodeA.firstChild || nodeB.firstChild;

と、するとここの行で使うべきブレークポイントの条件は「nodeA === null || nodeB === null」ですね。

そんな感じです。
「条件付じゃないブレークポイント」を使ってしまうと、不具合じゃないケースでも止まってしまってわずらわしいので「条件付ブレークポイントを使いましょう」

パフォーマンスチューニングをどうするか

パフォーマンスチューニングに関しては以下のことを気をつけました。

  • 最初から細かいチューニングに気を使うな
  • 決定的に遅くなることはやらない
  • 体感速度の改善を先にやる
  • パフォーマンスチューニングはピンポイントで
  • Google Closure Compiler を使う
最初から細かいチューニングに気を使うな

最初から細かいチューニングに気を使う必要はありません。
細かいチューニングは、あとからなんとでもなります。

決定的に遅くなることはやらない

とは言っても、ある程度の気遣いは必要です。
「DOM を全部走査しないと実現できない機能」などのように明らかに遅くならざるを得ないようなことは、最初からやってはいけません。

体感速度の改善を先にやる

ユーザーフィードバックが足りてない場合や、過度にアニメーションを使うと体感的に遅く感じます。
先にそこから手をつけましょう。

パフォーマンスチューニングはピンポイントで

実行時間の大半は、ごく一部の箇所で浪費されていることが多いです。
なので、パフォーマンスチューニングはピンポイントで効率的にやりましょう。
JavaScript のコードのパフォーマンスチューニングには、 FirebugWebkit のプロファイラを使いましょう。

また、コードが原因とも限りません。
サーバーのレスポンス時間を改善したり、サーバーへ XMLHttpRequest を投げる粒度を調整したりいろいろやることはあります。
全体を俯瞰するには WebKit の開発者ツールの timeline が便利です

Google Closure Compiler を使う

Google Closure Compiler を使うと、まったく使われていない無駄なプロパティへのアクセスとかも消してくれたりするので便利です。

まとめ

と、いうわけでサイボウズ・ラボ、サイボウズの皆様、本当にいろいろなことを勉強させていただき、いろいろなわがままを聞いてくださり、ありがとうございます。
このような会社に勤められたことを僕は誇りに思っています。
また、僕の書いたコードを引き継いでくれた @yo_waka id:ama-ch ありがとう。
そして、このブログを読んでくれた皆様、サイボウズJavaScript を書いてみませんか?
ではでは、また id:amachang の次回作にご期待ください。

みんな!またね!