問題自体を縮小する設計技法
技術::technique
序
私のコードはレベルが低い、と見られることが多い。
実際コード単体を見るとあまり大したことはしていなくて、そのコードが何を解決しているかということを無視されると別にすごくないコードを書いていることが多いし、結果失礼極まりない謎AIから初心者レベル判定されることもあったりする。
だが、実際にはコードを書くことに限らず、私は「凄まじく仕事が速い」ことに定評がある。
その仕事量と成果を支えているのは、実のところ単純に「書くのが速い」といったことはではなく、アプローチの特殊さにある。 そもそも私は「設計偏重」であり、「設計時点で問題を容易にしているから解決が早く、簡単なのである」という立場だ。
しかしこれが単なる「変な技術」みたいに扱われることも多いので、ここでそのテクニックを紹介しよう。
前提
問題は一定ではない
学校の試験などであればおよそ問題は一定であるが、現実における問題というのは一定ではない。
「およそ」とはどういうことかというと、学校の試験でも稀に一定でなくなる条件というのが存在することがある。
例えば私が通っていた学校では、試験において自身が授業においてとったノートを持ち込むことが許されていた。 ちなみに、定期的なノート提出がある上、試験前にノートを提出しておく必要があるため、授業外の情報や、自筆でない情報を含めることはできない。
そして、体が弱い上に仕事で休みがちだった私はそもそも授業ノートというものがなく、ノートなしで試験を受ける必要があった。 この場合、問題自体は同一だが、「解決すべき問題」がノートを持っている人はノートに書いた内容を応用し、あるいは拾い上げるというものであるのに対し、私はそもそもの知識を問われることになり、問題が一定ではなくなる。
現実には解決すべき問題は、その定義が予め存在するわけではない。これは、人為的に作られた問題と、自然に発生した問題の違いである。 そのため、自然に発生した問題というのは、どう捉えるか、何を解決するかという点から既に差異が発生する。
小さな問題を解決することで全体が自然に解消するのであれば、そのほうが解決は容易なのである。
解決方法は手段である。まして使用する技術は手段に違いない
問題について議論すると、なぜか解決手段や、さらには技術を前提とした主張をされることが多い。
だが、これは本質を見失っている。本当の目的は「問題を解決すること」であるはずであり、解決できれば手段や道具はどうでも良いだろう。 個人的な好みとしての手段や道具というのもあるだろうが、それにこだわるのはやはり道を違えている。 そして、これが問題を複雑にし、複雑な解決を要求する根本的原因である。
考えるのは問題そのものであり、解決方法にはこだわらない。 これができないようでは問題そのものを簡単にすることはできない。
なお、これを実現するためには、様々な解決手段や技術の選択肢を持つ必要があり、幅広い知見が要求されるのは言うまでもない。
具体的手法
問題を細かく、方法は端的かつ本質的に
問題そのものを小さくするには、とにかく問題の要素を極めて小さい単位で考える必要がある。
例えば、私がPureBuilderにおいて取り組んできた問題は 「同じ要素があると大変だしミスる」 である。 重要なのは
- 反復を避けたい
- 一括管理したい
- 動的生成したい
などではない ということだ。 これは既に問題を解釈・定義してしまっており、問題を正しく定義できていない。 問題になっているのはあくまで、HTML直書きしていると修正が大変であり、しかも修正においてミスが発生するということだ。
これに対する解決策は 「違う部分だけ書こう」 である。やっぱり、「動的生成」やら「一括管理」やらではない。 同じ部分を書くからミスが発生するし大変なのであるから、同じ部分を書かなければ問題は解決する のである。 別の言い方をすると、「この条件が成り立つことによって問題自体が発生しない」という状態を定義するのである。
重要なのは ここでそのまま次のステップに進まない ということだ。 あくまで、このレベルで既知の問題全てに対して繰り返していく。
全てを解決する必要はない
次のステップは、「定義した条件の達成手段を定義する」ことである。 これは多いほうがよい。
その定義を終えたら、それぞれの手段について検証する。 この検証は次の2点である。
- その手段は他の各問題にどのように影響するか
- その手段は新たにどのような問題を発生させるか
他の問題に対して悪い影響を与えない手段は良い手段であり、他の問題を多く解消させる手段は優れた手段である。 良い手段は常に良いが、優れた手段は常に良いとは限らない。また、良いことが最善であるとも限らない。
これを再帰的に行うのだが、 解決が困難な、コストの高い問題を発生させた手段は、そのツリーまるごと破棄しなければならない。 ここで目標としているのは解決のためのコストを下げることだから、新たに解決困難な問題を発生させてはならないのだ。
ここでできるだけコストが低く、「簡単だけれど多くの問題が解消してくれる手段」を選択するようにするのだが、 それが全ての問題を解決してくれない、特に「全ての問題を解決する手段があるにもかかわらず、良い手段は全ての問題を解決しない」という状況に直面することがある。
ここにおいて重要なのは 許容するコストが問題を解決するコストより低いのであれば許容せよ である。
例えば、「処理速度が遅い」「子プロセスを頻繁に発生する」「処理が重い」などの問題はよく発生する。 この問題が深刻な問題を発生するならばそれを軽減する方法を考えなければならないが、別に気持ちわるいだけで実害はないのならそれで良い。 実害があるとしても、VPSのプランをひとつ上げれば良い、みたいな話だと「それでいいじゃん」ということもある。
また、本質的に「問題を解消する」ということそのものが極めて困難なものというのも存在する。 特にミスをなくすとか、停止をなくすとか、障害をなくすとか、その手のものは解消しようとすると大概ドツボにはまる。
そうした「問題をコンパクトに解決しようとした結果残ってしまうもの」に関しては、その問題を解消することではなく、「問題を許容可能な状態にする」ということを考えたほうが良い。 フェイルセーフに設計するとか、ミスが発生する前提でリカバリー手順を確立するとか、障害が発生する可能性についてサービスとして明示するとかだ。 それらは強い抵抗をうける可能性があるが、そのコストと、コストをかけた上でのリスクを考えれば、安上がりである可能性は低くない。
小さいレベルで検証し、テストする
多くのことはやってみなければわからない。考えた上でイケそうに思っても、あるいはダメそうだと思っても、実際は結構異なるものだ。 だが、大規模な実装の上でテストするというのは、何度も方向転換することになり、好ましくない。
そこで、テスト自体をごく小さいレベルにして、感触を確かめるのが有効である。
この話は論点がわかりにくいだろうから、詳しく説明しよう。
PureBuilderの場合は、そもそもその手法が変遷したこともあり、非常に多くの方法が試された。 Perlのワンライナーで置き換える方法、プログラムでビルドする方法、パート分けしてcatでつなぐ方法、PHPで置き換える方法、テンプレートを使う方法。 テンプレートで使う方法といっても、プログラムからテンプレートを呼び出してそこに埋め込む方法(Perlのワンライナーも考え方はこれ)、テンプレート内にプログラムのロジックを埋め込む方法(PHPもこれに近い。eRubyはこれ)、テンプレート側からドキュメントを読み込む方法、さらには「環境変数にセットしてZshのヒアドキュメントをテンプレートにしてcatに流せばできるよね」なんてのも。
最後のはわからないかもしれないけれど、
cat <<EOF
<html>
<body>
<article>
$_BODY_DOCUMENT
</article>
</body>
</html>
EOF
というスクリプトを書いて、template.zsh
とかやっておくと、
% _BODY_DOCUMENT="$(cat foo.html)" template.zsh
とかやるだけでテンプレート展開ができるという話である。ACCSでは本当にこんなことをやっていた時期もある。ちなみに、環境変数をセットするのもZshスクリプトであり、こちらは結構複雑な処理であった。
このスクリプトを見ると、「catと環境変数で展開できるのは当たり前だから試すまでもないじゃないか」と思うかもしれないが、思うのと実際書くのでは全く話が違う。というよりも、これ単独ではあまり意味がないのだが、発想したものを一通り書くとわかる。
例えば、ここで置き換えを想定したトークン埋め込みバージョン(Perlワンライナーでやれる)と、eRubyバージョンを作ったとする。
環境変数バージョンは非常に汎用性が高く、呼び出し元がシェルスクリプトである場合にも親和性が高い。だが、環境変数には128kBとか2MBとかのサイズ制限があり、ひっかかる可能性がある。 だが、書くのはものすごく簡単である。「書ける上に単純な仕組みを使う」という魅力を感じることができる。書かないと分からない、意外と大きなメリットだ。そして、実際にやってみると意外と制限にひっかかったり、思わぬ事態を発生することがわかる。「これ外部から入力をうけるときはやっちゃ駄目だな」ということを確認できる。 「簡単に実現できて効力が高いが問題も多い」というのは、このときに使わなかったとしても「即席でなんかテンプレート的なものがいるときに書けるな」という引き出しになる。
トークン埋め込みバージョンは、シェルスクリプトでread/echo(print)し続ける方式だとかなり遅くて、シェルスクリプトでもperlを呼ぶことになる(sedだと難しい)。汎用性は高いのだが、テキスト処理を得意とする言語でないと難しい。ちゃんとした仕組み(構文)を作るかどうかというのは、この簡単さとコストとの兼ね合いになるなということを感じられる。
そして最後にeRubyバージョンを作るととても知見が得られる。まず何より、今までと比べると断然めんどくさい。 「ほんのちょっとのこと」なのだけど、そのほんのちょっとのことがこんなにめんどくさいんだなと感じられるし、考えるべき要素が多いことも面倒に感じられる。一方、他の方式よりも最初からかなりクリーンに書けるから、構文を自分で作らなくて良いという意味でメリットがある。
なにより、eRubyで書こうとすると、環境変数方式がほとんど唯一の解で、トークン方式がキーワードか構文か程度の話だったのが、いきなりたくさんの選択肢や考慮すべき点が降り掛かってくることがわかる。 もちろん、eRubyの中でドキュメントを展開しようとすると、呼び出し元はRubyスクリプトにならざるをえないのだが、eRubyのテンプレート側でドキュメントを読むようにするとeRubyで完結できる。 そのテンプレート側で読む方法も、単純にSTDINから読んで埋め込む方法のほかに、STDINから読んだものを解析して扱うこともできるということがわかる。STDIN経由ならサイズ制限とかはないから、YAMLとかJSONとかで受け渡してもいい。 そして、普通は気づかない「eRubyではSTDINは使えるけどARGFは使えない」ということに気づいたりする。
シェルスクリプトから扱いたい場合は、対象ドキュメントのパスを環境変数にセットして呼び出すというような方法もある。 他にも様々なアプローチや工夫があり、「いい発想」に至るには書いて動かしてみないと難しいし、その前のふたつを書いてないとeRubyの問題や特徴は「そんなもんだよな」で終わってしまうのだ。
ちなみに、私はRubyのMarshalというオブジェクトダンプ形式を環境変数で渡すプログラムを書いて、「環境変数にNUL(\000
)が渡せない」という事実にぶち当たったりした。知ってれば「そりゃそうだ」と思うだろうけれど、やらないとなかなか気づかない。
ちなみにこの件、2日前に授業で「LinuxはC言語であり、環境変数の受け渡しもC言語で書かれています」という話をしたばかりにもかかわらず、「NUL文字が渡せないなぁ」と思ってメーリングリストで聞いてしまったのは、私のIT人生においてトップクラスのやらかし案件である。
ちなみに、PureBuilder SimplyはテンプレートシステムはPandocに依存しており、オプショナルにeRubyを使うこともできるが、少なくとも本文展開はPandocテンプレートを使っている。 従来と比べてずっと複雑なシステムを採用したことになるが、「それを実装するのもメンテナンスするのも私じゃないし、機能が提供されている以上それを便利に使えばいいじゃない」ということで、「ドキュメント生成にPandocを採用するという選択肢をとった時点で、テンプレートシステムという問題は自動的に解消された」ということになる。
また、Pluto開発のときにPlutoを「リニアスケールするプログラム」として書くのがテーマだったのだが、その途中で「実際に並列で走らせる」というテストをしている。
「本質的な概念」を念頭に開発を進めているので、このときは並列に起動する方法は特に決めていなかった。 重要なのは「並列に走ることでそれに比例したパフォーマンスが発揮される」ということなのだ。
だから、まずテストされるのは並列に起動する数によってパフォーマンスがどのように変動するか、である1。 そのため、ごく単純にZshから
for i in {1..20}
do
pluto-worker.rb $(( ( i - 1 ) * 10000 )) 10000
done
みたいな感じで起動したのである。
これは並列処理のテストであるが、ここで重要なのは「どのように並列処理するかは決めていないので、確定された方法で並列処理を行っているわけではない」ということである。
事前にテストできれば事前に決めてしまえるのだが、「並列処理の起動方法」をテストするにあたり呼び出すプログラム自体の実装に影響を受けるため、そのプログラムの実装が存在している必要があった。だから、決めずに作った。
この上で、ごく単純な「形式違いの起動」のテストを繰り返すのである。 様々なアイディアを、「そのアイディアの部分だけが違いになるもの」を書く。 例えば「Rubyで呼び出したら速いのでは」と考えたなら、単純に
20.times do |i|
fork { exec "pluto-worker.rb", (i * 10000).to_s, "10000" }
end
というところから始めるのである。こんなん同じだろ、と思ったそこのあなた。これですらめっちゃ違う。
自明に思えることを試したり、より発展した方法が思いつくにもかかわらず小さなテストを繰り返すのは、このテストの省略は、決まった手法を採用する硬直した考えにつながりやすいからである。 そして、こうしたテストは、想像だにしないところで自明だと思っていたことが覆されるのはよくある。
問題と解決策は1:1ではない
「有力そうな手から考える」というのは重要で、問題に視点を置いて考えているといい手が浮かばないことというのは普通にある。
この視点において重要なのは、「有力そうな手のアイディアをストックしておく」ということだ。特に使う予定はないが、「こういうふうにすると色々うまくいくんじゃなかろうか」という筋の良さそうな直感をストックしておくことで、大胆な設計で問題を解消することができることもある。
私の大きな武器であるOrbital designもそのひとつだ。 Orbital designは「小さなインスタンスが自分がすべきことだけを規定されており、それ意外のことに関知する必要はない」というルールに基づいて設計するものであり、連続的に変化するデータを扱う場合や、並列処理において大幅にコストを下げ、柔軟性を上げることができるものである。
Orbital designは自然に誕生したものだが、これはやはり「干渉と同調が難しい」と感じたときに、「連携して動作すること自体をやめてしまおう」と考えたことから生まれている。 このときにOrbital designが誕生した背景には、私が「自分がやるべきことがあるのに、他の人がやらないせいで自分の手が止められて待たされる」という経験がすごく多かった2からというのがある。
だからこそ、私は他者に左右されることなく自分のすべきことを完了したいと思うし、これは「完全に自己完結する方法」と、「他者との間に依存性をなくす方法」の2種類がある。 当然ながら、それぞれが優秀だと仮定するのであれば後者のほうが良い。
こうした背景がOrbital designの発想に至ったことは否めない。
そして、Orbital designは単純に並列実行と同期に関する問題を解決するだけではない。 Orbital designは様々な問題を変えてしまう。
Orbital designにおいて決められているのは
- データベースの形式
- そのデータベースに書けるエージェントは1つだけであること
- データベースへのリード及びライトはブロックされないこと
- そのインスタンスがすべきこと
- インスタンスは反復して動作することを想定すること
だけであり、このデザインパターンを守ることでタイミングだけでなく、同期の問題や、さらには実装にかかる制限すらも解消してしまう。
例を出そう。例えば簡単なサーチエンジンプログラムがあるとする。 この場合、
- 検索対象のデータを収集するボット
- 検索対象データからインデックスを作成するエージェント
- ユーザーに検索をできるようにするウェブアプリケーション
の3つがある。それぞれデータベースに対してwriteできるのはひとつのエージェントと決められているので、
- 収集ボットが書き込むデータベース
- インデックスエージェントが書き込むデータベース
の2つのデータベースが必要となる。権限としては次の通りだ。
エージェント | 検索対象DB | インデックスDB |
---|---|---|
収集ボット | write | none |
インデクサ | read | write |
ウェブアプリ | none | read |
read及びwriteがブロックされないので、検索対象DBとインデックスDBは非同期に更新される。つまり、検索対象DBとインデックスDBには不整合が起こりうる。さらに言えば、ウェブアプリは検索対象DBの情報は参照できない。 (デザインパターン上、複数のDBからreadすることは違反ではないが好ましくない)
だが、それこそが重要な点である。連続的に変化するデータを扱う場合、ある瞬間にデータが同期されていることにはあまり意味がなく、常にデータを同期しようとすると著しいパフォーマンス損失が発生するとともに、並列性が低下する。
特にこの例のように、データを構成する要素に外部リソースがあると「完全に同期する」ということはそもそも不可能であるため、むしろこうした割り切りが大切なのだ。
これによってエージェントプログラムは小さく、そしてシンプルに動作できるようになる。 例えば収集ボットはシェルスクリプトとして書き、インデクサは文字列処理の得意なPerlやRubyなどを使用するといった方法も取れる。 担当者の得意とするものや、そのケースに適したものにすることができ、分業にも適している。
並列性を高めるために「IOブロックしない」というのがあるが、これも「データは非同期である」ということを前提とすると色々と方法があることがわかるだろう。 特に有力なのがtupple spaceで、エージェントはtupple spaceへ書き込む分にはブロックされることはない。tupple spaceからオブジェクトを拾って実際にデータベースに書き込むインスタンスがひとつであれば、並列と同期の問題は発生せず、インデックスエージェントはマルチインスタンスで動作することができる。 ちなみに、「一回の処理で同じキーに対する書き込みが発生しないことを保証する」という前提を作ってファイルシステムをKVSとして使うのもなかなか有力。
こうしたそれぞれの手法、そしてその結果が、元の問題ひとつひとつに対応しているわけではなく、あるデザインパターンを持ち込むことで多くの問題を解消している。 それは個々の問題に対して「これを実現するには」と考えていては見えてこないこともある。 「多くの問題が発生するのはデザインがまずいからである」という観点から、場当たり的な改善ではなく、スマートに問題が発生しない設計を導き出すのは、設計段階でしかできないのだ。
目からウロコかもしれない、私の問題対応
マイナンバー制度におけるマイナンバー管理
法的にも厳しい条件が設けられたマイナンバーの管理。ストレージシステム、サーバー環境、セキュリティ環境の構築など結構重たい案件として私のところにも結構問い合わせがあった。
これに対して、私に問い合わせがあった全てのケースにおけるベストプラクティスは、「紙に書いて金庫に保管してください」だった。
私のところに相談にくるのは基本中小企業であるし、その場合人事情報を扱う部署というのは大抵ない。 しかしその規模の企業でも「金庫がない」というケースは稀であり、金庫へのアクセスが管理されていないことはない。
つまり金庫という存在によって既に「機密性があり、アクセス権限が管理されている環境」が確立されている状態なので、ヒューマンエラー対策が大変なITセキュリティシステムを作るよりも、既に高い管理意識が根付いていて、実際にそのポリシーで管理されているものを利用するほうが良い。 それに、紙に鉛筆で書いたものというのは、データなんかよりはるかに強度があり、失われにくい。黒鉛は安定した物質であり、「鉛筆だと消える」と思う人が多いようだが、試しに何年も前に鉛筆で書いたものを消しゴムでこすってみれば良い。消えないから。
だから、私の回答は「水でほろほろにならない紙に鉛筆でしっかりと書いて、それを金庫に保管してください」であった。 全部受けていれば100万円はくだらない受注額になったと思うが、このアドバイスで全て解決してしまったので1件も受注にならなかった。
それgrepですよね
たとえば、大量のログファイルから、特定の項目を集計するソフトウェアを作って欲しいという相談を受けたことがある。
作る立場の人間だと、この手のものは「作ること」を前提に考えてしまいがちなのだが、実際これは
$ (find . -name '*.log' | xargs cat) | sed 's/ .*//' | sort | uniq -c
でいける。 (ログの量によっては厳しいのでひと工夫いる)
また、「膨大な文献から、ある特定のワードを含むファイルを見つけたい。できればその前後の行も見たい」という依頼を受けたこともあるが、完全に
$ grep -C1 -r -F 特定のワード .
な案件であり、CygwinとMSYSの説明をして丁重にお断りした。
もちろん、Mimir Yokohamaとしては開発案件は受けたほうが売上になるが、さすがに車輪の再発明でお金をとるのは気が引ける。
それExcelでよくないですか
割とある。
これは私を知らない人には「なんのこっちゃ」だろうけれど、私はExcelをひどく憎んでいて、「Excelフォーマットでデータを送りつけて、Excelで編集して返せと言われたから団体から脱退した」なんてこともあったし、「Excelでデータを作ることを求められたから依頼を断った」なんてこともある。
Excel(というより表計算ソフト)を使いたくないがためにソフトウェア開発したことも数知れないし、なにがなんでも使いたくない。
「CSVにある文字のフォーマットが一定じゃないので整形したい」「Excel関数じゃ駄目ですか…?」
少ないコストで問題が解決できるならそれで良いのである。
Auto-scrolling text
Auto-scrolling textはYouTubeに上がっているテキスト動画のように「ハンズフリーでテキストを読みたい。それも、任意のスクロールスピードで」という動機で作られた。
方法は実に色々あって、一番簡単なのは端末で流すことなのだけど、それだと読み心地がよくないので、「スムーズな画面のスクロールならJavaScriptにあるじゃん」ということで、HTMLページを生成するプログラムになった。単純極まりないが、かなり緻密にコントロールできるので良い解決策だった。キーボードやマウスのイベントを拾うのも楽。
ちなみに、快適に見られるブラウザが欲しくて、結局ブラウザを自作した挙げ句、そのブラウザ自体が新規に書き直されるというような事態に至っているが、これは問題解決のためではなく、趣味とトレーニングを兼ねているので採算度外視である。