なぜあなたの仕様書は"伝わらない"のか ── 一意性という落とし穴
この記事の対象:仕様書・設計書を書く/読む立場にいる、組込・通信系のエンジニア。設計レビューやテスト設計に関わる人。
この記事のゴール:「受信時、一定時間以内にレスポンスする」のような一文が、なぜ仕様定義者・設計(実装)・テストで別物になるのかを具体的に分解し、用語を一意に定義して書き直す観点を持ち帰る。
前提知識:UART などシリアル通信の基本(フレーム、ボーレートの概念)、ソフトウェア設計・テストの一般的な流れ。
この記事で扱わないこと:仕様書全体の構造化、仕様の正規化(重複定義の排除)。本稿は「仕様書・設計書など技術文書の一意性について」を範囲とします
1. はじめに
ある通信仕様書に、こう書かれていました。
受信時、一定時間以内にレスポンスする。
仕様定義のレビューで、この一文に反対した人はいませんでした。みな「わかる」と頷いて、先へ進みました。
それから時間が経ち、結合の段になって、応答が間に合わないケースが、出たり出なかったりし始めます。関係者を集めて確認すると、こうなりました。
仕様を書いた本人は言います。「一定時間以内にレスポンスする、と書いたとおりです。それ以上でも、それ以下でもありません。」
設計・実装を担当した側は言います。「受信してから応答を出し始めるまで、ちゃんと時間内に収めています。」
テストを担当した側は言います。「いえ、波形で測ると時間内に収まっていません。仕様違反です。」
三人とも、嘘はついていません。それぞれが、あの一文を素直に読み、自分の読みに忠実に仕事をしています。そして三人の言い分は、あの一文と、どれも矛盾しません。
では、何が問題だったのでしょうか。
これが、開発の現場で恐れられる「伝言ゲーム」です。同じ一文が、人を経るたびに別の意味になり、最後に突き合わせると食い違う。そして、この食い違いこそが、不具合の温床になります。
この記事は、たった一文のタイミング要求が、なぜ仕様定義者・設計(実装)・テストで別物になるのかを、一緒に解きほぐしていく読み物です。おそらくあなたも、これとそっくりな一文を、書いたか、レビューで通したことがあるはずです。
2. 「受信時、一定時間以内にレスポンスする」を分解する
まず、あの一文を品詞で見ます。名詞は「受信時」と「一定時間」。動詞は「レスポンスする」。一見、なにも難しくありません。しかしこの三つは、どれも複数の意味に割れます。割れ方を見る前に、今回の題材をはっきりさせておきます。
題材は、機器内の CPU 間(ユニット間)通信です。物理層は UART のようなバイト列で、その上に独自の通信フレームを載せます。流れはこうです。
- 受信側はバイトを 1 つずつ受け取る。
- 先頭バイトに、そのフレームの長さ(DLC:データ長)が数ビットで入っている。受信側はその数だけバイトを受け続け、数がそろったらフレーム完成とみなす。
- 完成したフレームの CRC(チェックサム)を検証する。
- 検証に合格したら、フレームから信号(コマンド)を取り出し、制御へ反映する。
フレームの終わりを「一定時間バイトが来なかったら完了」とするタイムアウト方式は採りません。バスの時間効率が悪くなるからです。先頭の DLC で長さを宣言し、その数で完了を判定する。インターフレームのバイト間隔が規定値を超えたら、それは正常な完了ではなく、タイムアウトエラー(エラーフレーム受信)として扱う。これが筋のいい設計です。
この通信を、三つの層で見ます。
- L1 物理層(バイト/ビット):スタートビット、1 バイト受信、STOP ビット、パリティ、フレーミングエラー、オーバーラン。
- L2 データリンク層(フレーム):DLC ぶんのバイトを連結してフレーム完成、CRC 検証。
- L3 アプリ層(信号/制御):検証済みフレームから信号を抽出し、制御へ反映。
この層を物差しに、三つの語を見ていきます。
2.1 「受信時」── どの層の話か
「受信時」が指しうる瞬間は、層ごとに違います。
- L1:1 バイトを受信した瞬間(あるいはスタートビットを検出した瞬間)
- L2:DLC ぶんのバイトがそろってフレームが完成した瞬間/CRC 検証に合格した瞬間
- L3:信号として認識し、制御へ反映した瞬間
「受信時」とだけ書けば、読み手はこのどれを指すかを、自分で選ぶことになります。L1 で読む人と L3 で読む人では、起点が数十ミリ秒ずれます。
2.2 「一定時間」── どこからどこまでか
「一定時間」には、数値(何 ms 以内か)の問題と、区間(どこからどこまでか)の問題があります。割れるのは後者です。
起点の候補:受信開始(要求の最初のバイト)/受信完了(フレーム完成)。
終点の候補:送信開始(応答の最初のバイト)/送信完了(応答の最後のバイト)。
起点 2 通り × 終点 2 通りで、最低 4 通りの区間があります。数値が同じ「100ms 以内」でも、どの区間を指すかが決まっていなければ、要求は確定していません。
2.3 「レスポンス」── 何をもって応答したか
「レスポンスする」も、完了の定義が割れます。応答フレームの先頭バイトを送り始めた時点/全バイトを送り終えた時点/さらに踏み込めば相手が受信し終えた時点。フレーム単位で見るか、バイト単位で見るかでも変わります。
2.4 そして「誰の時計で測るのか」
最後に、見落とされやすい軸です。この通信には相手(要求側の CPU)がいて、相手も自分のタイムアウトを持っています。相手は「自分が要求を送り終えてから、応答を受け終わるまで」で測るかもしれません。すると、こちら(応答側)が「受信完了から送信開始まで」を守っても、応答の送信時間ぶんは相手のタイムアウトに含まれます。応答側のローカルな 100ms と、要求側が許す 100ms は、同じ 100ms ではありません。
タイミング要求は、二つの観測点と「誰の視点か」を決めて、はじめて意味が一つに定まります。
3. 同じ回路、同じ実装、それでも判定が割れる
抽象論だと飲み込みにくいので、具体的な数値で見ます。以下はあくまで例示で、特定の実機の実測値ではありません[注1]。自分の条件に置き換えて読んでください。
条件(例):
- 物理層:UART、9600 bps、8N1(1 バイト = 10 ビット → 約 1.04ms / バイト)
- 要求フレーム 20 バイト → 約 20.8ms
- 応答フレーム 20 バイト → 約 20.8ms
- 応答側の処理時間(CRC 検証〜信号化〜応答生成)40ms
- 伝搬遅延は短距離バス前提で無視できる程度
要求の先頭バイトがバスに出た瞬間を t = 0 とします。先頭バイトで DLC が分かるので、受信側は 20 バイト目で「フレーム完成」と判定できます。時間軸はこうなります。
| 時刻 | 出来事 |
|---|---|
| 0 ms | 要求の先頭バイト(受信開始 / DLC 判明) |
| 20.8 ms | 要求の最終バイト = DLC ぶん受信完了(フレーム完成) |
| 60.8 ms | 応答の先頭バイト送信開始(送信開始) |
| 81.6 ms | 応答の最終バイト送信完了(送信完了) |
この一回のやり取りを、三者がそれぞれの読みで測ると、こうなります。
- 仕様定義者の意図(受信完了 → 送信完了):本当に欲しかったのは「要求を受けてから、応答を最後まで返し終えるまで」。81.6 − 20.8 = 60.8ms
- 設計(実装)の読み(受信完了 → 送信開始):「受けてから、応答を出し始めるまで」と読んだ。60.8 − 20.8 = 40.0ms
- テストの読み(受信開始 → 送信完了):バス上で観測できる、端から端まで。81.6 − 0 = 81.6ms
要求値が「100ms 以内」なら、三者とも合格です。問題は表面化しません。
では、要求値が「50ms 以内」だったら、どうなるか。
- 設計(実装)の読みでは 40ms → 合格
- 仕様定義者が本当に欲しかった 60.8ms → 不合格
- テストの読みでは 81.6ms → 不合格
回路は同じ、実装も同じ、波形も同じです。それでも、読みが割れているだけで、判定が割れます。しかも厄介なのは、設計(実装)は「仕様書どおり」に作って、自分の読みでは合格していることです。仕様定義者が本当に実現したかった時間は満たせていないのに、書かれた文には違反していない。テストは観測点で測って不合格を出す。三者の判定が割れ、そして誰も嘘をついていません。
さらに §2.4 の「誰の時計か」を重ねると、もう一段崩れます。要求側のタイムアウトが「要求を送り終えてから応答を受け終わるまで」で、その値が 70ms だったとします。この例では 81.6 − 20.8 = 60.8ms なので間に合います。しかし要求側が「要求を送り始めてから応答を受け終わるまで」で測り、70ms で切るなら、81.6ms > 70ms で、応答側が "100ms 以内" を守っていても、要求側のほうが先に諦めます。しかも応答側のログには異常が残りません。これが、視点を決めずに書いたときの、最もたちの悪い不具合です。
4. 誰も悪くない、そして「工学的な想像力」
ここで、はっきりさせておきたいことがあります。三者の誰も、悪いことはしていません。
仕様定義者は、自分の頭の中では一意なつもりで書きました。「受けてから返すまで」を時間内に、という当たり前のことを書いたつもりだった。設計(実装)は、その文を素直に読み、自分が制御する区間をきっちり守った。テストは、誰の目にも見える観測点で測った。三者とも、自分の責務に忠実です。手を抜いた人はいません。
では、どこで止めるべきだったか。本来は、仕様定義のレビューです。そしてそのレビューには、仕様定義者自身を出席させることが要ります。文を書いた人がその場にいて、「受信時とはどの瞬間か」「一定時間はどこからどこまでか」を問われて答える。それだけで、幅の多くは潰せます。
ただ、問う側にも力が要ります。私はそれを 「工学的な想像力」 と呼んでいます。「受信後」という抽象を、具体へ演繹的に展開できる力のことです。
ミクロには、こう展開できます。STOP ビットを検出した瞬間か。パリティエラーが出たときはどう扱うのか。STOP ビットの時刻に回線がまだアクティブ(スペース)のままだったら、それはフレーミングエラーなのか、ノイズと見なすのか。オーバーランは。
マクロには、こう展開できます。プロトコル上、相手と疎通できたとはどの状態か。フレームの整合が取れて、信号として認識できたのはどこか。
そして、ここが習熟したエンジニアの差が出るところですが、信号レベルの一つの事象が、そのユニットの挙動に、さらにそのユニットが属するシステムの整合に、どう波及するかを、再現可能な形で頭の中に立ち上げられる。この力があれば、レビューで「その『受信時』は、L1 ですか、L2 ですか、L3 ですか」を、不具合が出る前に問えます。
📝【執筆メモ・公開前に処理】〔要・実体験〕
ここに、自分が現場で見てきた具体例を入れる(顧客名・案件名・量産車種名は出さない。一般化する)。
候補:
- 一生懸命やっているのに噛み合わない場面を、どの立場で、どう観測し、どう切り分けたか。
- 「実装が仕様の意図を汲んでいない」のではなく、「文章に意図を一意に書けていなかった」と気づいた瞬間。
- レビューに仕様定義者を入れて、定義をそろえる前と後で何が変わったか。
※ ここは一般論で埋めない。一次経験が差別化の核。
私自身、こうした場面を数多く見てきました。指摘し、定義をそろえ、ソフトウェア、ひいてはシステム全体を成立させるところまで持っていく。その繰り返しでした。そして毎回、根は同じでした。人の能力や熱意ではなく、意思を共有するための文章に、最低限の一意性がなかったことです。
5. 読み手の認識に、書き手が責任を持つ
ここまでをひと言でまとめます。
一意性のない文章は、意味に幅を持たせ、その幅の中で、読み手に誤読の余地を与えます。
しかも、その誤読はたいてい善意です。読み手は手を抜いて誤読するのではなく、一生懸命に読んだうえで、自分の責務にとって自然な一点を、幅の中から選びます。
やっかいなのは、コンテキストを共有している間は、この幅が表に出ないことです。同じチーム、同じ時期、口頭の補足がある間は、全員が暗黙のうちに同じ一点を見ています。だから仕様定義のレビューでも問題になりません。通ってしまう。
幅が露出するのは、時間が経ったとき、人が変わったとき、別のエンジニアがその文章だけを頼りに作業したときです。口頭の補足は残りません。残るのは文章だけで、その文章には幅があります。
私は、こう考えています。わかりにくさや誤読の余地は、読み手の落ち度ではなく、書き手の落ち度です。 読み手の認識に、書き手が責任を持つ。そして、技術を実装する力と、それを相手に一意に伝える力は、別物です。後者を欠いたまま前者だけを磨いても、一人前のエンジニアとは言いにくい。一意に書くことは、文才ではなく、エンジニアリングのスキルです。
システムが複雑になるほど、文章への要求は厳しくなります。品質を管理する責務を持つ者ほど、この一意性が保たれている状態を、維持し続ける努力が要ります。
6. どう書けば伝わるか ── 用語を定義し、一貫させる
では、どう書けば一意になるのか。文才の話ではないので、手順にできます。考え方は、文を 形態素解析 するのに似ています。
- 要求文を品詞で分解する。 少なくとも、名詞(S)と動詞(V)を洗い出す。「受信時」「一定時間」「レスポンス」を、まず取り出す。
- 相手と共有する用語・動作を、事前に定義する。 「受信」「レスポンス」が、このシステムで何を指すのかを決める。名詞が汎用的すぎるときは、形容詞を付けるか、複合名詞を新しく作って限定する(例:「受信」→「フレーム受信完了」)。
- 定義した語は、文書全体で、その意味以外に使わない。 そして、本当に守られているかを、文書を検査して確かめる。同じ「受信」が、ある章では L1、別の章では L3、になっていないか。
- 一つひとつの語に、システム上の意味を与える。 例えば「受信時」なら、こう詰めます。フレーム受信のことか。スタートビット検出を含むのか。エラー検出時はどう扱うのか。曖昧なまま地の文に溶かさず、定義として一意に決める。
タイミングを表す語については、定義のときに最低限これだけは固定します。
- トリガ事象(どの瞬間か。観測できる点で)
- 計測の起点
- 計測の終点
- 上限値(数値と単位)
- 視点(応答側ローカルか、要求側ラウンドトリップか)
- 観測点(三者が同じ点を測れるよう、物理的にどこを見るか)
これを使って、最初の一文を書き直します。まず用語を定義します。
| 語 | システム上の意味 |
|---|---|
| 受信完了 | 要求フレームを DLC ぶん受信し、CRC 検証に合格した時点 |
| 応答開始 | 応答フレームの先頭バイトの送信を開始する時点 |
| 応答時間 | 受信完了から応答開始までの区間。バス上の信号で計測する |
書き直し前:
受信時、一定時間以内にレスポンスする。
書き直し後(例):
応答時間(受信完了から応答開始まで、バス上の信号で計測)を 100ms 以内とする。なお、要求側のタイムアウト(要求送信完了から応答受信完了まで 200ms)は別に管理する。
長くなったように見えます。しかし増えたのは文字数であって、解釈の幅は消えています。「一意」は「冗長」とは違います。目的は文を長くすることではなく、解釈を一つに定めることです。文章で書ききるのが重いなら、観測点を 1 枚のタイミングチャートで示すのが、たいてい最速です。バス上のエッジに矢印を引いてしまえば、仕様定義者も、設計(実装)も、テストも、同じ点を測れます。ソフトウェアの仕様を、物理的に観測できる点へ接地させる、ということです。
7. この記事で扱わなかったこと
本稿は「一文の意味の幅」だけに絞りました。次の二つは、関連はしますが、別の問題なので扱っていません。
- 構造化:仕様書全体をどう構成し、章立て・粒度・参照関係をどう設計するか。
- 正規化:必須の仕様が複数箇所に重複定義されることを許さず、一箇所に「正」を置くか。
一意性は「一つの記述の意味が一つに定まるか」、構造化と正規化は「記述の集合をどう配置するか」の話です。レイヤが違います。それぞれ単独で扱う価値があるので、別の機会に分けて書きます。
8. まとめ
- 一文の中の名詞・動詞は、しばしば隠れた分岐点です。「受信時」「一定時間」「レスポンス」は、L1 / L2 / L3 のどこを指すか、どこからどこまでか、何をもって完了か、で割れます。
- 同じ仕様書から、仕様定義者・設計(実装)・テストが、それぞれ違う一点を読みます。突き合わせると食い違う。これが伝言ゲームであり、不具合の温床です。
- 読みが割れていれば、同じ回路・同じ実装でも、判定が割れます。設計(実装)が「仕様書どおり」に作ってなお、仕様定義者の意図を外すことすらあります。
- 誤読の余地は、読み手ではなく書き手の責任です。一意に書くことは、エンジニアリングのスキルです。
- 書き方は手順にできます。用語を品詞で分解し、事前に定義し、文書全体で一貫させ、検査する。
- 明日からできること:自分が書いた、あるいはレビューで通したタイミング要求を一つ取り出し、本稿の六項目で点検してみてください。一つでも欠けていたら、それは読み手が誤読できる余地です。
注記
[注1](3章「同じ回路、同じ実装、それでも判定が割れる」)本文のバイト時間は、UART 8N1 で 1 バイト = 10 ビット(スタート 1 + データ 8 + ストップ 1)として、ボーレートで割った算術上の例示です。9600 bps なら 10 / 9600 ≈ 1.04ms / バイト。記載の数値は特定の実機の実測値ではありません。