設計判断とは何か?
ソフトウェアの開発においては、仕様を決める時に、並立できない複数の選択肢の中からどれか1つを選ばなければならないことがあります。特に設計に関わる場面での判断のことを、一般的に「設計判断」と言います。「この新機能を加えるのか、加えないのか」「この新形式をサポートするのか、しないのか」などのような、「やるか、やらないか」の判断はその典型です。
設計判断においては、常に「やる」という判断を下せるとは限りません。様々な事情から「やる」という選択肢を選べないこともありますし、あるいは何らかの明確な理由があって積極的に「やらない」という判断を下すこともあります。
つい最近も、Firefox用アドオン開発における自動テストを支援するテスティングフレームワーク「UxU」において、「Firefox 20以降ではE4Xを使ったテストケースをサポートしないことにする」という設計判断を下しました。これを例にとって、このエントリでは具体的な設計判断のプロセスを解説します。
発端
UxUには、テストケースの記述を容易にするためのユーティリティが多数実装されており、その中には、文字列として与えられたCSVを解釈したり加工したりするといった機能も含まれています1。これらの機能を使う際には、長い文字列をテストケース中に埋め込む形で記述する方法として、E4XのCDATAマーク区間をヒアドキュメント記法の代わりとして使用することを想定していました。
JavaScriptには現在の所、複数行にわたるテキストを素直に文字列リテラルとしてソースコード中に記述する方法がありません。この問題を解決するためにE4Xを使うことができます。E4Xは本来はJavaScript中にXMLの要素をリテラルとして記述するためのものですが、CDATAマーク区間の記法を利用すると複数行にわたるテキストを比較的素直にリテラルとして記述できます。そのため、以下のようにヒアドキュメント代わりに利用するという用法が、一部の開発者の間で知られていました。
var longText = <><![CDATA[
aaa bbb ccc
ddd eee fff
ggg hhh iii
]]></>.toString();
この用法は、配布物を1つのJavaScriptのファイルにまとめる必要があるユーザースクリプトなどにおいても重宝されているようです。
ところが、2013年にリリースされる予定のFirefox 20開発版において、E4Xを既定の状態で無効化するという決定が下りました(参照:788290 - Turn javascript.options.xml.chrome off by default)。このままFirefox 20がリリースされると仮定すると、UxU用に書かれたテストケースのうちE4Xを利用したテストケースはFirefox 20上では動作しません。
そこで、これを受けて、E4Xを使用して書かれたテストケースについてUxU側で何らかの対策を取ることができないかを検討することにしました。
テスティングフレームワークとして重視したい点
UxU用に書かれたテストケースを「UxUというアプリケーション専用のデータファイル」と捉えた場合、UxUが取るべき姿勢は「可能な限り互換性を保つ」「過去に作成されたデータファイルも、新しいバージョンで問題なく利用できるようにする」ということになります。この点について、疑問を差し挟む余地はないでしょう。
その方針に沿って考えると、今回のFirefoxの仕様変更に対して取り得るUxU側でのアプローチは、以下のようなものがあります。
-
Firefox(Thunderbird)のE4X機能を自動的に有効化するようにする。
-
E4Xを自前で実装する。
-
テストケースの読み込み時に、E4Xをヒアドキュメント代わりに使用している箇所を検出して、文字列リテラルに自動的に変換する。
それぞれのアプローチについて、メリットとデメリットを考えてみます。
Firefoxの隠し設定を変更するというアプローチの検討
前述のbugおよびコミットされたパッチを見るとわかりますが、今回の変更は、E4Xの有効無効を切り替える設定を、既定の状態でオフにしておくというだけのものです。この設定は隠し設定ではありますが、Firefox上で動作するアドオンから見た場合、通常の設定項目のひとつと何ら変わりありません。よって、UxU自身が自動的にこの隠し設定を変更して、E4Xを再度有効化するということは普通にできてしまいます。
このアプローチのメリットは、単にFirefox自体の設定を変更するだけなのでUxU側の負担が非常に小さく済む、という点です。
それに対して、この方法のデメリットは最低でも2つあります。1つは、テスト実行時の環境が「テスト対象のコードが実際のユーザーの環境」からかけ離れた環境になってしまうことです。そもそもUxUがFirefox上で動作するアドオンとして開発されているのは、実際の環境になるべく近い環境でテストを走らせたいという理由からです。テスト実行時に環境そのものを変更してしまうと、「実際の環境でどんな問題が起こりうるか」ということを確かめられなくなってしまいます。
もう1つのデメリットは、この方法が明日にでも利用できなくなってしまうリスクがあるという点です。今回はE4Xの有効無効を切り替える設定の初期値が変わっただけでしたが、E4Xのパーサー自体が削除されてしまった場合、この選択肢はとれなくなります。
E4Xを自前で実装するというアプローチの検討
次に、E4XそのものをUxU側で実装する場合を検討します。
現在、E4Xに対応したJavaScriptの処理系は、C++製のSpiderMonkeyとJava製のRhinoがあり、どちらもオープンソースのプロダクトとして公開されています。よって、極端なことを言えば、これらのJavaScript処理系を丸ごと抱え込んでしまうということも技術的には可能です。
ただ、抱え込むコードの量が増えるという事でもあるため、メンテナンスコストは明らかに増大します。Rhinoのソースコードは全体で20MB、SpiderMonkeyのソースコードは40MBにも及ぶ巨大なプロダクトですし、それらを組み込んで動作させるためのコードも相当な規模になることが予想されます。UxUはクリアコード内製の製品ですが、開発に大きなコストをかける余裕は残念ながらありません2。開発コストの増大は、プロジェクトそのものの存続を危うくしてしまいます。
また、この場合、必然的に、テスト対象のコードも「UxUが動作しているFirefoxのJavaScript実行エンジン」ではなく「テスト環境用にUxUが内蔵しているJavaScript実行エンジン」で走ることになります。これでは、テスト環境と実環境が大きくかけ離れてしまうという問題が残ってしまいます。
E4X(のCDATAマーク区間)の箇所を文字列リテラルに自動的に変換するというアプローチの検討
E4X全体に対応することは無理でも、E4Xをヒアドキュメント代わりに使っている箇所についてだけ対応することはできないか、という考え方もあります。100%完全にあらゆるケースに対応することは難しくとも、最小のコストで全体の8割程度の場合に対応できるようなやり方があるのであれば、それは有力な選択肢になり得ます。
このアプローチで鍵となるのは、E4Xがヒアドキュメントとして使われている箇所をどのようにして見つけるかです。その方法が十分に簡単なのであれば、この方法は検討に値すると言えます。
この方向でまず思いつくのは、スクリプト全体を文字列として扱った上で、E4Xの部分を正規表現で探して一括置換するという方法です。しかし、この方法には以下のようなスクリプトで「誤爆」が発生してしまうという問題があります。
var cdataLikeMatcher = /<![CDATA[]/;
if (array[object['property']]>10) { ... }
この問題を回避するためには、「<![CDATA[」という文字列がどのような文脈で登場しているのかを調べて、地の文として登場している場面だけを検出する必要があります。ですが、これは思ったほど簡単な事ではなく、JavaScriptにマクロ記法を導入するライブラリであるsweet.jsで採用されている字句解析レベルでの効率のよい手法を使ったとしても、それなりの規模のコードが必要となります。
「100%完全にあらゆるケースに対応することは難しくとも……」という観点でいえば、そこまでの事をせずとも、単純な文字列置換でできる場合についてはそれで動けばよしとして、動かない場合はE4Xの使用を諦めてスクリプトを書き直してもらう、というやり方もあるように思えるかもしれません。ですが、その場合に生じる「うまく動かないケース」には、E4Xをヒアドキュメント代わりに使っているケースだけではなく、先の例のような「それ自体は何の変哲もないスクリプトだが、たまたまE4Xに似ている文字列が登場する」ケースも含まれ得ます。本来使えないはずのE4XがE4Xとして使えないのは許容できるとしても、本来であれば何の問題も起こらないはずのスクリプトまでもが「E4X対策の影響」で動作しなくなってしまうのでは、本末転倒もいいところです。
前提を見直す
ここまでいくつかのアプローチを検討してきましたが、残念ながらどのアプローチにも問題があり、選ぶに選べない結果となってしまいました。
このように行き詰まってしまった時は、行き詰まりの原因になっている前提を疑ってみることも必要です。いくつかの選択肢を前にして、必ずしもその中からどれかを選ばなくてはならないということはありません。ある観点で見た時に解決策が見えなくても、視野を広げて別の観点から問題を捉え直すことで、解決策が見えてきたり、あるいは、問題が問題ではなくなってくることがあります。ここでは、「UxUでこの問題に対策を行わないといけない」という前提自体を疑ってみます。
そもそも今回の件の発端を考えると、問題提起の仕方は「E4Xをヒアドキュメント代わりに使っている部分がFirefox 20以降で動かなくなる。どうしよう。」というものでした。「E4Xの仕様がサポートされない」「E4Xで書かれた部分が動かない」という点は元々は問題視していなかったのです。
そして、ここでもう1つ大事なのが、ヒアドキュメントの記法があるか無いか・使えるか使えないかというのは、UxUに求められる機能というよりも、JavaScript一般で求められる機能であるという事です。
設計上の判断材料の1つとして、プロジェクトの責任範囲は常に頭に入れておく必要があります。いくら利便性が高まるといっても、本来の責任範囲を逸脱した部分に手を出し始めてしまうと、そのうち収拾がつかなくなってしまいます。今回はUxUのテストケースでのニーズが発端ではありましたが、「FirefoxやThunderbirdのアドオンの自動テストを支援する」というUxUプロジェクト本来の目的を考えると、これはいささか外れたトピックと言わざるを得ません。「Firefoxで動作するスクリプトはテストケース中でも全部利用できること」「UxUが提供するユーティリティやアサーションが正常に機能すること」といった事柄は本来の目的から演繹できる責任範囲ですが、「Firefoxが対応しなくなったJavaScriptの文法上の拡張仕様に対応すること」を演繹することは難しく、UxUの責任の範囲外にあると言えます。
結論
以上を踏まえて、最終的に、UxUプロジェクトはE4Xの事実上の廃止に対しては「特に何もしない」という判断を下すことにしました3。
この結論に至るまでに、以下の要素が判断基準として登場しました。
- 手間(コスト)はどれだけ必要か。
- プロジェクト本来の目的を疎外しないか。
- どれだけの場合に対応できるか。(100%すべての場合に対応できるのか、一部のエッジケースには対応できないのか。)
- 追加・変更が必要なコードの量はどの程度か4。
- プロジェクトの持続性にどれだけ影響するか。
- プロジェクトの責任範囲に含まれるか。
ある点を突き詰めると別の点で問題が増大するという風に、判断基準同士の間にトレードオフの関係がある場合、すべての基準を満足させる選択肢というものは無いことになります。その時に重要になるのが、判断基準同士の中での優先度です。そのプロジェクトが何を大事にしているのか・何を大事にしていくのかという軸がはっきりしていればいるほど、判断のぶれは小さくなりますし、素早い判断が可能になります。
また、目先の問題や、最初のよく調べていない時点で仮に設定した目標に囚われてしまうと、そのまま泥沼に突っ込んで戻って来られなくなってしまうことがあります。今回の事例で言えば、「E4Xの仕様を完全に満たさないといけない」という意識に囚われたばかりに、字句解析処理の実装に着手したものの、いつまで経っても完成に辿り着けず、そのうちプロジェクト自体が頓挫してしまう、という「最悪のケース」に陥ってしまう可能性も十分にありました。
自分が今進もうとしている道の先にあるのが底なし沼なのかどうなのかを早めに見極めて、これは駄目だとなったら深手を負う前に引き返すことが大切です。そのためにも、何を大事にするのかという判断基準が必要になります。「そこは大事にしなくてもよい」ということが分かっていれば、引き返す判断を早めに下しやすいですし、埋没費用の発生もなるべく小さく抑えられます。
最良の選択を最初の時点ですぐに行い、それに向かって無駄なく一直線で進むのが、理想的といえば理想的です。しかし、最初の時点ですべてを知り尽くせていない事はままあります。今回も、調べ始めてみるまで「ソースコードの中で、どこがE4XのCDATAマーク区間なのかを判別する」ということがそこまでコストの高い事であるとは分かっていませんでした。最初にすべての方針を決めてまっすぐ邁進することに比べると、このような進め方は一見、無駄が多く不格好かもしれません。しかし、一度に投入できるリソースに厳しい制限がある場合には、その時々の目標設定を随時見直してスピーディーに方針を転換していく方が、リスクやコストを平滑化できるのではないでしょうか。
もちろんその時には、ぶれない大目的が存在していることが前提にあります。大目的無しに方針転換を繰り返すのは、ただの迷走です。目標は、目的に向かって進むための途中の道しるべに過ぎません。今回の例で言えば、「アドオンのコードを実際の環境に近い環境で簡単にテストできるテスティングフレームワークを提供すること」が目的で、「E4XのCDATAマーク区間を使ったヒアドキュメント的な機能を利用できること」は(仮の)目標です。目的のためには目標は動かしてもいいものです。その逆に、目標のために目的が犠牲になって(目的がぶれて)はいけません。
皆さんもプロジェクトを運営される際には、目的をはっきりと持ち、それに則った設計判断を下すよう心がけましょう。
付録:ユーザ側での、あるいは別プロジェクトとしての解決の方向性
なお、UxUプロジェクトとしては「責任の範囲外なので、この件についてはなにもしない」という態度を取るとしても、実際にUxUを利用してテストケースを書くユーザー開発者としては、何らかの対策がないと困ります。そこで、UxUとは別のレイヤーでこの問題を解決する方法についても検討しました。
ヒアドキュメント風の機能を実現する別のアプローチとして、文法を拡張するのではなく、JavaScriptの文法の範囲内で実現するという考え方があります。その実例が、cho45さんがNode.js用に開発されているnode-hereというライブラリです。
このライブラリは「ライブラリが提供するhere()
という関数が呼び出された時のスタックトレースを参照して、ソース中の何文字目にあたる箇所で関数が呼ばれたのかを特定し、ソースを文字列として読み込んだ上でその箇所に書かれたコメントの内容を抽出して、それを文字列として返す」という処理を行うものです。スクリプトが実行される時点では純粋に単なる関数呼び出しでしかないため、これのせいで普通のスクリプトが動作しなくなるといった副作用もありません。Node.js(V8)用に開発されていますが、動作だけを見ればFirefox上でも同じようなことができるはずです5。
調査の結果、このアプローチにも限界があることは分かっています6が、実用上はほぼ問題ないと考えられます7。
今回はUxUプロジェクトとしての対応は見送りましたが、このような機能が十分に小さなライブラリとして実装されるならば、標準添付のライブラリの1つとして含めるという選択もあり得るかもしれません8。
何かの機能を廃止したり、今まではできたことをできなくしたりすると、ユーザーからは必ず反発があります。有効な代替案がないままにただ「できなくなりました」とだけ言い放ってしまうと、ユーザーの不興を買ってしまい、ユーザー離れを引き起こしたり、古いバージョンを使い続けられてしまったりする事もあります9。「なぜできなくなったのか」という事を説明したとしても、この点は変わりません10。ですのでこのような場合には、ユーザーの負担を減らすためにも、何らかの形で有効な代替案を用意しておくか、案内をしておくのが望ましいでしょう。
-
主に、データ駆動テストで利用することを想定しています。 ↩
-
UxUの開発そのものは会社の収入に直結しない上に、その間、収入に繋がる他の業務もストップしてしまうため。 ↩
-
もちろん、UxU自身のコードでE4Xを使っている箇所があれば、そのままではUxU自体が動作しなくなってしまうため、その点の修正は厭わない方針です。 ↩
-
変更の量は少なくても、その後に致命的な悪影響を与える変更、というものもあるので、量だけが重要というわけではない。 ↩
-
実際に、再起動不要なアドオンの開発用の簡易フレームワークRestartlessの一部としてhere.jsを実装しています。 ↩
-
Node.jsではスタックトレースから行内のカラム位置を取得できるため、正確に関数呼び出しが行われた位置を把握できるのですが、Firefoxではスタックトレースから行番号までしか特定できないため、同じ行に複数の
here()
が存在していた場合に、どのhere()
が呼ばれたのかがわかりません。 ↩ -
元々の目的が「複数行にわたる文字列をすっきり記述したい」という事であると考えれば、1行に複数の
here()
が登場するようなケースは相当イレギュラーであると言えるため。最小の実装でほとんどの場合に対応できるのであればよしとする、という考え方に基づけば、この点は目をつぶることができる。 ↩ -
例えば、非同期処理のテストを行う際の利便性向上のため、UxUにはJSDeferredを標準添付している。 ↩
-
実際に、愛用しているアドオンが対応しているという理由からセキュリティ上の脆弱性がある古いバージョンのFirefoxを使い続けている人もいる。このような事態が発生すると、結果的にユーザーを危険に晒してしまうことになるため、非常によろしくない。 ↩
-
背景事情など、ユーザーにとってはどうでもいいことなので。 ↩