HTMLタグのホワイトリスト実装にはまって半日を無駄にしてしまった。ありがちな、汎用性を求めすぎた結果だった。
実装したかったこと
- テキストボックスに入力された文字列中の使用HTMLタグホワイトリストチェック
- パースは別途ライブラリがする
- 要求は以下
- 「特定の属性必須」
- 「特定の属性はあってもなくても良い」
- 「特定の属性値は指定の正規表現にマッチすること」
- 「指定した属性以外は許容しないこと」
- 上記4条件を組み合わせたタグを定義すること
- 同じタグに対して、複数のホワイトリストが定義できること。(例:
<span style="color:red;">
と<span style="text-decoration:underline;">
のみ許可)
最初にしようとしてつぶれたこと
言語はJava。Javaといえばインタフェース。
デザインパターンでいうDecoratorやComposerのようなことをしようとして、以下のインタフェースを定義してみた。
public interface TagMatcher {
/**
* 渡されたHTMLタグがマッチングルールに適合するかを判定します。
* @param el HTMLタグ
* @return 成否
*/
public boolean isMatch(Element el);
}
これ自体は問題なかった。呼び出し元からバインドする事項は高々これ1つだった。
まず、「属性-属性値を1つ検証するルール」「無条件でtrueとするルール」など単発のルールを実装して、
それらをDecoratorする形で「配下に持つ複数のルールのand(or)をとるルール」「配下のルールを否定するルール」を作った。
ぶちあたったのはもう一つのインタフェース。
このホワイトリストは元のXMLから与えらえる引数によって引数が定義される仕様だった。
例として、先ほどの2つのspanタグの定義は、以下のように与えられる。
span[style="color:red"],span[style="text-decoration:underline;"]
なるほど単純な繰り返しで、パース自体は比較的簡単に可能だ。
しかしつなげてみると
ホワイトリスト定義
-> (ホワイトリストを分解して汎用データ構造に仕立てるロジック)
-> 汎用データ構造
-> (汎用データ構造をもとに、やりたい検証内容を仕立てるロジック)
-> 検証結果
というロジックになってしまった。言い換えるなら以下だ。
インタフェースA
-> (インタフェースA → 汎用データ構造の変換処理)
-> 汎用データ構造
-> (汎用データ構造 → インタフェースBの変換処理)
-> インタフェースB
公約数
例えて言うと、ここでいう「汎用データ構造」は公約数を因数にする数のようなものだと考えている。
インタフェース間で受け渡す情報量や表現力を増やすには、公約数を大きくせねばらなない。
乗除を減らすにはインタフェースA-Bの公約数を増やせばいい話だが、
この約数は「2」とか「10」とかのcommonな分かりやすい数が好ましい。
(例えば「19」とか「113」とか、メタったルールを約数にすると嫌なコードになる)
実装したいことと今までに積み上げたことを整理してみる。
- インタフェースA(ホワイトリスト定義)で定義する内容は結構多い
- 共通データ構造(isMatch)にバインドする内容は結構少ない
- インタフェースBを通して呼び出し元がしたい内容は結構多い
言い換えると
- インタフェースAの数はでかい
- 公約数が小さい
- インタフェースBの数はでかい
これを守った上で取る施策は以下になる。
- 汎用データ構造をインタフェースB寄りにする(インタフェースBの約数を増やす。インタフェースA → 汎用データ構造の変換処理のボリュームを積む)
- 汎用データ構造 → インタフェースBの変換処理のボリュームを積む(約数を単純にする。)
どうあがいてもロジックに影響が出る。また、互いの機能が連携しあうため、修正が入るならばどちらか膨らんだほうのロジックに手を出さなければならない。
思い返せば、典型的なインビーダンスミスマッチだった。各両端のインタフェースの公約数、なにより内部の汎用データ構造が1つのインタフェースであると見抜けなかった。
顛末
結局、インタフェースや汎用性の美しさを捨てて、古典的な業務モデルクラスを設計した。
気を付けたのは約数の種類、具体的には業務的ルールに関する構造と言語特性・データ構造に関する構造をなるべく共通化することだった。
公約数を大きくした。
共通データ構造を新しいインタフェースで利用したり、互いのインタフェースの因数が非常に多い場合、小さな公約数にも利があったと思う。
が、局所実装は結局仕様変更によって捨てられるコードであり、まだ300行に満たない共通実装はインタフェース変更を受けるし多少の汚れがあっても十分読めるはず。。願わくば今のきれいなままで死んでほしいけれどもきっと叶わないだろう。