SocialCalc
スプレッドシートは 30 年以上の歴史を持ちます。最初のスプレッドシートプログラム VisiCalc は Dan Bricklin によって 1978 年に構想され、1979 年に完成しました。根本的な考え方はとても単純です: 二次元に無限に広がる表があり、各マス (セル) にテキスト・数・式が入るというものです。式では基本的な算術演算やたくさんのビルトイン関数が利用でき、他のセル要素を値として参照できます。
スプレッドシートの考え方は単純ですが、応用は多岐にわたります: 会計、物品管理、リスト管理などはその一例です。その可能性は事実上無限であり、VisiCalc は個人用コンピューターにおける最初の「キラーアプリ」となりました。
スプレッドシートはそれから数十年の間に登場した Lotus 1-2-3 や Excel といったソフトウェアで段階的に改善されましたが、その中心的な考え方は変わりません。スプレッドシートはディスクにファイルとして保存され、編集のときにメモリに読み込まれます。このファイルベースのモデルでは、協調動作が非常に難しくなります:
-
全てのユーザーがスプレッドシートエディタをインストールする必要がある。
-
電子メールでのやり取り、共有フォルダ上での利用、バージョン管理システムによるトラッキングに個別のオーバーヘッドがかかる。
-
変更履歴が制限される: 例えば Excel はセルのコメントや書式に関する履歴を保存しない。
-
テンプレートの書式や式を更新すると、そのテンプレートを使う既存ファイルが大きく変更されてしまう。
幸いにも、エレガントな単純性を持ってこういった問題を解決する wiki という新たな協調モデルが生まれました。wiki は Ward Cunningham によって 1994 年に開発され、2000 年代初頭に Wikipedia によって広まりました。
wiki モデルはファイルではなくサーバーでホストされるページを扱います。ページは特別なソフトウェアをインストールせずともブラウザから編集が可能であり、ハイパーテキストを使って互いをリンクしたり、他のページの一部分を取ってきて大きなページを作ることができます。利用者はデフォルトで最新のバージョンを閲覧、編集し、編集履歴はサーバーによって自動的に管理されます。
wiki モデルに影響されて、Dan Bricklin は WikiCalc の開発を 2005 年に開始しました。WikiCalc の目標は、wiki が持つ複数人編集の簡単さと、スプレッドシートが持つなじみ深い視覚的な書式と計算の考え方を両立することです。
WikiCalc
WikiCalc の最初のバージョン (図 19.1) には、当時の他のスプレッドシートとは一線を画す機能がいくつかありました:
-
テキストデータとしてプレーンテキスト・HTML・wiki スタイルのマークアップを使うことができ、レンダリングも行える。
-
wiki スタイルのテキストではリンク・画像・他のセルの値の参照を利用できる。
-
式の入ったセルは他のウェブサイトでホストされる WikiCalc ページの値を参照できる。
-
他のウェブページに埋め込むためのデータを生成できる (動的・静的データの両方)。
-
セルの書式は CSS の属性とクラスにアクセスできる。
-
全ての編集操作を完全に記録できる。
-
wiki と同様にページの全てのバージョンを保存でき、ロールバックも行える。
WikiCalc 1.0 の内部アーキテクチャ (図 19.2) と情報フロー (図 19.3) は単純でありならもパワフルであるよう注意深く設計されています。中でも特に有用なのが、マスターのスプレッドシートを小さなスプレッドシートに分割する機能です。例えば営業担当の社員が自身の成績をスプレッドシートのページに打ち込み、営業部長が部下の成績を地域スプレッドシートにまとめ、VP がそれをさらにトップレベルのスプレッドシートにまとめる、といった使い方ができます。
あるスプレッドシートが更新されると、そのシートを参照する全てのシートが更新されます。詳細が知りたい場合には、何回かクリックすれば更新があったスプレッドシートを確認できます。この機能のおかげで、数字の更新を複数の場所に打ち込むという冗長でミスの起こりがちな作業は必要なくなり、さらに目にする全ての情報が最新のものであることが保証されます。
再計算が最新のデータを使うことを保証するために、WikiCalc は全ての状態データをサーバーに保存する薄いクライアント (thin-client) デザインを採用しています。スプレッドシートはブラウザ上で <table>
要素として表されます。セルを編集すると ajaxsetcell
の呼び出しがサーバーに送信され、これを受けてサーバーはブラウザにどのセルを更新するかを返します。
当たり前ではあるのですが、このデザインはブラウザとサーバーの間の高速な接続を仮定しています。レイテンシが高い環境では、ユーザーはセルの値の更新が反映されるまでの間に “Loading...” というメッセージを頻繁に目にすることになります (図 19.4)。入力をいじってインタラクティブに式を編集するような、結果がリアルタイムに見える必要のある使い方をするユーザーにとって、これは特に問題です。
さらに <table>
要素がスプレッドシートと同じ次元を持つので、100×100 のグリッドは 10,000 個もの <td>
DOM オブジェクトを生成します。これはブラウザのメモリリソースを食いつぶし、ページのサイズがさらに制限されます。
こういった欠点があったので、WikiCalc は localhost で動作するスタンドアローンのサーバーとしては便利だったものの、ウェブベースのコンテンツ管理システムとしてページに埋め込むという使い方はあまり実用的ではありませんでした。
2006 年、Dan Bricklin は Socialtext とチームを結成し、SocialCalc の開発を開始しました。SocialCalc は WikiCalc を JavaScript で根本から書き直したものであり、一部はオリジナルの Perl コードを元にしています。
この書き直しでは地理的に離れた大規模なユーザーの協調が目標とされ、デスクトップアプリケーションと変わらない見た目と操作感が追求されました。その他の設計目標を次に示します:
-
数十万のセルの処理。
-
編集操作の応答時間の短縮。
-
クライアントサイドの履歴と undo/redo スタック。
-
JavaScript と CSS を使った本格的なレイアウト。
-
クロスブラウザのサポートとレスポンシブ JavaScript の積極的な利用の両立。
三年の開発期間と数回のベータリリースを経て、Socialtext はこの設計目標を満たした SocialCalc 1.0 を 2009 年にリリースしました。SocialCalc システムのアーキテクチャを見ていきましょう。
SocialCalc
図 19.5 と 図 19.6 に SocialCalc のインターフェースとクラスをそれぞれ示します。WikiCalc と比べると、サーバーの役割は大きく削減されています。サーバーの唯一の役目は HTTP GET を受け取ったときに save フォーマットのスプレッドシートを送り返すことだけです。ブラウザがデータを受け取ると、それからの更新の検出や計算、およびユーザーとの対話は全て JavaScript で実行されます。
JavaScript 要素は層化 MVC (layered Model/View/Controller) スタイルで設計されており、それぞれのクラスが一つのことだけに注力します:
-
Sheet はデータモデルであり、スプレッドシートのメモリ上におけるデータ構造を表す。例えばセルを表す
Cell
オブジェクトを座標から引く辞書を持つ。空のセルはエントリーを必要とせず、メモリを全く消費しない。 -
Cell はセルの要素と書式を表す。表 19.1 に主なプロパティを示す。
-
RenderContext はビューを実装する。シートを DOM オブジェクトにレンダリングするのが役目である。
-
TableControl はメインのコントローラーであり、マウスとキーボードからのイベントを受け付ける。スクロールやリサイズといったイベントも受け取ったときには RenderContext を更新し、シートの要素に影響する更新イベントを受け取ったときにはシートのコマンドキューに新しいコマンドを送る。
-
SpreadSheetControl はトップレベルの UI であり、ツールバー、ステータスバー、ダイアログボックス、カラーピッカーなどを提供する。
-
SpreadSheetViewer はもう一つのトップレベル UI であり、読み込み専用のインタラクティブなビューを提供する。
属性 | 値 |
---|---|
datatype |
t |
datavalue |
1Q84 |
color |
black |
bgcolor |
white |
font |
italic bold 12pt Ubuntu |
comment |
Ichi-Kyu-Hachi-Yon |
クラスの利用を最小限にしたオブジェクトシステムを採用しており、合成と委譲は単純で、オブジェクトのプロトタイプや継承は一切使っていません。シンボルは全て SocialCalc.*
に配置され、名前の衝突を避けるようになっています。
シートの更新は全て ScheduleSheetCommands
メソッドを経由します。このメソッドは実行する編集を表すコマンド文字列を入力として受け取ります (例を 表 19.2 に示します)。SocialCalc を埋め込むアプリケーションから独自のコマンドを追加することも可能であり、そのときには SocialCalc.SheetCommandInfo.CmdExtensionCallbacks
オブジェクトに名前を付けたコールバック関数を追加して startcmdextension
で呼びます。
コマンドの例 |
---|
set sheet defaultcolor blue |
set A width 100 |
set A1 value n 42 |
set A2 text t Hello |
set A3 formula A1*2 |
set A4 empty |
set A5 bgcolor green |
merge A1:B2 |
unmerge A1 |
erase A2 |
cut A3 |
paste A4 |
copy A5 |
sort A1:B9 A up B down |
name define Foo A1:A5 |
name desc Foo Used in formulas like SUM(Foo) |
name delete Foo |
startcmdextension UserDefined args |
コマンド実行ループ
動作をレスポンシブにするために、SocialCalc はセルの値の再計算と DOM の更新をバックグラウンドで行います。このためユーザーが複数のセルを立て続けに変更したとしても、エンジンがコマンドキューを使って変更を順番通りに処理します。
コマンドが実行されている間、TableEditor
オブジェクトは busy
フラグを true にします。この間その他のコマンドは deferredCommands
キューにプッシュされ、実行の順番が保たれるようになります。図 19.7 中のイベントループが示すように、Sheet オブジェクトは StatusCallback
イベントを事あるごとに送って現在コマンド実行のどの段階にあるかをユーザーに伝えます。コマンドの実行には四つのステップがあります:
-
ExecuteCommand: 開始時に
cmdstart
を送り、実行が終わったらcmdend
を送る。もしコマンドがセルの値を間接的に変更したなら、Recalc
ステップに入る。そうでない場合、もしコマンドがスクリーン上にあるセルの見た目を変更したなら、Render
ステップに入る。このどちらでもない場合 (例えばcopy
コマンドを実行したとき) には、PositionCalculations ステップまで実行を進める。 -
Recalc (必要なら): 開始時に
calcstart
を送り、依存するセルをチェックしている間は 100 ms 秒ごとにcalcorder
を送る。このチェックが終わったらcalccheckdone
を送り、影響を受けた全てのセルが再計算された値を受け取ったらcalcfinished
を送る。この後には必ず Render ステップを実行する。 -
Render (必要なら): 開始時に
schedrender
を送り、<table>
要素内のセルの書式を更新したらrenderdone
を送る。この後には必ず PositionCalculations ステップを実行する。 -
PositionCalculations: 開始時に
schedposcalc
を送る。スクロールバー、現在の編集中セルカーソル、TableEditor
の表示要素を更新し終わったらdoneposcalc
を送る。
全てのコマンドは実行時に保存されるので、操作の完全なログが自然に手に入ります。Sheet.CreateAuditString
メソッドを使えばコマンドが一行ごとに含まれる改行で分割された履歴文字列を得られます。
ExecuteSheetCommand
は実行するコマンドごとに undo コマンドを生成します。例えば A1 に “Foo” が入っている状況でユーザーが set A1 text Bar
を実行すると、undo コマンド set A1 text Foo
が undo スタックにプッシュされます。ユーザーが Undo をクリックするとこの undo コマンドが実行され、A1 の値が元の値に戻ります。
表エディタ
続いて TableEditor
レイヤーを見ていきます。このレイヤーは RenderContext
のスクリーン上の座標を計算し、垂直および水平のスクロールバーを二つの TableControl
インスタンスを使って管理します。
RenderContext
クラスによって管理されるビューレイヤーも WikiCalc の設計と異なっています。各セルを <td>
要素に結び付けるのではなく、ブラウザの表示領域に収まる固定サイズの <table>
を最初に作り、前もって <td>
要素を詰めておくという設計になっています。
ユーザーが特別に描画されたスクロールバーを使ってスプレッドシートをスクロールすると、SocialCalc は最初に詰めておいた <td>
要素の innerHTML
を更新します。こうすると多くの場合 <tr>
要素や <td>
要素を生成・削除しなくて済むので、反応速度が大きく向上します。
RenderContext
は見えている領域だけをレンダリングするので、Sheet
オブジェクトのサイズを任意に大きくしてもパフォーマンスは低下しません。
TableEditor
には CellHandles
オブジェクトも含まれます。これは現在編集中のセル (current editable cell, ECell) の右下にある放射状のメニューで、fill/mode/slide の操作が行えます (図 19.9)。
入力ボックスは二つのクラスが管理します: InputBox
と InputEcho
です。InputBox
は格子の上にある編集行を管理し、InputEcho
は ECell に被さってタイプと同時に更新されるプレビューレイヤーを管理します (図 19.10)。
通常、SocialCalc エンジンがサーバーと通信をするのは編集するスプレッドシートを開くときと、編集が終わったスプレッドシートをサーバーに保存するときだけです。このときには Sheet.CreateSheetSave
メソッドが save フォーマットの文字列を Sheet
オブジェクトにパースし、Sheet.CreateSheetSave
メソッドが Sheet
オブジェクトを save フォーマットに変換します。
セルの式では URL を使って任意のリモートスプレッドシートの値を参照できます。recalc
コマンドは参照されている外部スプレッドシートを再フェッチし、Sheet.ParseSheetSave
を使ってもう一度パースします。このときキャッシュにパース結果を保存して、同じリモートスプレッドを参照する他のセルがあった場合にフェッチせずに値を読めるようにします。
save フォーマット
SocialCalc が使う save フォーマットは標準的な MIME (multipart/mixed
) フォーマットであり、四つの text/plain; charset=UTF-8
パートを持ちます。各パートは改行で区切られたテキストであり、フィールドはコロンで区切られます。四つのパートは次の通りです:
-
meta
パートは他のパートのタイプを並べる。 -
sheet
パートには各セルの書式と要素、行の幅 (デフォルトの値でない場合)、シートのデフォルトの書式、その後にフォントのリスト、シートで使われる色とボーダーを並べる。 -
edit
パートはTableEditor
の編集状態を保存する (省略可能)。例えば ECell の最後の場所や行/列ペインの固定された幅などが保存される。 -
audit
パートには前回の編集セッションで実行されたコマンドの履歴が保存される (省略可能)。
例えば 図 19.11 のスプレッドシートには三つのセルがあり、A1 には 1874
が、A2 には 2^2*43
という式が、A3 には SUM(Foo)
という式があり、A1 が ECell で、A3 は太字でレンダリングされています。また A3 は Foo
と呼ばれる区間 A1:A2
を参照しています。
このスプレッドシートをシリアライズした save フォーマットは次のようになります:
socialcalc:version:1.0
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary=SocialCalcSpreadsheetControlSave
--SocialCalcSpreadsheetControlSave
Content-type: text/plain; charset=UTF-8
# SocialCalc Spreadsheet Control Save
version:1.0
part:sheet
part:edit
part:audit
--SocialCalcSpreadsheetControlSave
Content-type: text/plain; charset=UTF-8
version:1.5
cell:A1:v:1874
cell:A2:vtf:n:172:2^2*43
cell:A3:vtf:n:2046:SUM(Foo):f:1
sheet:c:1:r:3
font:1:normal bold * *
name:FOO::A1\cA2
--SocialCalcSpreadsheetControlSave
Content-type: text/plain; charset=UTF-8
version:1.0
rowpane:0:1:14
colpane:0:1:16
ecell:A1
--SocialCalcSpreadsheetControlSave
Content-type: text/plain; charset=UTF-8
set A1 value n 1874
set A2 formula 2^2*43
name define Foo A1:A2
set A3 formula SUM(Foo)
--SocialCalcSpreadsheetControlSave--
save フォーマットは人間が読めるように設計されており、プログラムで生成するのも比較的簡単です。このため例えば Drupal による Sheetnode プラグインを使えば、PHP を使ってこのフォーマットを Excel (.xls
) や OpenDocument (.ods
) といったよく使われるフォーマットへ変換できます。
SocialCalc の各部分がどう結び付くか理解できたと思うので、次は現実世界の例において SocialCalc がどのように拡張されているかを見ていきます。
リッチテキスト編集
最初の例は SocialCalc のテキストセルを wiki のマークアップ言語で拡張して、表エディタでリッチテキストをレンダリングするものです (図 19.12)。
この機能はバージョン 1.0 がリリースしてすぐに追加されました。画像やリンク、テキストのマークアップを統一した記法で利用したいというリクエストが多くあったためです。当時 Socialtext は既にオープンソースの wiki プラットフォームだったので、wiki の記法を SocialCalc でも使うのが自然でした。
これを実装するために、私たちは text-wiki
という textvalueformat
に対するカスタムレンダラを実装し、テキストセルのデフォルトフォーマットを変更しました。
textvalueformat
が何かって? 読み進めてください。
タイプとフォーマット
SocialCalc のセルは datatype
と valuetype
を持ちます。例えばテキストあるいは数値の入ったデータセルはそれぞれ text と numeric という valuetype
を持ち、datatype="f"
である式セルは numeric あるいは text の値を生成します。
Render ステップにおいて Sheet
オブジェクトがセルから HTML を生成していたことを思い出してください。このとき Sheet
はセルの valuetype
を調べます。もし valuetype
が t
で始まるなら、セルの textvalueformat
属性を使って HTML の生成方法を決めます。もし n
で始まるなら、代わりにセルの nontextvalueformat
を使います。
しかしセルの textvalueformat
と nontextvalueformat
がどちらも設定されていない場合には、セルの valuetype
からデフォルトのフォーマットが検索されます。この様子を 図 19.13 に示します。
value format text-wiki
に対する処理は SocialCalc.format_text_for_display
に埋め込まれています:
if (SocialCalc.Callbacks.expand_wiki && /^text-wiki/.test(valueformat)) {
// wiki マークアップを行う
displayvalue = SocialCalc.Callbacks.expand_wiki(
displayvalue, sheetobj, linkstyle, valueformat
);
}
ここでは wiki 記法を HTML に変換する処理を format_text_for_display
にインライン化することはせずに、新しいフックを SocialCalc.Callbacks
に定義しています。これは SocialCalc のコードベースで推奨されるスタイルであり、様々な方法で wikitext を拡張できるようにしながらも、この機能を必要としない埋め込み用途での利用も可能にしています。
wikitext のレンダリング
次に使うのは wikitext と HTML の間の相互変換を提供する JavaScript ライブラリ Wikiwyg1 です。
セルのテキストを受け取ってそれを Wikiwyg の wikitext パーサーと HTML エミッターに渡す関数 expand_wiki
を定義します:
var parser = new Document.Parser.Wikitext();
var emitter = new Document.Emitter.HTML();
SocialCalc.Callbacks.expand_wiki = function(val) {
// val を wikitext から HTML に変換する
return parser.parse(val, emitter);
}
最後のステップとして、set sheet defaulttextvalueformat text-wiki
コマンドをスプレッドシートの初期化の直後にスケジュールします:
// DOM に <div id="tableeditor"/> があることを仮定している
var spreadsheet = new SocialCalc.SpreadsheetControl();
spreadsheet.InitializeSpreadsheetControl("tableeditor", 0, 0, 0);
spreadsheet.ExecuteCommand('set sheet defaulttextvalueformat text-wiki');
以上をまとめると、図 19.14 に示す Render ステップとなります。
これで完了です! この拡張によって、wiki マークアップシンタックスによる様々なリッチテキストが SocialCalc で利用可能になります:
*bold* _italic_ `monospace` {{unformatted}}
> indented text
* unordered list
# ordered list
"Hyperlink with label"<http://softwaregarden.com/>
{image: http://www.socialtext.com/images/logo.png}
A1 に *bold* _italic_ `monospace`
と入力すれば、図 19.15 のようにレンダリングされたリッチテキストが表示されます。
リアルタイム共同編集
次に見る例は、共有スプレッドシートの複数人によるリアルタイム編集です。一見すると複雑に見えるこの問題も、SocialCalc のモジュラーな設計のおかげで、オンラインのユーザー全員に他のユーザーのコマンドをブロードキャストする処理を追加するだけで済みます。
ローカルのコマンドとリモートのコマンドを区別するために、ScheduleSheetCommands
メソッドに isRemote
パラメータを追加します:
SocialCalc.ScheduleSheetCommands = function(sheet, cmdstr, saveundo, isRemote) {
if (SocialCalc.Callbacks.broadcast && !isRemote) {
SocialCalc.Callbacks.broadcast('execute', {
cmdstr: cmdstr, saveundo: saveundo
});
}
// ...元々の ScheduleSheetCommands のコードが続く...
}
こうすれば後はコールバック関数 SocialCalc.Callbacks.broadcast
を実装するだけです。この関数が動作すれば、あるユーザーが実行するコマンドが同じスプレッドシートに接続している全てのユーザーにブロードキャストされます。
この機能は OLPC (One Laptop Per Child2) 向けに SEETA の Sugar Labs3 で 2009 年に最初に実装されました。broadcast
関数は OLPC/Sugar ネットワークにおける標準的な転送方式である D-Bus/Telepathy への XPCOM 呼び出しを使っています (図 19.16)。
これで同一 Sugar ネットワーク内にある複数の XO インスタンスが共通の SocialCalc スプレッドシートを同時編集できるようになり、上手く行きます。しかしこの方法は、Mozilla/XPCOM というブラウザプラットフォームおよび D-Bus/Telepathy メッセージングプラットフォームに依存しています。
ブラウザ間の転送
ブラウザと OS をまたいだ動作を可能にするために、私たちは Web::Hippie
4 を利用しました。このライブラリは WebSocket を通した JSON 通信を抽象化するもので、便利な jQuery バインディングと WebSocket が使えない場合の MXHR (Multipart XML HTTP Request5) へのフォールバックを持ちます。
WebSocket がサポートされていなくても Adobe Flash プラグインがインストールされていれば、web_socket.js
6 プロジェクトが提供する Flash を使った WebSocket エミュレーションを利用できます。このエミュレーションは多くの場合 MXHR よりも高速で高い信頼性を持ちます。この様子を 図 19.17 に示します。
クライアントサイドの SocialCalc.Callbacks.broadcast
は次のように定義されます:
var hpipe = new Hippie.Pipe();
SocialCalc.Callbacks.broadcast = function(type, data) {
hpipe.send({ type: type, data: data });
};
$(hpipe).bind("message.execute", function (e, d) {
var sheet = SocialCalc.CurrentSpreadsheetControlObject.context.sheetobj;
sheet.ScheduleSheetCommands(
d.data.cmdstr, d.data.saveundo, true // isRemote = true
);
break;
});
これは非常に上手く動作しますが、まだ解決すべき問題が二つあります。
衝突の解決
最初の問題はコマンドの実行順序に関する競合状態です: ユーザー A とユーザー B が同じセルを書き換える操作を同時に行ってその後コマンドを互いにブロードキャストすると、最終的な二人のスプレッドシートの状態が同じになりません (図 19.18)。
この問題は SocialCalc に組み込みの undo/redo の仕組みを使って解決します。図 19.19 にこの様子を示します。
衝突は次のように解決されます。クライアントがコマンドをブロードキャストするとき、まずコマンドを Pending キューに追加します。そしてクライアントがリモートコマンドを受け取ったときには、そのコマンドが Pending キューにあるかどうかをチェックします。
もし Pending キューが空なら、そのコマンドはリモート操作としてそのまま実行します。もしリモートコマンドと全く同じコマンドが Pending キューにあるなら、ローカルのコマンドをキューから取り除きます。
それ以外の場合には、クライアントはキューにあるコマンドが受信したコマンドと衝突するかを調べ、もし衝突するコマンドがあった場合にはその Undo
処理を行い、後で Redo
するために印を付けておきます。衝突するコマンドを全て Undo
し終わったら、受け取ったリモートコマンドを通常通り実行します。
その後 redo の印の付いたのと同じコマンドがサーバーから送られてきた場合には、クライアントはそれをもう一度実行し、キューからコマンドを取り除きます。
リモートカーソル
競合状態が解決されても、ユーザーが編集中のセルに誤って書き込んでしまうという事態を防ぐにはまだ不完全です。簡単にできる改善の一つに、カーソルの位置を他のユーザーにブロードキャストし、編集しているセルが見えるようにするというのがあります。
このアイデアを実装するには、MoveECellCallback
イベントにも broadcast
を追加します:
editor.MoveECellCallback.broadcast = function(e) {
hpipe.send({
type: 'ecell',
data: e.ecell.coord
});
};
$(hpipe).bind("message.ecell", function (e, d) {
var cr = SocialCalc.coordToCr(d.data);
var cell = SocialCalc.GetEditorCellElement(editor, cr.row, cr.col);
// ...リモートユーザーに応じてセルに色を付けるなどの処理...
});
他ユーザーのセルをスプレッドシート内で目立たせるために、セルを色付きの枠で囲うことがよくあります。しかし、セルには既に border
プロパティが定義されている可能性があり、その場合 border
は単色なので一つのカーソルしか表すことができません。
そのため CSS3 をサポートするブラウザでは、box-shadow
を使って同じセルにある複数のピアカーソルを表現するようになっています:
/* 同じセルにある二つのカーソル */
box-shadow: inset 0 0 0 4px red, inset 0 0 0 2px green;
教訓
私たちが SocialCalc 1.0 をリリースしたのは 2009 年 10 月 19 日であり、最初の VisiCalc がリリースされてからちょうど 30 年後です。Dan Bricklin の指導の下 Socialtext 社で同僚と関わった経験は私にとって非常に価値のあるものでした。そのときに学んだ教訓をいくつかここで共有します。
主任設計者は明確なビジョンを
[Bro10] で Fred Brooks は、複雑なシステムを構築するときには、首尾一貫した設計コンセプトに集中すると (派生した表現を使ったときに比べて) 会話がずっと直接的になると主張しました。Brooks によると、そのような首尾一貫した設計コンセプトは一人の人間の頭にあるのが一番だと言います:
整合したコンセプトを持つことが偉大な設計の一番重要な要素であり、そしてそれは一人ないし数人の頭脳が 心を一つする (uni animo) ときに達成されるものであるから、賢明なマネージャーは設計のタスクを才能ある主任設計者に一任する。
SocialCalc の場合には、主任ユーザーエクスペリエンスデザイナの Tracy Ruggles がプロジェクトのビジョンを共通化する上での鍵でした。内部の SocialCalc エンジンは非常に柔軟なので機能を好き勝手に追加してしまう誘惑はとても大きかったのですが、そんな中で Tracy の設計スケッチを使ったコミュニケーションはユーザーにとって直感的な機能の提示方法を模索する上で大きな助けとなりました。
プロジェクトの継続性のために wiki を
SocialCalc プロジェクトに私が加入した時点で、設計と開発は二年に渡って続いていました。にもかかわらず私は一週間もしないうちに必要な知識を得て、コントリビューションを開始できました。これが可能だったのは全てが wiki に書いてあるからです。最初期の設計ノートから最新のブラウザサポートマトリクスに至るまで、開発プロセスの全てが wiki ページと SocialCalc スプレッドシートにまとめられていました。
プロジェクトのワークスペースを読み漁ることで他人と同じステージに立つことができ、新しいチームメンバーに付き物の細かい指導は必要ありませんでした。
議論が IRC やメーリングリストで行われ、wiki が (存在したとしても) ドキュメントや開発リソースへのリンクにしか使われない伝統的なオープンソースプロジェクトではこれは不可能だったでしょう。構造化されていない IRC のログやメールアーカイブから文脈を読み取るのは新参者にとって非常に困難です。
タイムゾーンの違いを受け入れる
Ruby on Rails の作者 David Heinemeier Hansson は、地理的に離れたメンバーからなるチームの利点に 37signals に加わってすぐ気付いたそうです: 「コペンハーゲンとシカゴの間にタイムゾーンが 7 つあるおかげで、互いに干渉せずにたくさんの仕事を片付けることができる」 SocialCalc の場合は台北とパロアルトの間の 9 つのタイムゾーンでしたが、私たちも同様の経験をしました。
私たちは設計-開発-QA というフィードバックサイクルを基本的に 24 時間で回し、それぞれの段階は現地時間昼間の 8 時間で行われました。この非同期的な労働スタイルは分かりやすいアウトプット (設計スケッチ、コード、テスト) につながり、互いの信頼関係を大きく向上させました。
楽しさを最適化する
2006 年の CONISLI カンファレンスのキーノート [Tan06] で、私は地理的に離れたメンバーからなるチームで Perl 6 言語を実装したときの経験をまとめました。そこで触れた「ロードマップを必ず持て」 「許しを請う > 許可を求める」 「デッドロックを排除せよ」 「探すのはアイデアであって合意ではない」 「アイデアをコードでスケッチせよ」といった観察は、規模の小さいチームであっても当てはまります。
SocialCalc の開発において私たちは、コードに触れる可能性のあるチームメンバーとの知識の共有に細心の注意を払い、重大なボトルネックになってしまうメンバーが出ないようにしました。
さらに意見が衝突したときには、複数の選択肢を実際にコードして設計空間を探り、前もって衝突を解決しました。より良い設計が出て来たときには現在動作しているプロトタイプを置き換えることも躊躇しませんでした。
こういった文化的な特徴は物理的に顔を合わせることない中で開発の見通しを明確化し、チームの結束を高めるのに一役買いました。ルールは最小限で済み、SocialCalc に取り組むのはとても楽しいものになりました。
ストーリーテスト駆動開発
Socialtext に加わる前の私は「テストと仕様に交互に取り組む」アプローチが良いと思っていました。例えば Perl 6 の仕様書7のように、言語仕様に公式のテストスイートを付けるというやり方です。しかし SocialCalc の QA チームの Ken Pier と Matt Heusser は、私の考えを次のレベルへと開眼させました。テストを実行可能な仕様として書く、というやり方です。
[GR09] の第十六章で、Matt はストーリーテスト駆動開発について次のように説明しています:
作業の基本単位は「ストーリー」である。ストーリーとは要件を記した極端に短いドキュメントであり、機能の簡単な説明と共に完了したときに起こるべきことの例を記す。「受け入れテスト (acceptance test)」と呼ばれるこの例は平易な英語で記述する。
ストーリーの初稿はプロダクトオーナーが受け入れテストを誠実に書くよう努力する。開発者とテスターがこれを補強し、それから初めてコードを書く。
ストーリーテストはその後 Ward Cunningham の FIT フレームワーク8に影響を受けたテーブルベースの仕様記述言語 wikitests に翻訳され、Test::WWW::Mechanize
9 や Test::WWW::Selenium
10 を使ってテストされます。
要件を表現・検証するための共通言語としてストーリーテストを使うことの効果は計り知れません。要件の取り違えを減らし、月例のリリースからは回帰テストがほとんど消えました。
CPAL を使ったオープンソース
忘れてはいけないのが、SocialCalc のオープンソースモデルから得られた教訓です。
Socialtext は SocialCalc のために Common Public Attribution License11 (CPAL) を作成しました。CPAL は Mozilla Public License に基づいており、ソフトウェアのユーザーインターフェースに原作者の名前を表示することを強制できるように設計されています。またネットワークを通じた利用における条項として、派生作品がネットワーク越しにホストされたとしても同じライセンスで公開することを要求します。
Open Source initiative12 と Free Software Foundation13 に認証されてからは Facebook14 や Reddit15 といった巨大サイトでもプラットフォームのソースコードの公開に CPAL が採用され始め、大きな励みになりました。
CPAL は「弱いコピーレフト」のライセンスなので、他の自由ソフトウェアおよびプロプライエタリソフトウェアと組み合わせて使うことができ、そのときは SocialCalc に対する変更だけを公開するだけで済みます。これによって様々なコミュニティが SocialCalc を採用し、CPAL はより素晴らしいものになりました。
SocialCalc というオープンソースのスプレッドシートエンジンにはたくさんの可能性があります。もしあなたが気になるプロジェクトに SocialCalc を組み込む方法を見つけたら、ぜひ知らせてください。