若くない何かの悩み

何かをdisっているときは、たいていツンデレですので大目に見てやってください。

仕様駆動開発をする前に押さえておくべき「振る舞い仕様」のこと

仕様駆動開発とは振る舞いに関する仕様を実装に先立って(ときにAIと共同で)書き、その振る舞い仕様をもとに AI に実装を指示する開発スタイルです。ではそのとき AI とともに作り上げる「振る舞い仕様」はどんな形で書くのがよいのでしょうか。この記事では振る舞い仕様の4つのポイントを易しく解説します:

  1. 振る舞い仕様に適したフォーマット
  2. 振る舞い仕様をAIと作成する方法
  3. 振る舞い仕様が意図に沿っているか AI と確認する方法
  4. 実装が仕様通りに振る舞うか AI と確認する方法

仕様とは何か

その前に、そもそも仕様とは何でしょうか。この記事では仕様を次のように考えます。

仕様とは、実装の正解・不正解を判定するための成果物である。

ただこれ以外にも仕様にはいろいろな定義があります。しかし仕様駆動開発で使うなら最低でも実装の正しさを判断する材料になっていなければ困ります。また仕様がAI以外にも誰にどう使われるかを考えると仕様のあるべき姿が分かりやすくなります。

仕様はプログラマーには実装の材料として使われます。テスターにはテストケースを考える材料として使われます。カスタマーサポートにはユーザーが遭遇した事象が意図されたものなのか、それとも不具合なのかを判断する材料として使われます。立場は違いますが、どの場合も仕様は「正しさの基準」として使われています。

したがって仕様として認められるための最低条件は、実装を見たときにそれが正しいのか不正解なのかを判断する基準になっていることです。もちろん、その条件を満たす仕様にも良し悪しがあります。

仕様を見ただけでは判断できず毎回追加で質問しないといけないなら、それはたいてい良い仕様とはいえません。コミュニケーションのオーバーヘッドが大きくなるからです1。ただし仕様を詳しく書くにはコストがかかります。仕様を書くコストとあとから質問に答えるコストは天秤にかける必要があります。

それでも仕様駆動開発では仕様を書く側に寄せた方がうまくいくことが多いです。仕様を書く訓練や環境整備によって仕様を書くコストは下げられます。一方で曖昧な仕様から生じる認識合わせのコストは訓練や環境整備では抑えにくいからです。

ここでは良い仕様を次のように考えます。

良い仕様とは、実装の正誤を判断でき、かつその判断に追加の質問をなるべく要しないものである。

多くの場合では仕様の読み手が毎回人間に聞かないと先に進めない状態を避けたほうがよいでしょう。

今回紹介する振る舞い仕様の表現

この記事では、仕様の中でも外部から観測可能な振る舞いに関して正誤を決める2つの振る舞い仕様を紹介します。 システムの性質によって適した振る舞い仕様の表現は変わります。入力するといつか停止して出力を返すシステムでは入力と許される出力の関係として振る舞い仕様を考えます。何度も入力できて停止しないシステムでは、発生できるイベントの流れと発生してはいけないイベントの流れとして振る舞い仕様を考えます。

前者は表で考えると分かりやすいです。後者はイベントの流れや状態遷移図で考えると分かりやすいです。

これから紹介する振る舞い仕様はアカデミックを背景としてきちんと理論にもとづいたものです。ここではその骨子をなるべく易しく解説します。ただ内容をだいぶ割愛しているので詳しくは末尾の教科書を読んで欲しいです。

変換システムの振る舞い仕様

まず入力するといつか停止して出力を返すシステムを考えます。ここではこのようなシステムを「変換システム」と呼びます。FizzBuzz関数やソート関数、コンパイラなどが変換システムです。

変換システムの振る舞い仕様は入力と許される出力の関係です。表はその関係を一番素朴に表したものです。ただし現実には全行を書けないので、場合分けや数式で圧縮して書きます。

FizzBuzz関数

FizzBuzz 関数を考えます。素朴に書くなら次のような表になります。

入力 出力
1 "1"
2 "2"
3 "Fizz"
4 "4"
5 "Buzz"
6 "Fizz"
... ...
15 "FizzBuzz"
... ...

この表は分かりやすいですが、すべての整数について行を書くのは現実的ではありません。そこで、場合分けで圧縮します。

  • 1 未満の入力は不定
  • 3 でも 5 でも割り切れないときは入力の 10 進数文字列を返す
  • 3 で割り切れ 5 では割り切れないときは "Fizz" を返す
  • 3 では割り切れず 5 で割り切れるときは "Buzz" を返す
  • 3 でも 5 でも割り切れるときは "FizzBuzz" を返す

これは表を圧縮して書いたものです。表で書くか、場合分けで書くか、数式で書くかは表現だけの違いです。意味に変わりはありません。大事なのは入力と許される出力の関係が分かることです。

なお、... で省略すればするほど仕様を書くコストは減ります。その代わり読み手が補完しなければならない部分が増えます。読み手が補完した内容と書き手の意図が一致するとは限りません。

ソート関数

ソート関数も変換システムです。いくつかの例だけを書くなら次のようになります。

入力 出力
[] []
[0] [0]
[0, 1] [0, 1]
[1, 0] [0, 1]
... ...

これもすべての配列について書き下せません。そこで性質として書きます。

  • 出力の要素は入力の要素と同じである
  • 出力ではすべての隣接する要素について前の要素が後ろの要素以下である

この 2 つを合わせるとソート関数の振る舞い仕様になります2。片方だけでは足りません。

「入力の要素と出力の要素は同じ」とだけ書くと並び順の条件がありません。[1, 0] を入力して [1, 0] が返ってきても正解になってしまいます。逆に「出力は昇順である」とだけ書くと入力と関係ない値を返しても正解になってしまいます。[1, 0] を入力して [] を返しても空配列は昇順なので正解になってしまいます。

仕様を書くときは意図した正解だけが正解になるように条件を決める必要があります。

Web API

Web API も、同じリクエストについて同じ範囲のレスポンスが返されるなら変換システムとして扱えます3。たとえば Greeting API を考えます。この API は Authorization ヘッダに紐づくユーザー名を使って挨拶を返すものとします。

入力 出力
Authorization ヘッダがない 401 Unauthorized。Body は自由
Authorization ヘッダに紐づくユーザーが存在しない 401 Unauthorized。Body は自由
Authorization ヘッダに紐づくユーザーが存在する 200 OK。Body は "Hello, " + ユーザー名

異常系も振る舞い仕様に書きます。「Authorization ヘッダがない場合は 401 を返す」という期待があるなら、それは振る舞い仕様に書かなければなりません。

表に入力を書かないということは、その入力に対して実装がどう振る舞っても仕様違反にしないという意味です。エラーを返してもよいし、無限ループなどで何も返せなくてもよいし、クラッシュしてもよい。そういう扱いになります。

これは誤解されやすいところです。「この入力は異常系だから表に書かなくてよい」とはなりません。エラーを返してほしいならエラーを返すことを振る舞い仕様に書きます。このときエラーメッセージはなんでもよいとしておくとよいです。処理系のアップデートでエラーメッセージのフォーマットが小さく変わったとしてその実装は正しいとしておいた方が得だからです。

一般的な Web API では異常系でも何らかのレスポンスを返すことが期待されることが多いです。その場合は異常系も振る舞い仕様に含めるべきです。

1 つの入力に複数の出力がある場合

1 つの入力に対して複数の出力が許されることもあります。たとえばサイコロを振るシステムを考えます。入力は「サイコロを振る」です。出力は 1 から 6 のいずれかです。

この場合、表では同じ入力を複数行書いてよいです。

入力 出力
サイコロを振る 1
サイコロを振る 2
サイコロを振る 3
サイコロを振る 4
サイコロを振る 5
サイコロを振る 6

これは同じ入力に対して複数の出力が許されるという意味です。実装が 1 を返しても正解です。6 を返しても正解です。しかし 7 を返したら不正解です。

振る舞い仕様とは実装がどの出力を返してもよいかを決めるものです。

リアクティブシステムの振る舞い仕様

次に何度も入力できて停止しないシステムを考えます。ここではこのようなシステムを「リアクティブシステム」と呼びます。

Web アプリケーション、モバイルアプリ、GUI、サーバープロセス、自販機、ゲームなどは多くの場合リアクティブシステムとして考えた方が自然です。リアクティブシステムは 1 回入力して 1 回出力して終わりではありません。ユーザーがボタンを押す。画面が変わる。さらにユーザーが入力する。システムが応答する。時間が経つ。他のシステムと通信する。こうしたやり取りが続きます。

このようなシステムの振る舞い仕様はイベントの流れとして考えます。イベントとは外から見て起きたことです。たとえばクリック、タップ、キーボード入力、画面の表示、通信の送受信、時間経過、商品の排出、エラー表示などです。

たとえば検索画面なら次のようなイベントの流れを考えられます。

リアクティブシステムでは何がどの順番で起きるかが重要になります。

カウンター

簡単なカウンターを考えます。ボタンを押すたびに表示される数が増えるシステムです。イベントの流れとして書くなら次のようになります。

これでも雰囲気は分かります。しかし ... が多い仕様は注意が必要です。省略すればするほど仕様を書くコストは減ります。その代わり質問や誤解のリスクは増えます。

状態変数を使って状態をグルーピングすると を使わないようにできます。

状態遷移図では状態そのものは重要ではありません。状態遷移図を使う目的はそこから発生できるイベントの流れを表すことです。実装は振る舞い仕様とはまったく異なる状態でも構いません。重要なのはイベントの流れだけです。

自販機

自販機は変換システムというよりリアクティブシステムとして考えた方が自然です。商品一覧を提示する。貨幣を投入する。購入可能な商品が点灯する。選択ボタンを押す。商品が排出される。お釣りが返る。商品が売り切れになる。たとえば正常系の流れは次のように書けます。

これだけでは足りません。売り切れの商品を選んだらどうなるのか。全部売り切れていたらどうなるのか。管理者が商品を補充するとどうなるのか。現金を回収するとどうなるのか。そうした流れも意図された振る舞いなら振る舞い仕様に書く必要があります。

たとえば売り切れ商品の選択は次のように書けます。

管理者操作を含めるなら、たとえば次のような流れも振る舞い仕様に入ります。

リアクティブシステムの振る舞い仕様でも正常系だけでなくエラーになる流れも書きます。エラーになること自体が意図された振る舞いだからです。

状態遷移図で圧縮する

自販機のようなシステムではイベントの流れをすべて書き下すのは難しくなります。そこで、状態遷移図で表現します。たとえば次のようになります。

この図からイベントの流れがわかります。たとえば Idle から Paid に進み、Paid から ShowingPurchasable に進み、そこから Dropping に進めることが分かります。反対に図にない遷移は、その振る舞い仕様では発生できない流れとして扱います。

状態遷移図では状態よりもどういうイベントがあるか、あるイベントの流れのあとにどのイベントが発生できないのかが重要です。

AI に作成してもらう際には以下のプロンプトを使うといいでしょう:

ユーザーに代表的なトレースを質問し、https://raw.githubusercontent.com/Kuniwak/puml-parallel/refs/heads/refinement/docs/SYNTAX.md を参考に PlantUML で状態遷移図を作成してください。
CSDF ではイベントが英数文字しか許していませんが初学者の読みやすさのために英数字以外(e.g. 日本語)での記述を許します。ガードと事後条件は、ユーザーに適した言語圏の自然言語で記述してください。ガードで参照できるのはチャネルパラメータと事前状態の状態変数と定数だけです。事後条件で参照できるのはチャネルパラメータと事前事後の状態変数と定数だけです。
このとき「X'はX+1になる」より「Xが増える/減る/置き換わる」のように変化を中心に書くと初学者に読みやすくできます。
なお正常系だけでなく異常系も内部選択を用いて漏らさず表現してください。
作成後は PNG 画像にレンダリングしてユーザーに仕様アニメーションを促してください。

内部イベント τ

リアクティブシステムでは外から制御も観測もできないイベントがあります。たとえばサーバーとクライアントの間の隠れた通信、マイクロサービス間の隠れた通信、時間経過などです。このような内部イベントを τ (タウと読む)と書くことがあります。

おみくじのシステムを考えます。ユーザーが「おみくじを引く」ボタンを押すとシステムが内部で運勢を選び、その結果を表示します。

ここで τ はユーザーから見えない内部イベントです。τ は発生を制御できないのでユーザーは「大吉にするイベント」を選べません。ユーザーができるのは「おみくじを引く」、「結果を見る」ことだけです。そのあとどの運勢になるかはシステム内部で決まります。

もし τ を使わずに次のように書いてしまうと、ユーザーが自分で運勢を選べる振る舞い仕様になります。

外側から制御できないイベントは普通のイベントではなくτとして書きます。典型的に通信エラーは外側からは制御できませんから正常系と異常系が τ で分岐します。

コラム:実装は仕様になりえるか

皆さんも自身の仕様の定義を確認するために立ち止まって考えてみてください。仕様と実装の違いはなんでしょうか。実装は仕様になりえるでしょうか。

実はなりえます。実装を見れば、その実装と同じ振る舞いをするものを正しいと判定できます。その意味では実装も仕様として使えます。

しかし実装はたいてい良い仕様ではありません。実装が本来は規定しなくてよい振る舞いまで規定してしまうからです。

仕様が制限するべきなのは外から観測できる振る舞いです。変数名や内部処理の計算順序や内部に隠れたデータ構造は外から観測されないので、そもそも仕様では制限しません。問題になるのは、外から観測できるもののどうなってもよい振る舞いです。

たとえば Web API で「認証に失敗したら 401 Unauthorized を返す」と決めたい場合を考えます。このときレスポンス body の細かい文面まで規定したいとは限りません。

401 Unauthorized
Body は自由

と書けば、ステータスコードは仕様で決めつつ body の内容は実装に任せられます。

しかし、ある実装がたまたま次のような body を返していたとします。

{
  "error": "Invalid authorization header."
}

この実装をそのまま仕様にしてしまうと、エラーメッセージの文法、単語、句読点、大文字小文字まで正解の一部になってしまいます。本当は次のような body でもよかったかもしれません。

{
  "message": "Unauthorized"
}

あるいは body が空でもよかったかもしれません。仕様として決めたかったのが 401 Unauthorized だけなら、body の細部まで固定するのは決めすぎです。

ところが実装をそのまま仕様にすると、その仕様対象外の入力に対する現在の振る舞いまで固定されます。本当は「そこはどうでもよい」と言いたかったのに、実装を仕様にしたことで「そこも同じでなければならない」と読めてしまいます。

これが、実装を仕様にすると正解を狭くしすぎるという意味です。仕様は、決めるべきことだけを決める必要があります。決めなければならない振る舞いを決めないと、本当は不正解にしたかった実装まで正解になってしまいます。逆に決めなくてよい振る舞いまで決めると、本当は正解にしたかった実装を不正解にしてしまいます。

仕様を書くときは「ここは正解・不正解を分ける基準なのか、それとも実装に任せてよい細部なのか」を分ける必要があります。

仕様バグをどう見つけるか

実装にバグがあるように振る舞い仕様にもバグがあります。振る舞い仕様が間違っていれば、その振る舞い仕様に従って実装しても意図したシステムにはなりません。

たとえば自販機の振る舞い仕様を次のように書いたとします。

この振る舞い仕様には問題があります。まず、選んだ商品を記憶していません。そのためユーザーが水を選んだのに、コーヒーが出てきても仕様違反とは言えなくなります。どの商品を選んだかが状態に入っていないからです。これは意図しないイベントの流れが混ざっている例です。

次に商品を選ぶイベントがありません。購入ボタンを点灯させたあとどのボタンを押すのかというイベントがないまま、商品排出に進んでいます。これは意図したイベントの流れが入っていない例です。

さらに売り切れになることもありません。現実には自販機の中の商品数には限りがあります。しかしその情報が振る舞い仕様にないなら、売り切れる振る舞いが許されていません。したがって実装するときに困ってしまいます。

最後に ... のような省略があると、どう埋めればよいか分からなくなるときがあります。商品の補充を含むのか。現金の回収を含むのか。売り切れ処理を含むのか。省略が多いほど読み手の想像に依存する部分が増え、誤解のリスクが高まります。

振る舞い仕様を歩き回る

仕様バグを見つけるには振る舞い仕様を歩き回るのが有効です。表で書いた振る舞い仕様なら自分が想定している入力と出力の組を、仕様を見ずに書き出してみます。そしてそれらが表や場合分けに含まれているか確認します。

状態遷移図で書いた振る舞い仕様なら双六のように状態遷移図を歩き回ります。意図しないイベントの流れが発生できないかを確認します。また、自分が想定している正常系の流れを状態遷移図を見ずに書き出してみます。そしてそれらが状態遷移図に含まれているか確認します。

内部イベント τ だけで進み続ける流れがないかも確認します。

このような流れがあると外から見るとシステムが反応しないように見えることがあります。この無限に続く τ の遷移をできてしまうことを「発散している」といいます。発散する仕様も実装もその実装が仕様通りであることを確認する作業が難しいため、あらかじて発散しないことを確認しておくとよいでしょう。

振る舞い仕様を歩き回る作業は人間だけでやる必要はありません。AI に振る舞い仕様を読ませて意図しない流れがないか、抜けている流れがないかを質問できます。

また仕様の正誤を人間が判断できるなら、歩き回れる仕様を AI に書いてもらうのも強力な手段です。人間はいくつかの代表的な入力やイベントの流れ、達成したいことがらを指示し、AIに推測で振る舞いを埋めてもらってから人間が確認することで振る舞い仕様を書くコストを大幅に減らせます。この場合は「エラーが起こりうるところは τ で正常系と異常系に分岐させること」を AI に念押ししてください。そうでないと AI は異常系を省きがちになります。

実装が仕様通りに振る舞うことを確認する

最後に実装が仕様通りに振る舞うことをどう確認するかを考えます。変換システムの場合は表や場合分けから入力を選び、実装に入力してみます。そして実装の出力が振る舞い仕様で許されたいずれかの出力と一致するかを確認します。

どの出力にも一致しないなら、その入力に対する実装は欠陥です。たとえばサイコロシステムなら、出力が 1 から 6 のいずれかであれば正解です。7 が返ってきたら不正解です。

これは入力条件と出力条件で考えることもできます。

入力条件: 入力が振る舞い仕様の対象である
出力条件: 出力が、その入力に対して振る舞い仕様で許された出力である

リアクティブシステムの場合は実装を実際に動かして、イベントの流れを観察します。実装で発生したイベントの流れが振る舞い仕様で許されたイベントの流れに含まれているかを確認します。振る舞い仕様では発生できないイベントの流れが実装で発生したなら、それは欠陥です。

また、あるイベントの流れのあとに本来なら受け付けるべきイベントを受け付けないなら、それも欠陥です。たとえば振る舞い仕様では「貨幣を投入したあと、購入可能商品のボタンが点灯する」と書いてあるのに、実装ではボタンが点灯しえないなら、振る舞い仕様を満たしていません。

実装が τ による無限の発生でフリーズしないかも確認します。

振る舞い仕様からテストを作る

振る舞い仕様からテストケースを作る作業はAIやプログラムに任せやすい部分です。変換システムなら振る舞い仕様から入力を選び、実装の出力が振る舞い仕様に合っているかを確認できます。リアクティブシステムなら状態遷移図からイベント列を生成し、実装でその流れを試すことができます。

このような確認は、入力やイベント列を自動生成して性質を確認するテストと相性がよいです4。AIやプログラマーに対しては仕様を渡したうえで、「振る舞い仕様からテストを作って」と指示すると伝わりやすいことが多いです。

ただしテストは振る舞い仕様の代わりにはなりません。テストは振る舞い仕様の決めている入力やイベントの流れの中から、ごく一部を選んで試しているに過ぎません。そのため、テストで言及されていない入力やイベントの流れを補完する情報が追加で必要になります。典型的にはテストケースの名前で補足します。詳しくは「テストケースの名前はどうつけるべきか?」を参照してください。

まとめ

仕様駆動開発で重要なのは実装の正解・不正解を少ない誤読や質問で判定できる振る舞い仕様を書くことです。

入力するといつか停止して出力を返すシステムでは入力と許される出力の関係として振る舞い仕様を考えます。表はその関係を一番素朴に表したものです。全行を書けないときは場合分けや式で表現します。

何度も入力できて停止しないシステムでは発生できるイベントの流れと、発生してはいけないイベントの流れとして振る舞い仕様を考えます。すべての流れを書き下せないときは状態遷移図で圧縮します。

振る舞い仕様を書くときは正常系だけでなく異常系も書きます。エラーを返してほしいならエラーを返すことも振る舞い仕様に含めます。振る舞い仕様を書いたら歩き回ります。意図しない流れが混ざっていないか。意図した流れが抜けていないか。実装できない振る舞い仕様になっていないか。省略が多すぎて読み手に解釈を委ねていないか。

AI は振る舞い仕様作成と実装と検証の全てを助けてくれます。しかし最終的に何が正解なのかを決めるのは人間です。人間の指示が曖昧なら AI は当てずっぽうで実装・検証します。また振る舞い仕様が間違っていれば AI は間違った仕様にしたがって振る舞いを実装します。

仕様駆動開発を始める前に、まず振る舞い仕様とは何かを押さえておく必要があります。仕様とは、実装の正解・不正解を判定するための成果物です。その中でも外部から観測可能な振る舞いの正誤を決める基準が振る舞い仕様です。

おすすめの教科書

きちんと理解して使いこなせるようになるには教科書にあたるのが一番です。ここでは変換システムとリアクティブシステムのそれぞれの教科書を紹介しておきます。

変換システムの振る舞い仕様の教科書

リアクティブシステムの振る舞い仕様の教科書

最後に

面白かったら投げ銭お願いします!(有料コンテンツに内容はありません)


  1. 例外はあります。探索的な開発では振る舞い仕様をあえて粗くして会話しながら詰める方がよい場合もあります。
  2. 重複要素を含む場合は「同じ要素」を多重集合として同じ、という意味で読む必要があります。安定ソートを要求する場合は同じ大きさの異なる要素の相対順序を保つ条件も追加します。
  3. Web API 全体を厳密に見ると認証状態、レート制限、外部サービス、時間、リトライなどが絡むため単純な変換システムでは表しにくくリアクティブシステムで表した方がいい場合があります。
  4. property-based testing と呼ばれます。この記事では詳しく扱いません。
この続きはcodocで購入