日々常々

ふつうのプログラマがあたりまえにしたいこと。

決定の遅延と添木の設計

設計してますか?

ぶっちゃけ「設計 is 何」ってよくわからないんだけど、設計はしてます。 で、その設計なんだけど、今の何かを満たすものだけでなく未来に備える系があるなと思った。 私の中では「決定の遅延」と「添木」と呼んでるものです。

決定の遅延

決定を遅延させるために行う設計があります。

現実はいろんなことが起こり、未来がどうなるかはわかりません。 少し待てば情報が増えることがわかっていることはあります。でもどのような情報が来るかは分かりません。

「すべての情報が揃ってから動けばいい」という選択もあり、これはこれで一理あります。 ですがそのようにしていると期限に間に合わないこともしばしばあります。 時間は融通が効かないので、他で融通を効かせる必要があります。

そういう時に「決定を遅延させる設計」を使うことがあります。 決まっている部分を進めて、決まっていない部分を後回しにするという当たり前のような手段ですが、往々にして決まっていることと決まっていないことはないまぜになっています。

そういう時にいろんな整理をします。

  • 決まっていることと決まっていないことを分ける
  • 決まっていない度合いで濃淡をつける
  • 決まる時に取られうる選択肢を限定する
  • ...

かたい部分はハードパーツって呼んだりするもののこともある。

(直接ここで書いているものとイコールではないが。)

あの手この手で決め難いところを柔らかく包み込んで、決定を遅らせることが可能なようにする。 単なる先送りではなく、重要な部分を決めるための時間を稼ぎ、決まった時に憂いなく取り組めるようにする準備を整える。 そんな感じ。

添木

プロジェクトの期間中に決定が訪れるものであり、それが待つ価値のあるものであれば、可能な限り遅延させられるようにして進めます。 それはそれとして決定が訪れないものもそれなりにあります。

「将来こうなるかもしれない」と考えるのは重要ではあるものの、なっていないことを前提に作るのはYAGNIと言われるアンチパターンです。(作るコストがAIで劇的に下がってるので「作っちゃえばいいじゃん」って話もありはしますが、本稿の趣旨ではないのでそれはそれとしてください。) でも考えるのはいいことなんです。 前提にした対応はしない、でも方向づけはする。 そういう設計を「添木」と呼んでいます。

具体例はうまく伝わるかわかんないのですが、構造や名前は添木の側面を持ちます。

たとえばパッケージ構造はクラスの増え方を方向づけします。 新規プロダクトで序盤に用意したどこからか借りてきたパッケージ構造が邪魔くさく感じたことはないでしょうか。 それはおそらく借りてくる元のコンテキストでは「その方向に進んでほしくない」というものです。意図的であれ結果的であれ。 そのコンテキストを踏襲するなら「邪魔くさい」と感じたのは何かしらアーキテクチャのルールに違反しているからでしょう。

たとえば名前は他の名前に影響を与えます。 「添木」という言葉があれば「鉢」とか「枝」とかが連想されるものです。 システム開発での名前選びは、今後の他の名前に何かしらの制約を与えます。具体例?触ったことあるシステムのクラス名とかを思い返して、新しいクラスを追加しようとしたらそれに引っ張られますよね。それです。

「添木」は存在を認識されないなど、伝わらないこともしばしばです。 実際の植物でも添木を無視して成長してることもままあるものですし、まぁ仕方ない。 「そうはならんやろ」と思うような方向に成長することも稀によくある。

添木で物足りないなら周りをしっかり囲いましょう。 ニュアンスや文化で伝えるのではなく、規約やルールでしっかり固めましょう。 どちらがいいかなーというのもまちまちだと思うんだけど、AIを前提にすると添木は主張が弱すぎて役に立たない気もしなくはない。

speakerdeck.com

ルールや規約の話を出してしまったので「添木」がその系に読み取れなくもない気はするけど、あくまで「成長に方向性を与える」という設計パターンです。 「"今は決めない"と決めたもの」でも「成長するならこっち方向かなー」という意図を込める。そんな設計ってのもあるよなぁと。伝わるかなー。

補足: 仮止め

私の中で「仮止め」と呼んでいる設計パターンもあるので触れておく。

「仮止め」は家具を組み立てる時とかにネジを軽く締めておく、キツく締める前にやっておくあれです。 永続的ではなく一時的にその状態で固定して全体のバランスを調整したり、場合によってはやめたりするのだけど、とにかくそこを固定しないと他のことを考えようにも空中分解してしまうようなことはそれなりにあります。 「一旦これで決めて次に進めよう」という感じで決定はしているので、「決定の遅延」とは区別しています。

なおADRなどの軽量ドキュメントによる「取り消し可能な決定」も「仮止め」に近いけれど、そのままにする可能性があるのが「取り消し可能な決定」です。「仮止め」はその場所で固定するにしても「ちゃんと締める」は必須になる点が違ったりする。

しめ

どんな設計でも動けばいい。それはそう。動くのはいいこと。動かないのは話にならない。 とは言えコンピューターの発達に伴い、「動く」を満たした上で取れる選択肢も非常に広くなっている。 「こういう作りをしないとそもそも動くと言えない」のような制約は縮小し、超富豪的プログラミングも十分成立してしまう今日この頃。 そんな制約が少ない中での設計の考え方ってのもあるわけで。

決められるなら決めてしまえばいいことも多いのだけど、決めない方がいいこともそれなりにありはします。 そういう時に「決定を遅延」させられるようにし、さらに決めないまま方向性を主張する「添木」を配置する。 そんな感じの考え方をすることがなくはないなぁ、という話でした。

「いちばんめ」が大事でないならfindFirstよりfindAnyを使う

Javaの Stream の話です。

Stream は複数のものを扱いますが、結果が1つ(もしくは0 or 1個)だけ欲しい場合がよくあります。 こういう時に使用できるのが findFirstfindAny です。

jshell> Stream.of("hoge", "fuga", "piyo").findFirst()
$1 ==> Optional[hoge]

jshell> Stream.of("hoge", "fuga", "piyo").findAny()
$2 ==> Optional[hoge]

結果は同じで、先頭の "hoge" が返っています。findなんで Optional で包まれています。

実際の使用シーンはコレクションをIDとかで filter して、1件に絞られている場合とか、filter の条件を満たすものならどれでもいい場合です。 filterせずに findFirst() するのはあまりない気がする。そんなものは最初から Optional にしとけって感じだし。

こういう時に findFirst() が使われがちです。AIも findFirst() を生成してきます。ですがーー

「いちばんめ」に強い意志がないならば findAny() を選ぶ のがいいです。

実装している時は軽い気持ちで選ぶでしょうが、何か不具合があった時にこういうコードを見ると「これfirstでないとダメなのかな?」とか「このStreamの順序ってどうなってるんだっけ?」とか考えなきゃいけなくなります。まじめんどい。

脱線:2件以上あったらまずいときの話

「1件しか入っていないから findFirst() でいいや」としている場合。 2件以上入ってたら変なことになるけど目を瞑って findFirst() している場合。前者も大概だけど、後者のほうが罪深いです。

実は複数件入っているとかに起因する不具合がそれなりに見ます。わかりづらいです。 取得できて処理が続行できるので、不正な状態になっていることが握りつぶされている。2件目以降は消え去るので、本番でしか起こらないとかだとほんと調査しづらい。

もし「1件であること」が大事なのなら findFirst() を使ってはいけないです。もちろん findAny() もだめです。 ただ findAny() を選択している場合は「複数あったらどれが返ってもいいやつなんだよな」という確認が入っている感じがします。 FirstとAnyの違いに意識を向けられる場合は複数件にも意識が向く感じ。完璧じゃないけど結構な精度があるとは思う。 思考停止や「 findFirst() はグチグチ言われるから findAny() にする」とかだと効果ないですけどね。

やるなら一度 toList() とかでコレクションにして、サイズチェックしてから1件取り出しです。 ものによっては迂闊に toList() したら思った以上の件数があってOOMEとかも起こったりするので、 .limit(2).toList() とかですかね。

めんどくさいのはわかるけど、障害のほうがめんどくさいですし、お金もかかります。せっかくJavaつかってんだから実装で防げるものは防ぐのがいいです。

reduce で2件目以降がきたら例外にするとかもできます。ちゃっぴーに書いてもらったらこんな感じ。

T value = stream.reduce((a, b) -> {
    throw new IllegalStateException("More than one element");
}).orElseThrow(() -> new IllegalStateException("No element"));

これをいろんなとこにコピペするのはNGです。やるなら共通のユーティリティとかにしましょう。

……脱線のほうが長くね?

おまけ:StreamのAPI仕様

最後に一応、Javadocも見ておきましょう。サイドにメソッド一覧とか出るようになって使いやすくなりましたね。って今日気づきました。普段IDEでみるからね。。。

安定した結果が必要な場合は、かわりにfindFirst()を使用してください。

こんな書き方をされると「安定してる findFirst() の方を使った方が良さそう」と思っても仕方ないと思います。

でももし安定した結果が大事なのであれば、ストリームが順序を持つものであることを保証しなければなりません。 Stream自身に isOrdered() みたいな判定メソッドはないため、メソッド内でListから生成されたStreamであることが明らかであり、今後もそれが維持されることが前提になります。これは思っている以上に難しいことです。 そういうことにとらわれずにやるとすれば sorted() を読んで並び替えてから findFirst() でしょうか。 sorted() で実行時例外が出ないといいですねー。

おまけ:Codexさん

あなたがいま書いてきたコードですけど。

人間なら「わかってんのになんでやらんの」と言いたくなっちゃいますが、AIにそういう説教するのは「お金払って話を聞いてもらって相槌を打ってもらう」というアレですから、アレ。

"壊してよいオモチャ"を持つ

"壊してよいオモチャ"はアプレンティスシップ・パターンにある私のお気に入りパターンの一つです。

お気に入り具合は干支が一周して同じようなタイトルでブログを書いてることから察してください。

"壊してよいオモチャ" とは

自分で作った、自分で使う、自分のためのツールのことです。

書籍の「問題」セクションはこういう文章で始まります。

あなたは、失敗を許さない環境で働いています。

先のブログでも「仕事では安易に失敗することはできません。」と書いていたりします。書籍ではこう続きます。

しかしながら、何かを学ぶ最善の方法は、たいていは失敗することです。

「 "壊してよいオモチャ" を通して失敗を経験していこうぜ」と言うのが大まかなパターンの説明です。

"壊してよい" という言葉

壊してよいと言える条件にはどのようなものがあるでしょうか。 仕事で扱うプロダクトで「壊してよい」と言うためには様々な条件があるでしょう。 自分で使うツールであれば、直せるならば壊しても構わないと言えることが多いです。

着目したいのは「壊れてよい」ではなく「壊してよい」と言うところ。 原題だと「Breakable」となっています。ableなので「壊せる」のような積極的な感じも感じたり感じなかったり。英語わからん。 「何もしてないのに壊れた」は定番のネタですが、このコンテキストでは自分の行動によって「壊す」が盛り込まれています。 壊れてしまうような、むしろ積極的に壊すようなチャレンジをしてもよい。それが "壊してよい" にあるように思います。

"オモチャ" という言葉

「オモチャ」という言葉には「楽しめる」ということが含まれます。

オモチャであり楽しめるものであるべきことを忘れないでください。それらが楽しめなくなり、最初の熱狂の嵐が過ぎ去った後は、それらは埃を被ることになり(以下略

楽しめることが大事です。時間を忘れていじってしまうような、そういうもの。 この辺りがうまく名付けられたパターン名の良さですね。

なおアプレンティスシップパターンには情熱という言葉を含む「情熱を放つ」と「情熱を育む」があります。そういえば同年代に情熱プログラマーという本も出版されてました。「情熱」が流行っていた時期なんでしたっけ。

"壊してよいオモチャ" という言葉

夢中でいじり回して、ぶっ壊してしまって、直して、またいじる。 そんな物体が "壊してよいオモチャ" です。

子供が遊んでいるオモチャを見ていると、簡単に壊れるものの、簡単に直せるようになっているものが多いです。 負荷が集中する稼働部はあえて外れやすく作り、そうでない部分は頑丈に作っている、というような話を聞いたことがある気がします。 これも "壊してよいオモチャ" が持つべき特性の一つでしょう。

ソフトウェア開発者が扱うのはソフトウェアですから、ハードウェアや物体、人などを扱う仕事に比べて "壊してよいオモチャ" を得るハードルは非常に低くあります。せっかくのアドバンテージなので活かしましょう。

"壊してよいオモチャ" を持つ

さてタイトル。 "壊してよいオモチャ" は昔からある言葉ではあるし、珍しいアプローチではないものの、なかなかハードルが高くもありました。

特に近年は自分でツールをつくらずとも、よくできたツールに溢れていました。 特化したツールを使わずとも汎用的なツールでなんとかなる場面も多いです。

書籍でも以下のように書かれています。

たいてい、これらのオモチャは、業界の標準ツールの簡単な再実装であり、(以下略

これは明確に「車輪の再実装」と呼ばれるアンチパターンですが、アンチパターンは特定文脈においては推奨される行動になります。 たとえば「プログラマが知るべき97のこと」の "車輪の再発明の効用" が挙げられます。

"車輪の再発明の効用" では作る過程で知ることが多いところにフォーカスしています。 "壊してよいオモチャ" も同様の記述はありますが、それ以上に使い続けてメンテナンスすることに力点があるように思います。 これは「メンテナンス」というプロセス自体も再発明の範囲と言えたりする感じ。

本稿の趣旨は「車輪の再発明をしよう」という話ではなく「持ち続けよう」です。前者も大事なんだけど、それを踏まえた後者です。

自分のためのツールを作って、それを使い続けましょう。 その過程で得られるものは非常に多いです。 メンテナンスの中には「0から作り直し」があっても構いません。同じ名前をつけた同じ目的のツールを「使い続ける」のが大事です。

AI時代だからこそ

なんでこんなことを書いているかって言うと、AIによって「それっぽく動くもの」を作る速度は明確に早くなったからです。

なのでそれに乗じて自分のための道具を作って、それを使い倒してみるといいんじゃないかなーと最近思っている次第。実際やってみたら思った以上に手応えがあったので書いてます。

speakerdeck.com

「こういうことできる気がする」と言う、自分の「気がする」と言う直感を検証するのが容易になりました。 開発者としての直感を鍛えたり検証したりすると、いろいろ捗ります。

このスライドでは「重々しく捉えなくても100行くらいならノリで書けるでしょ?」とか言ってますが、そりゃ書く労力だけならねぇって感じで。実際は脳内で設計とか走らせてるわけで。難しいものもあるよね。 でもその辺もAIが担ってくれます。思いついたらやってみましょう。

その過程で自分の知らない技術が勝手に出てくることもしばしばあります。そこをとっかかりに対象技術への理解を深めましょう。 「この技術使ってるけど、なんで?」とかAIを問い詰めてみるのもいいでしょう。 知っている技術だけで構成されてしまったなら、意識的に気になっていた技術を盛り込んでみましょう。特定ライブラリやフレームワークでもいいですし、設計パターンでもよいでしょう。キーワードだけぶちこめばそれに沿ってそれっぽいものを作ってくれます。

AIでの初期作成はさっくり作れてしまうので「車輪の再発明」の過程で得られるものはほとんどありません。プロンプトの試行錯誤などはありますが、それはその技術であって「車輪」に用いられる技術ではないわけで。 それゆえに使い続ける点に力点を置きます。 作ったツールを使っていると、当然のように痒いところに手が届きません。 どんどん変更していきましょう。 そして、ふとした変更で簡単に壊れてしまうでしょう。

壊れにくくするのか、直しやすくするのか、置き換えやすくするのか。そういうことを考えて "壊してよいオモチャ" に取り組むのは、これまでも大事だったことですが、今後も活きていくんじゃないかなって。そうおもったりします。

具体例

JIGは私にとっての "壊してよいオモチャ" の代表例です。

github.com

2018年4月からなので8年弱さわり続けていることになります。 あまり変更していない時期もありますが、ずっと生きていますし、色々と「壊す」ようなことにも取り組んできました。 壊したときの影響範囲をどうコントロールするかとかも重要なテーマ。 こういうプロダクトがあると「AIと既存コードをぶつけたらどうなるかとか」とかの肌感覚も得やすいです。

JIGは他の人も使っているので「自分だけのため」ではありませんが、 "壊してよいオモチャ" は別に「自分のため」であって「自分だけのため」ではないです。 「自分のため」は自分自身がそのツールに対して要求を出す第一人者であり、動かなくなったりしたら真っ先に困る人ってこと。 他の人のためのツールは動かなくなって困るまでにラグがあったり、ちょっと困っても「別に言わなくていいか」となったりするので違うんです。 作りっぱなしでそれっぽく動くだけのものは "壊してよいオモチャ" ではありません。ここ大事。

あと全然開発に関係ないものだと、将棋の棋譜閲覧ツール。

github.com

完全に自分専用で配布とか考えてないんだけど、vibe coding 100%でKotlinつかったデスクトップアプリと言う。Kotlinなんもわからん。vibeなので一行も書いてないけど、一応読んではいる。純粋に「趣味で自分が欲しいと思ったもの」に振り切っているのがポイントで、作りが微妙でも「まぁ動くならいいや」と進めていたりする。そのうち「仕様を出力してゼロから別の技術使って作り直した時にどの程度のものになるんだろう」みたいなのを試しそうと思ってる。

他にも開発で使うツールは色々あったりします。 "壊してよいオモチャ" は毎日のように使うのが大事だと思います。 少しの引っ掛かり、ちょっとした機能追加。そう言うのを使いながら感じるのはある種の才能かもしれませんが、ある程度は習得できる技術だとも思います。 使っている間は情熱を持ってメンテナンスするのですが、役目を終えて使わなくなったものはメンテナンスもしなくなります。打ち捨てられるのもオモチャの宿命かね。

脱線:失敗について

失敗に関しては昔(2013年)にこんなこと言っていたり

最近だと若干まるい表現に。

表現がまるいだけで、言ってることは「失敗しようぜ」である。

脱線:パターンの使い方

「アンチパターン」とか「デザインパターン」とかはよく見聞きするでしょうし、個々のパターンが言及されることは日常でも少なくありません。

パターンはパターンとして名前がついて輪郭を帯びるだけでもかなりの力を発揮します。 そのため「特定の状況のこと」をパターンと呼んでいることもしばしばあります。

これはこれで間違いないのですが、パターンの力はそんなものではありません。 パターンにはコンテキストやフォースなどもありますが、大きいのは「パターンはパターン間で繋がる」ということです。 パターン間が繋がるとテコの原理が働き、相乗効果が生まれます。 一つのパターンだと語りきれない状況もパターンを組み合わせて網目を構築すると掬えたりしますし、新たなパターンで補うこともできます。

「これパターンだっけ?」と思ったら、そのパターンの原典をあたるなりAIに聞くでもいいので、関連パターンに意識を向けてみましょう。 きっと新たな発見があります。

ちなみにChatGPTさんに「壊してよいオモチャの関連パターンは?」と雑に投げると、以下が返ってきました。

書籍で記載のあるものとかぶっていたりいなかったりですが、まぁ確かにという感じ。どう関連あるかとかも言ってくれるのでよかですね。

まとめ

好きなAIで好きにまとめて。←

いや正直、人が書くブログなんて思考を原液で垂れ流すくらいのほうが今後は価値あるんじゃないかなぁって思ったりするわけですよ。

あ、 "壊してよいオモチャ" って言う言葉から個の何かをイメージしがちなんだけど、部分的に「ここは "壊してよいオモチャ" にできるな」みたいなのがあります。 たとえば仕事の中でプロダクトでは難しくても、テストコードだとかCI環境だとかは "壊してよいオモチャ" として、壊れてしまうかもしれないチャレンジをしがいがある場所です。うまいこと設計すればプロダクトの中でも "壊してよいオモチャ" 領域は作れます。仕事の中で "壊してよいオモチャ" で遊びながら力もつける、なんてことを無意識にできるようになると、経験値も爆上がりでタイパも良いです。と言うかそう言うことができないと「仕事外の時間で勉強しなければならない」みたいなことになっちゃって、なんともだよねって思ったりします。

まとめセクションに発散することを書くんじゃない。