はじめに
どんなソフトウェアエンジニアも拡張しやすくメンテナンスしやすいソフトウェアを作りたいと思っているはずです。また、どんなプロダクトマネージャも同様に拡張しやすいシンプルな要求を作りたいと考えているはずです。
しかし、将来の不確実性や発展性に対して見通しを立てるのは難しいものです。そのため、開発チームの思いとは裏腹にソフトウェアの複雑性はどんどんと増大していきます。気がついたら技術的負債と呼ばれるような手もつけられない泥団子になってしまうということもしばしばです。誰もが生産性を下げるために機能を追加したいわけではなく、ビジネス価値を提供するために機能を追加したいだけなのにです。
このような状況を避けるためにはどうしたらよいのでしょうか。今回はその一つの手段として、要求には隠れた「意図」があり、それを発見していくことの重要性についてまずはお話しします。さらにわかりやすい要求が持つ仕様の「直交性」というお話をします。そしてこの2つは相互に関連しているのだということをサンプルケースを用いて解説していきます。
ソフトウェアの複雑性を増大させてしまう原因を理解し、言語化していくことでメンテナンスしやすくスケールする要求仕様を作ることにつながれば良いなと思っています。
要求の「意図」するところ
プロダクトバックログなどの要求文書には必ずWHY(なぜ)をつけましょうというのは口を酸っぱくして言われます。でもそれは「なぜ」なのでしょうか。たしかに要求文書にその要求をする背景が描かれていると文脈を補完することがしやすいです。さらには、企画の背景がわかればもっと良い機能を提案をできるかもしれません。
つまり、「要求の意図をはっきりさせましょう」という指針は、実装そのものに直接影響があるわけではなく、あくまでコミュニケーションをする上での補間的な役割として「WHY」をつけた方が良いということなのだと考えられています。
ところが、実際には、「WHY=意図」が記述されている要求のメリットはそれだけではありません。その「意図」の記述が、直接ソースコードや設計に影響を与えるのです。つまりは、ソフトウェアの実装として、コード上に何らかの形で書き込まれていくことが多いのです。そして、「意図」がコードに書き込まれているような設計の方が、「意図」が喪失してしまっている状態で記述された設計よりも、拡張性が高いのです。
意図の喪失した要求では、変化の予想がつかない
たとえば、極端な例ですが、「全く意図を喪失した要求」と「意図が残っている要求」を比べてみましょう。
全く意図を喪失した要求には、コードに落とすべき「抽象」が見当たりません。だから、よくわからないので(対話が不可能であれば)何も抽象化せずにコードに落とさざるを得なくなってしまいます。
一方で、「意図」を保持した要求を見てみると、具体の羅列ではなくコードに落とした方がよい「抽象」を発見するのが容易です。そうすると、追加された要求を実装する際にも極めて単純な対応ができます。
このように、どのような要求にもその意図に基づいて抽象構造が発見され、それが実際のコードに落ちていくことになります。つまり、要求の意図というのはその背後にある抽象構造を発見するための重要なヒントであるし、しばしば抽象構造そのものであったりします。このような対象とする問題を解決するための抽象構造のことをドメインモデルといいます。
実際には、ここまで極端に「意図」が喪失していることはほとんどありません。しかし、要求における「意図」と設計における「抽象構造」には切っても切れない関係があります。要求における「意図」が深く記述されていると、それだけ設計における「抽象構造」の発見しやすさが増大するのです。もう少しだけ、具体的な例で見てみましょう。
意図がドメインモデルの発見を促す
極端すぎるケースで考えるとピンときにくいかもしれません、もう少し具体的なケースで意図のあるなしがどのような影響を与えるかみていきましょう。
たとえば、次のような要求があったとします。
- 利用アカウントは、メールアドレスと電話番号を持つ。
- メールアドレスは必須ではなく任意。電話番号は必須ではなく任意である。
- ただし、作成時、追加時、編集時にどちらもない場合はエラーとする。
- どちらも入力されている場合はメールアドレスのほうにメッセージを送る。
このような要求があった場合、一般的なエンジニアはメールアドレスと電話番号を持つクラスを定義し、どちらもオプショナルな値(必須ではない)にするような設計をするかと思います。そして、作成時・追加時・編集時にどちらもなければエラーにするというバリデーションを実装することでしょう。つまり、要求をそのままに実装に落としていくという意味です。要求自体はかなり具体的に書いてありますから、そのまま実装に落とすこと自体が悪いとは言い難いでしょう。
一方、なぜメールアドレスと電話番号の2つを利用アカウントが持っているのかという「意図」をよりはっきりと記述してもらった次のような要求を見てみましょう。
- 利用アカウントには、最低1つは運営からのメッセージが送信できる連絡先がほしい。
- 連絡先手段としては、メールアドレス、電話番号の2つがある。
- 連絡先手段のうち1つをメッセージを送信するプライマリ連絡先とする。デフォルトはメール。
要求を発する側の意図としては、ただ単にメールアドレスや電話番号が独立して欲しいと考えていたのではなくて、利用アカウントごとに1つは「連絡先」が欲しい。ひいては、何らかの手段で連絡できるといいなと考えていたことがわかります。
このような要求定義の場合、利用アカウントが持つべき実装として「連絡先」があり、そのリストの1つとしてメールアドレスと電話番号があるのだとすぐに理解することができます。その結果、連絡先という抽象構造を利用アカウントに持たせるのが自然な実装になります。
もし、「意図」が要求定義にはっきりと記述され、それを理解した上での実装がされていると、このような「意図」に基づいた新たな要求に対するレジリエンス(耐久性)が上がります。
たとえば、
- 新たに連絡先としてLINEアカウントを足したい
- 複数のメールアドレスを登録できるようにしたい
- 連絡先が認証済みかを管理できるようにしたい
といった要求が新しくきても「連絡先」と「最低一つは連絡先が欲しい」という意図はなんらかの抽象構造が存在するコードであれば対応は比較的容易になります。
一方で、意図が喪失してしまった要求で実装された場合、連絡先という抽象構造が見出されずに純粋に利用アカウントの各フィールドとして実装することが自然になるでしょうから、他の多くの場所でメールアドレスが一つしかないという前提の実装が各所に入ってしまい、なかなか修正しづらいという現象が発生してしまいます。
このように要求定義において意図の明確化は、「未来を予言して拡張性を組み込む」ことができない場合においても、「将来の不確実性」に対応する手段になるのです。ですから、マーケットが不確実で途中でさまざまな施策を考えなければならないケースであっても、ビジネスオーナーや関連するステークホルダーのシステムにそのような要求をする「意図」が発見できるようなコミュニケーションが重要になります。
存在論的抽象と目的論的抽象
この要求定義における「意図」は、自明のものではありません。ドメインエキスパートや開発者との対話の中で発見的に見出されることが多いものです。要求における意図とは、ドメインモデルのような抽象構造と極めて類似しています。
一般的に抽象と具体の話をすると「ヒラメ」と「シイラ」はどちらも「魚類」だから、抽象クラスは魚類クラスだというような静的で問題領域によって変化しないような自明な関係性の抽象をイメージすることが多いかと思います。このような抽象を オントロジー的な抽象、あるいは存在論的な抽象と呼びましょう。
それに対して、「シャケ」と「豚」を見たときに「脊椎動物」を見出すのではなく、「今日の夕飯のメイン食材」という共通する構造を見出すような抽象も考えられます。このようなある場面での目的に合わせて、事後的に発見される抽象構造をテレオロジー的な抽象、あるいは目的論的な抽象と呼ぶことにしましょう。
実は、ソフトウェアアプリケーションを作っていく場面で重要なのは、存在論的抽象というよりもむしろ、目的論的な抽象になります。
しかし、この目的論的な抽象というのは、(例えば先ほどの例でいうと「電話番号」と「メールアドレス」から見出される「連絡先」といったものは、)自明ではなく問題領域が特定されたときに事後的に発見されることになります。
この発見を促しやすくし、また将来に備えたソフトウェアを作るためにも、要求定義にはできる限り「WHY=意図」を記述することが求められるのです。また、同時にソフトウェア開発者も「意図の喪失した要求」を発見したときに、隠れた意図を抽出して発見していくようなアクションが求められます。
プロダクトマネージャの立場になってみたら、なぜこのような仕様にしたいのかが十分に考えられておらず、なんとなくということも多くあるでしょう。そのような場合に、喧嘩腰になることなく、実際に何がしたいのか、なんでそうしたいのかをうまく聞き出していくことが重要です。それができるとその背後にある隠れた意図を発見しやすくなります。
仕様の直交性
ここでソフトウェア仕様の直交性という概念を説明します。この直交性という概念を理解すると、ソフトウェアを「複雑にしてしまう要求」と「あまり複雑にしない要求」の正体が判明するようになります。そして、仕様の直交性と要求の意図には関連性があるのです。
2つの機能は直交している = 2つの機能が無関係に動作すること
たとえば、テレビのリモコンを考えてみましょう。チャンネルを変更する⏫⏬ボタンと、音量を変更する⏫⏬ボタンのそれぞれがあったとします。この2種のボタンは直交しています。
つまり、音量のボタンをどのように動かしてもチャンネルを変更する機能に影響は与えません。逆にチャンネルのボタンをどれだけ動かしても、音量を変えることはできません。このように互いに無関係で影響を及ぼし合わないことを「直交している」と言います。
ここでソフトウェアにおける直交性に関してThe Art of Unix Programmingから引用します。(日本語訳はGoogle翻訳を筆者修正)
直交性 は、複雑な設計でもコンパクトにするのに役立つ最も重要な特性の1つです。純粋に直交する設計では、操作に副作用はありません。各アクション(API呼び出し、マクロ呼び出し、言語操作のいずれであっても)は、他のアクションに影響を与えることなく、1つのことだけを変更します。制御しているシステムの各プロパティを変更する方法は1つだけです。
モニターには直交コントロールがあります。コントラストレベルに関係なく明るさを変更でき、(モニターに1つある場合)カラーバランスコントロールは両方に依存しません。明るさのノブがカラーバランスに影響を与えるモニターを調整するのがどれほど難しいか想像してみてください。明るさを変更した後は、毎回カラーバランスを調整して補正する必要があります。さらに悪いことに、コントラストコントロールがカラーバランスにも影響を与えているかどうか想像してみてください。次に、両方のノブを正確に正しい方法で同時に調整して、コントラストまたはカラーバランスのいずれかを、もう一方を一定に保ちながら単独で変更する必要があります。
http://www.catb.org/~esr/writings/taoup/html/ch04s02.html#orthogonality
このような直交性の定義は、具体的な数学的定義に裏付けられたものではなく、ある種のジャーゴンとして古くから使われてきたものです。
そのため、厳密な議論が難しいのですが単純な捉え方をすると機能同士が相互に疎結合な関係である場合には、複雑性の増加が抑えられ「直交性が高い」、相互に密結合な関係の場合は複雑性が爆発するような「直交性が低い」という機能同士の関係のことを意味しています。
複雑性が足し算になるか、掛け算になるか
完全に2つの機能が直交であれば、機能同士は無関係ということになり、独立してテストすることができます。そうでなく、お互いに関係しあって1つの機能を実現している場合、それぞれの機能の組み合わせの分だけ複雑性が増大します。ここでいう複雑性は、ホワイトボックステストで必要十分なテストケースの数を考えれば良いでしょう。
たとえば、直交性の高い仕様の組み合わせとしてある機能制限のルーチンを考えてみましょう。
この機能はつぎの4つの機能の合成です。
- 運営からの制限があるか否か
- 年齢制限に当てはまるか否か
- 相手からブロックされているか否か
- 高負荷のため停止中か否か
それぞれ2通りの可能性がありますので、全部で2^4=16通りのパターンが考えられます。本来なら16通りを全てチェックする必要があります。
しかし、これらの機能は、お互いが影響を及ぼし合うことがなく「どれか1つでも当てはまれば機能停止」という関係で合成されています。
このとき、互いに影響を及ぼし合わないという知識を前提にすれば、
(0,0,0,0)
(1,0,0,0)
(0,1,0,0)
(0,0,1,0)
(0,0,0,1)
の5通りのチェックさえできていれば、機能の確認としては十分だと言えます。(もちろんブラックボックステストとしては16パターンの可能性があれば、全てチェックすることもありえます。)多くの場合、このような明示的・暗黙の知識によってテストケースは実際に存在しうるすべての組み合わせよりも小さいケースだけが検査されます。いずれにしても、このように機能の数が増えても、検査の数が足し算でしか増えていかないような仕様を「直交した」ないし「直交性の高い」仕様という言い方をします。
逆に直交性が低い仕様は組み合わせで結果が変わります。
たとえば
- 年齢が13歳未満、20歳以下、20才以上、年齢確認済み(4パターン)
- 性別が男性、女性、その他(3パターン)
という情報に基づいて、その組み合わせである機能が使えるか使えないかが決まったとします。このときは43の12パターンの組み合わせでテストする必要が出てきます。もし、仮に職業が(学生、社会人、主婦)の3パターンも組み合わせに追加された場合には、123=36パターンに膨れ上がってしまいます。このように機能を追加するたびにお互いの関係が直交していないと組み合わせが膨大になりソフトウェアは一瞬で複雑になってしまいます。
隠れた意図が直交性を上げる
実は、この仕様の直交性の問題と要求の「意図」には関係があります。
先程の直交していない要求の意思決定テーブルを見てみると、いくつかの気づきがあります。
(この表自体は、乱数で作ったので大元の意図は実はないのですが)
- 年齢確認済みなら全部使えていいのでは?
- 女性は年齢確認済みじゃなければ使えないことに何か意図があるのでは?
もし、このような意図が存在していれば、何らかの直交した機能の組み合わせに変えることもできるかもしれません。
ある具体的なケースを用いて、この直交性と意図の関係に迫っていきましょう。
チケット料金モデリングのケーススタディ
DDD(ドメイン駆動設計)のテストケースとして一時期話題になっていた映画館の料金プランがあります。
https://togetter.com/li/1378684
ここで紹介されているモデリングもとても面白いのでぜひ読んでみてください。
今回はモデリングというよりも、この料金表から隠れた要求の意図をリバースエンジニアリングしてみます。
この例をケーススタディとして、要求の意図と直交性について探っていきましょう。
まず、こちらが対象の映画料金表です。
こちらをみると、縦軸に10種類のお客さんのタイプと横軸に平日土日、映画の日、レイトショーなどの時間帯の5つのカテゴリがあり、それぞれの組み合わせで料金が決まっているように見えます。
そのため、この表は50通りのパターンが存在する要求なのだと理解できます。
しかし、実際にはこの表は「システムの要求そのもの」ではありません。単にお客様に映画の料金をトラブルがないように伝えるための表に過ぎません。この表からは料金設定時に考えられた「意図」は喪失してしまっているのです。なので、これからその意図を発掘して、50パターンという複雑性をどれだけ減らしてシンプルな要求にリバースエンジニアリングできるかを考えてみましょう。
まず最初に注目するのは、標準的な時間(平日20時まで)のときにお客さんのカテゴリでどう変わるかです。
基本料金が1800円で、何らかの資格情報を店員さんに提示して料金を変えるわけですから、そこには「値下げしたい」という意図があるはずです。さもなければ、お客さんは自分で値上げのために情報を開示することになってしまいます。そんなことは誰もしませんし、設計者の意図もそこにはないはずです。
実際にどの顧客区分であっても1800円よりも安くなっています。
1. [基本料金]は1800円である。
2. [顧客区分]によって、[基本料金]を値下げしたい。これを[顧客区分割引価格]と呼ぶことにしよう。
なので、このような意図が隠れているのだと仮定してみましょう。
こちらも同じように時間帯によって、料金を値下げしたいという意図を汲み取ることができます。
3. 時間帯に応じて、基本価格を値下げしたい。これを[時間帯割引価格]と呼ぼう。
そこでこのようなルールを仮定してみましょう。
このとき、2つの値下げルールがあることになりました。[顧客区分割引]と[時間帯割引]です。
2つの割引を共存させるときに組み合わせ方のルールとして考えられるのは
- どちらかの値引額の安い方を優先する
- 両方の値引き額の合計を引いた値段にする
の2通りでしょうか。当然、高い方にするとか、そもそも値引きを適用させないとかそういう方法もあるにはあるのですが、「お客様に損をさせたくない」「つまらないことでトラブルを避けたい」と意図するのはビジネスルール上自然かなと思いますので、この2通りがまずは考えられるでしょう。
そのように考えてみると、結果的に安い方を優先するのが今回のケースでは当てはまりが良さそうです。
4. [顧客区分割引価格]と[時間帯割引価格]の両方が当てはまる場合、結果の料金が安い金額にする。割引プランでお客さんが損をしないようにしたい。
たとえば、大学生がレイトショーに行くケースを考えてみましょう。
大学生の場合、基本料金から300円引いた1500円が**[顧客区分割引料金]になります。また、レイトショーでは500円引きですので、1300円が[時間帯割引料金]**になります。
このとき、より安い方で提供したいので1300円が映画料金となります。これで、直交する2つの仕様の合成でほぼ全てを表現することができます。
直交した仕様の合成だけで表現できれば、組み合わせの数は極めて少なくなります。
シネマシティズンのみに限って、通常であれば1000円になるところ、1300円になっています。少し高いのです。
ここでまた意図を考えてみます。
5. でも、土日の昼間は混み合うので、シネマシティズンはちょっと高くして遠慮してもらえると助かるなぁ。シネマシティズンの土日20時までの価格は1300円にしよう。
(直交性の乱れ)
きっとこのようなことではないでしょうか。
一旦このイレギュラーを認めた上で、整理してみましょう。テーブルのままだとすべてのパターンがなぜそうなるかが不明確だったので、50パターン分のテストが必要でした。
しかし、意図をリバースエンジニアリングして、それらを反映させた要求にしたら、10個の顧客区分割引と2個の時間帯割引、そして1つのイレギュラールールという合計13パターンのチェックで全ての要求を表せることになりました。
複雑に見えたものが意図を発見することでシンプルな直交した仕様に分解することできるのです。しかし、このケースがそうであるように意図が発見されないまま拡張されていった要求というのはしばしば直交性が破れている箇所が存在します。このような部分があるからといって直交した実装をあきらめるのではなく、イレギュラーとして一度分離した上で、そこにはビジネス上の新しい意図が隠れていないかを注意していくことが重要です。たとえば、混み合い具合によって料金を柔軟に変えたいとか、そういうことが隠れているのでは?とそう考え始めてコミュニケーションを取るのも良いかもしれません。
拡張性が高いのはどちらか。
さて、このような表で表現されていたものを、直交した仕様に分解した場合、ソフトウェアの拡張性はどうなるのでしょうか。時間帯 x 顧客区分で映画料金が決まるのだから、その表を用意したほうが”拡張性”が高いのでは?と考える人もいるかもしれません。
たとえば、「障害者の土曜日だけ値段を2500円にしたい」という要求がきたら、また例外を増やすべきなのか。そうではなくて、最初から自由に50パターンを業務側が組み替えられるようにすれば良いのではないか。そう考えるのは自然な発想です。
最初から”自由に時間帯と区分を組み合わせたい”が意図されていたら、そうかもしれません。しかし、本当にそうでしょうか。
たとえば、次のような新しい要求を考えてみましょう。
- 顧客区分をもう一つ増やしたい
- 時間帯割をもう一つ増やしたい
- さらに映画館の場所によって割引料金を変えたい
これらの要求に対してどちらが”拡張性が高い”のでしょうか。このような要求がくる場合、表として実装されている方がテスト項目が増え、拡張しにくいことでしょう。
たとえば、こちらの記述
平日なら映画の日関係なく1000円!(シネマシティズンの備考欄)
これは、二つの割引の安い方だとしたら自明です。映画の日には平日のケースと土日のケースもあるためです。このような表で表現しようとしたため矛盾が生じてしまい、それを備考欄で補う形なってしまっているのです。
設計意図は隠れやすいし、提案者本人でさえ気が付いていないこともままあります。
経験上、要求の意図が不明確な部分にこそ、直交性の低い仕様が入り込みます。意図をより明確にし、場合によってはリバースエンジニアリングすることで、拡張しやすく複雑性が爆発しにくい要求を取り戻すことができます。
おわりに
今回は、要求の意図を明確にする重要性と、それを対話によって発見していくプロセスがソフトウェアの複雑性を下げることにつながるという話でした。