「テスト戦略」を解説している書籍まとめ

 最近自分の周りで「テスト戦略とは何か」という議論をちらほら見ます。それを見ていて、世の中のテスト本ではどのような定義で「テスト戦略」の言葉を使っているか気になりました。
 そこで今回、テスト戦略について解説のある書籍を、概要とともにリストアップしてみました。


リストアップの対象

 数が多いので、テストの解説本で、紙面化されいて、日本語で書かれている書籍に対象を絞っています(よく引用されるJSTQBだけ例外)。
 なお、一般的に市販されている日本語のソフトウェアテスト専門書はほとんど読んでいると思いますが、忘れたり、手元になかったり、破棄したりした本もあるので、抜け漏れもあるかと思います。

前置き:推薦図書

 リストを書く前に、先に推薦文献や全体の傾向に触れます。
 推薦文献ですが、テスト戦略を学ぶ取っ掛かりとしては、「http://www.jasst.jp/archives/jasst10s/pdf/S1.pdf」「JSTQB Advanced Level シラバス日本語版 テストマネージャ」の2つがお勧めです。両方とも手軽に読めて無料です。
 次のステップは、学びたいテスト戦略の目的やスコープによって変わると思います。たとえばマネジメントを含むテストプロジェクトの戦略を学ぶ場合、以降のリストで「詳細度★★★」にしている書籍は推薦できると思います。テスト設計の戦略ならばとっかかりとして「ソフトウェアテスト 293の鉄則」が、具体例としてリストの後半にあるバイザー本や、HAYST法の本等が良いと思います。

前置き:リストアップした書籍の全体の傾向

 次にリストアップした書籍の全体の概要です。
 多くの書籍は、一般的な「戦略」の定義に則った上で、書籍のテーマの戦略を「テスト戦略」と呼んでいるようです。例えばテスト設計の解説書なら、テスト設計の戦略をテスト戦略と呼称することが多いです。マネジメントの解説書なら、テストマネジメントの戦略をテスト戦略と呼称することが多いです。

 また「テスト戦略」の定義は多少のブレがあります。リストアップした書籍をグループ分けすると、概ね以下の流派がありそうです。

  • 特定プロジェクトに依存せず横断的に適用できる、組織で蓄積したソフトウェアテストの工夫全般(JSTQB
  • 良いテスト設計を行うための、テスト設計の段取りや技術の戦略(293の鉄則、ISO29119など)
  • テストプロジェクトを成功させるための戦略全般(RexBlackの書籍など)
  • テストプロジェクトを成功させるための戦略のうち、リソース配分と段取りに関するもの(TPI NEXTなど)
  • テスト設計技法(バイザー本などの古典)

前置き:凡例

 整理のため、書籍解説では以下の凡例に基づいて項目分けしています。

  • 【解説の詳細度】として、テスト戦略の直接的な解説の詳しさに応じて★を大まかに付けています。★が多いほど解説が多いです。
  • 【解説目的】として、本でテスト戦略を解説している目的を、以下から複数選択で記載します。
    • 定義解説:テスト戦略の用語定義の解説を目的とする
    • グッドプラクティス解説:良いテスト戦略についての、普遍的な原則やパターン、プラクティスの解説を目的とする
    • 事例解説:特定のプロジェクトや組織で実践したテスト戦略の解説を目的とする
  • 【遂行手段】として、テスト戦略を遂行するために工夫・実践する活動を、以下から複数選択で記載します。
    • スケジュール・リソース:マネジメントに含まれる、スケジュール・人員・環境確保・組織体制の戦略を策定する
    • テストプロセス:テストプロジェクト全体のプロセスの戦略を策定する
    • テスト設計:テスト設計や、テスト設計に限定したプロセスの戦略を策定する
    • テスト実装:テスト実装の戦略を策定する

テスト戦略を解説した書籍(タイトルが詳細へのリンクになっています)

「実践ソフトウェアエンジニアリング」

【解説の詳細度】★★★
【解説目的】定義解説、グッドプラクティス解説
【遂行手段】スケジュール・リソース、テストプロセス、テスト設計、テスト実装
【特徴や留意点など】
ソフトウェアテストの戦略とは、品質の高いソフトウェアが開発できるように、テスト設計手法を手順や計画としてまとめあげることである」と記述。
良いテスト設計をするためのテストプロジェクトの戦略を、テスト戦略と呼称している。テスト設計の進め方がメインだが、テスト設計を円滑にするための組織構成やプロジェクトの段取りもテスト戦略に含まれる。

「ソフトウェアテスト12の必勝プロセス」

【解説の詳細度】★★★
【解説目的】定義解説、グッドプラクティス解説、事例解説
【遂行手段】スケジュール・リソース、テストプロセス、テスト設計、テスト実装
【特徴や留意点など】
テストプロジェクトとテストチームの目標または任務に沿って採用した手法及び意思決定の総体」をテスト戦略と記述。「テストを実行する時の具体的な方針、技法、プロセス、方式」はテスト戦術として、テスト戦略と区別している。
プロジェクトを成功させるためのテストプロジェクトの戦略を、テスト戦略と呼称している。環境確保、予算の工夫など広い範囲のマネジメントの工夫もテスト戦略に含む。

JSTQB,ISTQB,「Advanced Level シラバス日本語版 テストマネージャ」

【解説の詳細度】★★★
【解説目的】定義解説、グッドプラクティス解説
【遂行手段】スケジュール・リソース、テストプロセス、テスト設計、テスト実装
【特徴や留意点など】
テスト戦略は、組織の一般的なテスト方法を記述している。プロダクトおよびプロジェクトのリスクマネジメント、テストレベルへの分割、およびテストに関する上位の活動を含む」と呼称。
テスト戦略は、複数のプロジェクトに横断的に適用できる、普遍的・一般的なテスト活動の工夫と定義している。特定プロジェクトに限定されない、組織横断的な知見であることが判断基準で、テスト戦略が扱う活動に縛りは設けていない。テスト技法の選択、自動化ツールの導入、コンサルの活用など様々なものをテスト戦略として紹介している。
本書はテスト戦略の参考文献として、引用されていることが多い。

「ソフトウェアPRESS vol.5」

【解説の詳細度】★★★
【解説目的】定義解説、グッドプラクティス解説
【遂行手段】スケジュール・リソース、テストプロセス、テスト設計
【特徴や留意点など】
「テスト戦略とは、テストがうまくいくようにテスト設計の方法(アプローチ)や考え方をまとめ上げることです」と記述。
テストレベルの分け方の工夫、リソース配分、テスト設計技法の選択など、リソースの割り当て、テストの段取り、テスト設計が主な内容。そのほか、レビューや設計のコーディネートなど、他の関連活動の戦略もテスト戦略で扱っている。

ソフトウェアテスト実践ワークブック

【解説の詳細度】★★★
【解説目的】定義解説、グッドプラクティス解説
【遂行手段】テスト設計
【特徴や留意点など】
「テストによりプロジェクトにどんな利益がもたらされるか」というテスト目標をインプットとして策定した、「テストの指針となる原則とアイデア」がテスト戦略と記述している。
テスト設計をうまくするための、テストの十分性基準の設定と、テスト設計の段取り・技法選択が主な内容である。

「TPI NEXT ビジネス主導のテストプロセス改善」

【解説の詳細度】★★
【解説目的】グッドプラクティス解説
【遂行手段】スケジュール・リソース、テストプロセス、テスト設計
【特徴や留意点など】
「テスト戦略とは、工数やリソースをテストプロセスへ最適に割り振るための指針である。テスト戦略には、テストにかける工数の分配と、テストする箇所のカバレッジやテスト対象の側面について定義する」と記述。
定義は独自のもの。TPI NEXTはテストの活動をキーエリアという要素に分割している。本書のテスト戦略はそのキーエリアの一つであるが、他書のテスト戦略に含まれるテスト設計や組織、計画などの戦略は、他のキーエリアに分離されている。

「ソフトウェアテスト 293の鉄則」

【解説の詳細度】★★
【解説目的】定義解説、グッドプラクティス解説
【遂行手段】テスト設計
【特徴や留意点など】
「テスト戦略とは、テスト設計の指針をまとめたものを指している」と記述。「テスト戦略」に、プロジェクトのリソースやスケジュールの工夫といったマネジメントの計画は含まれない。
良いテストを作るためのテスト設計のやテスト設計プロセスの戦略を、テスト戦略と呼称している。
本書はテスト戦略の参考書籍として、引用されていることが多い。

「ソフトウェア品質保証の考え方と実際」「ソフトウェア品質保証入門」

【解説の詳細度】★★
【解説目的】事例解説、グッドプラクティス解説
【遂行手段】テストプロセス、テスト設計
【特徴や留意点など】
内容が重複しているので二冊をまとめる。前者が詳細本、後者が入門本と位置づけられているが、テスト戦略に限っては、後者のほうが解説が多い。
良いテストを作るためのテスト設計の戦略を、テスト戦略と呼称している。
テスト設計と、テスト設計を主観点にしたプロセスの戦略が中心。本書ではレビューをテストの一部(机上テストと呼称)としているため、テスト戦略にレビューの戦略も含まれる。

「事例とツールで学ぶHAYST法」

【解説の詳細度】★★
【解説目的】定義解説、グッドプラクティス解説、事例解説
【遂行手段】テストプロセス、テスト設計、テスト実装
【特徴や留意点など】
政略と戦略の2つの用語を区別して使い分けている。戦略は英語のStrategyと対応付ける事が多いと思うが、本書は、政略にPrinciple・Policyを、戦略にPersonnel・Planを対応付けている。
良いテスト作るためのテストプロジェクトの戦略を、テスト戦略と呼称している。
テスト戦略はテスト設計と、テスト設計プロセスの戦略が主だけれど、他部署との連携などマネジメントの戦略も含んでいる。

「ソフトウェアテスト見積りガイドブック」

【解説の詳細度】★★
【解説目的】グッドプラクティス解説
【遂行手段】スケジュール・リソース、テストプロセス、テスト設計、テスト実装
【特徴や留意点など】
「ソフトウェアの重要な部分を特定したうえで、品質をどこまで確保し、そのためにテストにどれだけの資源を投入するか、また投入する資源をどう使うかといった方針」をテスト戦略と呼称。
戦略に対応する英語にはApproachを挙げている。
テストの解説というより見積もりの解説の書籍である。ただ見積もりの分析材料としてテスト戦略を重視しているため、全体に渡って戦略への言及がある。

「自動ソフトウェアテスト」

【解説の詳細度】★★
【解説目的】グッドプラクティス解説
【遂行手段】スケジュール・リソース、テストプロセス、テスト設計
【特徴や留意点など】
テスト戦略を、欠陥予防戦略と、欠陥検出戦略の2つで構成している。両方共プロジェクト全体の活動を含む。欠陥予防戦略は、早期のテスト関与、標準ガイドラインの活用、レビューなどを扱う。欠陥検出戦略はレビュー、テスタビリティの作り込み、テスト自動化、リスクアセスメントなどを扱う。
テスト戦略に、テスタビリティの作り込みと言った設計・実装の工夫が含まれるのが特徴。

「ソフトウェアのテスト技法(計算機科学/ソフトウェア技術講座)」

【解説の詳細度】★★
【解説目的】グッドプラクティス解説
【遂行手段】テストプロセス、テスト設計
【特徴や留意点など】
単体テスト結合テストなどテストの作業段階の設定を含め、どのような順序でプログラムの構成要素をテストしていくかを定める」ことをテストの戦略と記述している。
テストレベル・テストフェーズの策定、結合テストのアプローチ、テストの実施準備など、テスト実施の段取りの話が中心。

ソフトウェアテストを改善する50の実践手法

【解説の詳細度】★★
【解説目的】グッドプラクティス解説
【遂行手段】スケジュール・リソース、テストプロセス、テスト設計、テスト実装
【特徴や留意点など】
「テスト戦略は○○である」という定義の説明はない。ただ「テスト目標を最も有効な方法で達成する助け」「所定の目標を達成するために、テスト・チームが遂行すべき非常に具体的なアクティビティが含まれる」なものがテスト戦略と説明される。
テスト戦略は、テストプロジェクトを成功させるための戦略全般を指す。テストプロジェクトのリスクマネジメントが中心である。

バイザー本 「実践的プログラムテスト入門」 , 「ソフトウェアテスト技法」

【解説の詳細度】★
【解説目的】グッドプラクティス解説
【遂行手段】テスト設計
【特徴や留意点など】
著者が同じで言葉遣いが似ているため2冊をまとめる。
「テストスイートに含めるテストケースを選択/生成するためのシステマティックな方法」をテスト戦略と記述。
良いテストを作るためのテスト設計の技法やアプローチを「テスト戦略」と呼称している。テスト戦略とテスト設計技法を同等のものとして扱っているのが特徴的。
なお「ソフトウェアテスト技法」ではテストのための開発の戦略も解説しているが、テスト戦略とは区別されている。

「ソフトウェアテストと品質保証の実際」

【解説の詳細度】★
【解説目的】事例解説
【遂行手段】テストプロセス、テスト設計
【特徴や留意点など】
色々な会社のテストや品質保証の事例をまとめた書籍。
「戦略」は開発を含めて包括的に解説されていたり、他の用語(アプローチ等)に置き換わっていたりするので、本書では「テスト戦略」という言葉はあまり出てこない。ただテスト戦略に該当する活動の解説は複数ある。
日立の事例でのテスト戦略として、前述の「ソフトウェア品質保証の考え方と実際」とほぼ同じ解説が行われている。
アクセンチュアの事例で、「各テストステージ(JSTQBでいうテストレベル)の範囲、テストプロセス、役割と責任、必要となるMetricsを明確に定義する」のがテスト戦略と解説されている。

「ソフトウェアテストの技法第2版」

【解説の詳細度】★
【解説目的】グッドプラクティス解説
【遂行手段】テスト設計
【特徴や留意点など】
良いテストを作るためのテスト設計の戦略を、「テストの戦略」と呼称している。
テストの戦略として、ホワイトボックステストブラックボックステストを挙げている。内容はテスト設計のみ。バイザー本と同じく、テスト設計技法とテスト戦略を区別していないように見える。ただ、テスト設計技法の書籍なのでたまたまそうなっているだけかもしれない。

「基本から学ぶテストプロセス管理」

【解説の詳細度】★
【解説目的】グッドプラクティス解説
【遂行手段】スケジュール・リソース、テストプロセス、テスト設計、テスト実装
【特徴や留意点など】
「テストの戦略とは、できるだけ多くのバグを突き止めて識別するための全体的な計画」と記述。テスト戦略の範囲は広く、要員確保などリソースマネジメントの戦略も包含。
プロジェクトを成功させるための、テストプロジェクトの戦略をテスト戦略と呼称している。
「テスト戦略」の言葉の解説は少ない。ただ書籍全体がテスト戦略のグッドプラクティスの解説を行っているような内容で、今回のテーマにとっては重要度が高い本だと思う。

「体系的ソフトウェアテスト入門」

【解説の詳細度】★
【解説目的】グッドプラクティス解説
【遂行手段】スケジュール・リソース、テストプロセス、テスト設計、テスト実装
【特徴や留意点など】
プロジェクトを成功させるためのテストプロジェクトの戦略を「テストの戦略」と呼称している。
テスト戦略には、誰がテストするか、トレーニングをどうするかといった、マネジメントの要素を含む。直接的な説明は少ないが、テスト戦略に該当する活動の解説は結構ある。

「基本から学ぶソフトウェアテスト」

【解説の詳細度】★
【解説目的】グッドプラクティス解説
【遂行手段】スケジュール・リソース、テストプロセス、テスト設計、テスト実装
【特徴や留意点など】
まとまった解説はなく、散発的にテストの戦略という言葉を使っている。プロジェクトを成功させるための、テストプロジェクトの戦略全般をテスト戦略と呼称しているようだ。

「ソフトウェアテストHAYST法入門」

【解説の詳細度】★
【解説目的】グッドプラクティス解説、事例解説
【遂行手段】テストプロセス、テスト設計、テスト実装
【特徴や留意点など】
良いテストを作るための、テスト設計と、テスト設計プロセスの工夫を「テストの戦略」と呼称している。ただ戦略がテスト設計の話ばかりなのは、テスト設計手法の解説本なのでたまたまそうなっている可能性もある。
同じ著者の方の別のHAYST法の本で、テスト戦略の解説が行われている。

「現場の仕事がバリバリ進むソフトウェアテスト手法」

【解説の詳細度】★
【解説目的】用語解説、グッドプラクティス解説
【遂行手段】テストプロセス、テスト設計、テスト実装
【特徴や留意点など】
「テスト全体の活動のバランスを考えるのがテスト戦略です」「テスト戦略とは、品質目標達成のために、どうテストを進めれば市場に出てはいけない重大なバグを『できるだけ早く、できるだけ低コストで発見する』ことができるかを決めることです」と記述。
プロジェクトを成功させるためのテストプロジェクトの戦略を、テスト戦略と呼称している。テスト戦略には、ミーティング方針などの開発チームとの連携の戦略や、見積もりの戦略も含まれる。
なおプレゼン資料なので今回含めていないが、著者の方がテスト戦略についての講演資料を公開している(http://www.jasst.jp/archives/jasst10s/pdf/S1.pdf)。こちらは「テスト戦略とは何か」をダイレクトに解説している資料で、今回のテーマを学ぶなら必読だと思う。

「ステップアップのためのソフトウェアテスト実践ガイド」

【解説の詳細度】★
【解説目的】用語解説、グッドプラクティス解説
【遂行手段】テストプロセス、テスト設計
【特徴や留意点など】
大辞泉の戦略の解説を引用し、それに準拠している。その定義にもとづいて、テストプロジェクトの戦略をテスト戦略と呼称。
戦術と戦略の区別を重視している。「ツールによる自動化」「リソース不足解消のための人員追加」「テスト設計技法の選択」といったものはテスト戦術であり、テスト戦略に含めるべきでないと記述している。

「ソフトウェア品質知識体系ガイドV2」

【解説の詳細度】★
【解説目的】グッドプラクティス解説
【遂行手段】テストプロセス、テスト設計
【特徴や留意点など】
解説はISO/IEC29119を紹介するのみ。一応の引用としてISO/IEC 29119はOrganizational Test StrategyとTest Strategyの2つを提示している。後者は「part of the Test Plan that describes the approach to testing for a specific test project or test sub-process or sub-processes」と記述している。ここにはテスト設計だけでなく、ツールや環境の戦略も含まれる。
(このBOKは日本でのテストの話題を結構網羅しているので)意外だったが、テスト戦略の定義や解説はほぼなかった。

実践アジャイルテスト

【解説の詳細度】★
【解説目的】グッドプラクティス解説、事例解説
【遂行手段】テストプロセス、テスト設計
【特徴や留意点など】
「戦略は長期の行動計画」と記述している。複数のプロジェクトで共通なテストのプロセス・戦略をテスト戦略と呼称している。JSTQBと類似している。
テスト戦略には、テストレベル、テストパラダイム、ツール、環境など、テストのエンジニアリングプロセス全般が含まれる。
テスト戦略についての直接的な解説は少ないが、アジャイルテストにおける、テスト戦略に該当する活動についての解説を全体に渡って行っている。

継続的デリバリー

【解説の詳細度】★★
【解説目的】グッドプラクティス解説、事例解説
【遂行手段】テストプロセス、テスト設計
【特徴や留意点など】
テスト解説本ではないが、テスト戦略の具体例の解説が充実しているためおまけとして掲載。
「テスト戦略の設計は、プロジェクトのリスクを識別して優先順尾をつけ、それを緩和するためにどんなアクションを取ればよいかを決定するプロセス」と記述。
良いテストを行うための、テストプロジェクトの戦略をテスト戦略と呼称。

優先度上限プロトコルによる優先度逆転の回避・リソース共有効率化

 並行処理での優先度逆転の回避や効率性改善の実現手段として、優先度上限プロトコルがあります。優先度上限プロトコルは、リソースをロックしたプロセスの優先度を上限値に引き上げる仕組みです。
 今回はTOPPERS/ASP3での実装を例に、簡単な解説を書きたいと思います。

優先度上限プロトコルを使用しない実装

 優先度上限プロトコルは、シングルタスクのRTOSですと動きが明快です。そのためシミュレータが提供されていてPCで簡単に動かせるRTOSTOPPERS/ASP3で動作確認します。

 今回は、基本的に前エントリ「TOPPERS/ASP3をシミュレータで動かす & タスクを実装する - 千里霧中」に追加変更を加えたコードを用います。環境構築やコンフィギュレータの操作はそのエントリを参照ください。
 まずは比較対象として、優先度上限プロトコルを用いない版を説明します。

実装

 前述エントリと同じく、TASK_HOGE、TASK_HIGH_HOGEの2つのタスクを用意します。タスク優先度は、TASK_HIGH_HOGE > TASK_HOGE です。
 そのタスクのコンフィギュレーション設定は、例えば以下のように記述します。

CRE_TSK(TASK_HOGE, { TA_NULL, 10, task_hoge, 5, STACK_SIZE, NULL });
CRE_TSK(TASK_HIGH_HOGE, { TA_NULL, 11, task_highlevel_hoge, 4, STACK_SIZE, NULL });

 次にタスクの実装です。今回は、メインタスクがTASK_HOGEをウェイクアップし、そのTASK_HOGEは途中でTASK_HIGH_HOGEをウェイクアップする動作を実装します。
 まずメインタスクに以下のウェイクアップ処理を追記します。

act_tsk(TASK_HOGE);
act_tsk(TASK_HIGH_HOGE);
for (i = 0; i < task_loop; i++);
wup_tsk(TASK_HOGE);

 TASK_HOGEの実装は以下の通りです。

void task_hoge(intptr_t exinf)
{
  while (true) {
    slp_tsk();
    syslog(LOG_INFO, "hoge");
    wup_tsk(TASK_HIGH_HOGE);    
    syslog(LOG_INFO, "hoge end");
  }
}

 TASK_HIGH_HOGEの実装は以下の通りです。

void task_highlevel_hoge(intptr_t exinf)
{
  while (true) {
    slp_tsk();
    syslog(LOG_INFO, "high hoge");
    syslog(LOG_INFO, "high hoge end");
  }
}

動作

 上記のサンプルを動かすと、以下のログ出力が得られます。

hoge
high hoge
high hoge end
hoge end

 コンフィギュレーションで指定したタスク優先度の通り、TASK_HIGH_HOGEをウェイクアップしたタイミングで、実行タスクがTASK_HOGEからTASK_HIGH_HOGEにスイッチしているのがわかります。

優先度上限プロトコルを使用する実装

 次に優先度上限プロトコルを使用した例です。今回は、TASK_HOGEがTASK_HIGH_HOGEをウェイクアップする際、優先度上限プロトコルを設定したミューテックスMTX_RESOUCEをロックするように実装します。
 まずコンフィギュレーションの記述です。ミューテックスMTX_RESOUCEの設定を追加します。MTX_RESOUCEの優先度上限は全タスク中最高の値を指定します。

CRE_TSK(TASK_HOGE, { TA_NULL, 10, task_hoge, 5, STACK_SIZE, NULL });
CRE_TSK(TASK_HIGH_HOGE, { TA_NULL, 11, task_highlevel_hoge, 4, STACK_SIZE, NULL });
CRE_MTX(MTX_RESOUCE, { TA_CEILING, 1 });

 そしてタスク実装です。最初の実装例のうち、TASK_HOGEを以下のように変更します。MTX_RESOUCEのロック制御処理を、TASK_HIGH_HOGEウェイクアップ前後に追記しています。

void task_hoge(intptr_t exinf)
{
  while (true) {
    slp_tsk();
    syslog(LOG_INFO, "hoge");
    loc_mtx(MTX_RESOUCE);
    wup_tsk(TASK_HIGH_HOGE);    
    syslog(LOG_INFO, "hoge end");
    unl_mtx(MTX_RESOUCE);
  }
}

動作

 上記のサンプルを動かすと、以下のログ出力が得られます。

hoge
hoge end
high hoge
high hoge end

 MTX_RESOUCEのロックを開放するまで、TASK_HIGH_HOGEのタスク実行が行われないことがわかります。ロック中、TASK_HOGEの優先度がTASK_HIGH_HOGEを上回っているということです。

優先度上限プロトコルの用途

 マルチタスクでの優先度設定は、一般的にタスクごとの優先度設定で行います。優先度上限プロトコルは、それに加えて、リソースごとの優先度設定を可能にする仕組みとも解釈できます。
 優先度上限プロトコルの主用途は、優先度逆転の防止です。これによりデッドロックの回避等を行います。
 また、リソース占有時間の最適化にも用いられます。例えばノンプリエンプティブならば、特定のリソース専有中での無用なコンテキストスイッチやタスク中断を抑止します。一般的ではないですが、プリエンプティブで、特定のリソース専有処理を早く処理させるために活用しているところも見たことがあります。
 優先度上限プロトコルPOSIXスレッドのサポートが有名です。特にPOSIX準拠のRTOSでは活用の余地があります。

 なお「リソースごとの優先度設定を可能にする」というと聞こえは良いのですが、マルチプロセスで常用されているわけではありません。以下のような制約を持つためです。

  • タスクの優先度を動的に変えてしまうので、マルチタスクの設計をかえって複雑化させる場合があります。まず優先度上限プロトコルを使用しなくても良いようにタスク設計を工夫したほうが良いことがあります(タスクが使用するリソースを限定的に、リソースが紐づくタスクを限定的に)。
  • ラウンドロビンなどのプリエンプティブなスケジューリング(よくある典型がPOSIXを使う組み込みLinux)では、メリットをどこまで得られるかが不透明です。例えばロック時間の短縮を意図しても、具体的にどこまで時間短縮できるかが見積もりが難しいです。そのためラウンドロビンでの活用は少ないです。デッドラインの遵守などには、タスク単位の割り込み制御、CPUアフィニティの設定、FPGA・他プロセッサへの切り出し等など、別対策に頼らざるを得ない場合が多いです。

 もちろん上記の制約は状況によります。例えば重要なクリティカルセクション1、2個程度に限定して優先度上限プロトコルを適用することで、マルチタスク設計を簡略化できる場合もあります。また優先度逆転を事前のコンフィギュレーションで防止できるのが便利な状況もあります。

POSIXスレッドにて複数スレッドのタイミング制御をまとめて行う(セマフォと条件変数を使う場合)

 POSIXスレッドを使用する場合で、複数のスレッドを一斉にブロック解除させる方法についてまとめます。

 今回は、割と課題になりがちな、スレッドの開始制御について扱います。POSIXでは、この開始時におけるタイミング同期の一斉制御は少し面倒になります。pthread_create実行からスレッド内の処理が開始するまでのタイミングが不確定なためです。
 おおよそ以下のステップが必要になります。

  1. すべての制御対象スレッドを生成する
  2. 各制御対象スレッドは、制御元にスレッド開始を通知し、制御元からの指示を待つ
  3. 制御元は、すべての制御対象スレッドからスレッド開始の通知を受けるまで待つ
  4. 制御元は、各制御対象スレッドに処理命令を通知する
  5. 各制御対象スレッドは、制御元からの処理命令に基づいて処理を実行する

 これらの実現手段として、POSIXではセマフォや条件変数が使われます。その場合、この各ステップの同期制御を実現するために、いずれも2つ分のセマフォ・条件変数を要します。以降にそれぞれの実装例を示します。

 なお、組み込みのリアルタイム処理の視点に限ると、POSIXスレッドのAPIはやや力不足です。リアルタイム処理でより複雑な一斉同期制御を行う場合は、各スレッドで独自のスレッド制御状態を管理させ、そのスレッド制御状態を共有しながら同期制御を行う処理を追加実装するアプローチが妥当なことが多いです。

セマフォの場合

 複数スレッドのまとめての同期制御には、セマフォが有望です。名前付きセマフォを使用した場合のサンプルコードを以下に示します。これは、制御元main()関数が3つのスレッドthread_func()の開始タイミングの制御を行うものです。(なお解説用なのでエラー処理は副作用の大きいもの以外省略しています)
 前述したステップ2、3に、スレッド→制御元のセマフォ制御(サンプルのpsync_prestart)、ステップ4、5に制御元→スレッドのセマフォ制御(サンプルのpsync_start)を用いています。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <semaphore.h>
#include <sys/stat.h>
#include <fcntl.h>

#define NUM_OF_THREADS 3
#define SEMNAME_PRESTART "hoge"
#define SEMNAME_START "fuga"

static sem_t *psync_prestart, *psync_start;

void *thread_func(void *arg)
{
	int id = (int)pthread_self();
	printf("[%x]start\n", id);
	sleep(1);
	sem_post(psync_prestart);
	printf("[%x]wait for psync_start\n", id);
	sleep(1);
	sem_wait(psync_start);
	printf("[%x]start\n", id);
	return NULL;
}

int main(void)
{
	int i, id;
	pthread_t th[NUM_OF_THREADS];
	psync_prestart = sem_open(SEMNAME_PRESTART, O_CREAT | O_EXCL, 0x777, 0);
	if (psync_prestart == SEM_FAILED) {
		sem_unlink(SEMNAME_PRESTART);
		perror("sem_open error\n");
		exit(EXIT_FAILURE);
	}
	psync_start = sem_open(SEMNAME_START, O_CREAT | O_EXCL, 0x777, 0);
	if (psync_start == SEM_FAILED) {
		sem_unlink(SEMNAME_START);
		perror("sem_open error\n");
		exit(EXIT_FAILURE);
	}
	for (i = 0; i < NUM_OF_THREADS; i++) {
		pthread_create(th + 1, NULL, thread_func, NULL);
	}
	for (i = 0; i < NUM_OF_THREADS; i++) {
		sem_wait(psync_prestart);
	}
	printf("all threads are waiting\n");
	for (i = 0; i < NUM_OF_THREADS; i++) {
		sem_post(psync_start);
	}
	printf("all threads are running\n");
	for (i = 0; i < NUM_OF_THREADS; i++) {
		pthread_join(th[i], NULL);
	}
	sem_close(psync_prestart);
	sem_close(psync_start);
	sem_unlink(SEMNAME_PRESTART);
	sem_unlink(SEMNAME_START);
	exit(EXIT_SUCCESS);
}

出力は以下のようになります。

[67c8000]start
[68ce000]start
[684b000]start
[68ce000]wait for psync_start
[67c8000]wait for psync_start
[684b000]wait for psync_start
all threads are waiting
all threads are running
[68ce000]start
[684b000]start
[67c8000]start

条件変数の場合

 条件変数のbroadcastを利用しても、複数スレッドのまとめての制御が可能です。
 セマフォのサンプルコードで、セマフォを条件変数に置き換えたものを以降に示します。ここではステップ4、5のブロック・ブロック解除処理に条件変数のAPI:pthread_cond_broadcast()を利用しています。なお前述したステップ2については、コードが少し面倒なのでセマフォのままとしています(条件変数のみを使用した実装については http://codezine.jp/article/detail/1893 に解説があります)。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <semaphore.h>
#include <sys/stat.h>
#include <fcntl.h>

#define NUM_OF_THREADS 3
#define SEMNAME_PRESTART "hoge"

static sem_t *psync_prestart;
pthread_mutex_t mutex_start = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond_start = PTHREAD_COND_INITIALIZER;

void *thread_func(void *arg)
{
	unsigned int id = (unsigned int)pthread_self();
	printf("[%x]start\n", id);
	sem_post(psync_prestart);
	printf("[%x]wait for cond\n", id);
	pthread_mutex_lock(&mutex_start);
	pthread_cond_wait(&cond_start, &mutex_start);
	pthread_mutex_unlock(&mutex_start);
	printf("[%x]exit\n", id);
	return NULL;
}

int main(void)
{
	int i, id;
	pthread_t th[NUM_OF_THREADS];
	psync_prestart = sem_open(SEMNAME_PRESTART, O_CREAT | O_EXCL, 0x777, 0);
	if (psync_prestart == SEM_FAILED) {
		sem_unlink(SEMNAME_PRESTART);
		perror("sem_open error\n");
		exit(EXIT_FAILURE);
	}
	for (i = 0; i < NUM_OF_THREADS; i++) {
		pthread_create(th + 1, NULL, thread_func, NULL);
	}
	for (i = 0; i < NUM_OF_THREADS; i++) {
		sem_wait(psync_prestart);
	}
	printf("all threads are waiting\n");
	pthread_cond_broadcast(&cond_start);
	printf("all threads are running\n");
	sleep(1);
	for (i = 0; i < NUM_OF_THREADS; i++) {
		pthread_join(th[i], NULL);
	}
	sem_close(psync_prestart);
	sem_unlink(SEMNAME_PRESTART);
	exit(EXIT_SUCCESS);
}

出力はセマフォを利用したときと同様になります。

pythonのdoctestとcoverageでコメントの説明の十分性を簡易確認

pythonでは、ドキュメントとして書かれたコメントと、実際の動作が一致するか確認する手段として、doctestを標準で提供しています。
題材として、大まかな動作の説明をdocstringに書いた、以下のhoge()という簡単なメソッドを扱います。

# coding:utf-8
# sample.py
import re

def hoge(line):
	'''
	指定した文字列から定数名の定義を抽出する
	# (1)プリプロセッサのマクロ定義名を抽出する
	>>> from sample import hoge
	>>> hoge('#define PREPRO_MACRO 33')
	'PREPRO_MACRO'
	
	# (2)const変数の名前を抽出する
	>>> hoge('const int const_variable_name = 33;')
	'const_variable_name'
	'''
	
	result = re.search('[\s\t]*#define[\s\t]+([a-zA-Z_0-9]*)', line)
	if result != None:
		return result.group(1)
	result = re.search('[\s\t]*const[\s\t]+[a-zA-Z_0-9]*[\s\t]+([a-zA-Z0-9_]*)[\s\t]+=', line)
	if result != None:
		return result.group(1)
	result = re.search('[\s\t]*constexpr[\s\t]+[a-zA-Z_0-9]*[\s\t]+([a-zA-Z_0-9]*)[\s\t]+=', line)
	if result != None:
		return result.group(1)
	return ''

これを以下のようなコマンドで、doctest上で実行します。

python -m doctest sample.py

すると、doctestはコメント内容に基づき、hoge()を実行して以下の確認を行います。

  • hoge('#define PREPRO_MACRO 33')」の戻り値が「'PREPRO_MACRO'」であること。
  • hoge('const int const_variable_name = 33;')」の戻り値が「'const_variable_name'」であること。

たとえば適当なバグをコードに埋め込むと、次のようにコメントの仕様と動作の差異を報告してくれます。

File "sample.py", line 14, in sample.hoge
Failed example:
    hoge('const int const_variable_name = 33;')
Expected:
    'const_variable_name'
Got:
    ''

このdoctestは外部向けに仕様を解説するコメントの保守に便利です。特にTest as DocumentationやSpecification by Exampleを推進するテストファースト手法を直接的にサポートしてくれます。

カバレッジでコメント解説の十分性を評価

doctestではコードカバレッジの計測も可能です。doctestでのコードカバレッジを見ることで、コメントの解説の十分性を簡易チェックできるようになります。
(あくまで参考指標です。ここで「doctestでカバレッジ100%得られるまでコメントを書くこと」を強制しだすと当然おかしくなります)

例えばcoverage.pyを使う場合、以下のようなコマンドを実行します。

coverage run -m doctest sample.py

すると以下のconstexprについてのコードが実行されていないことがカバレッジレポートで報告されます。それで、該当部分についてのコメントの説明が足りないことに気づけます。

result = re.search('[\s\t]*constexpr[\s\t]+[a-zA-Z_0-9]*[\s\t]+([a-zA-Z_0-9]*)[\s\t]+=', line)
if result != None:
    return result.group(1)

astah*で作成されたモデルをJava APIで解析・編集する

組み込みのモデリングツールとして一般的なEnterprise Architectのファイルは、zipファイルとして解凍すると、テキストベースのXMLとして、モデルデータの各種解析や編集が可能になるのが一部で知られています。
一方で同じく組み込み一般的なAstah*の方は、zipファイルとして解凍は可能なものの、中身はバイナリファイルでEAのような解析編集が難しくなっています。ただAPIが提供されていて、その活用で柔軟な処理を行えます。今回はそのAPIの使い方について触れたいと思います。

概要

astah* APIJavaAPIです。最新版だと無料のCommunity版でもモデルの解析や編集が可能になっています。
http://astah.change-vision.com/ja/astah-api.html などに情報がまとまっています。

astah* APIライブラリの追加

astah* APIを使用する際は、Community版の場合、astah-communityとastah-apiの2つが必要です。Mac環境のIntelliJだと、ProjectSettingのLibrariesに以下を追加します。

/Applications/astah community/astah community.app/Contents/Java/astah-community.jar
/Applications/astah community/astah community.app/Contents/Java/astah-api.jar

実装

ProjectAccessorを使ってastahのファイルを操作します。細かな仕様は以下などで情報が提供されています。

http://astah.change-vision.com/ja/astah-api.html
http://members.change-vision.com/javadoc/astah-api/7_1_0/api/ja/doc/index.html

実装として、テスト設計の補助を想定して、astah*の状態遷移図のトリガイベントを網羅的にピックアップするコードを書いてみました。

//TARGET_FILE内で作成されている、TARGET_SMDの名前のステートマシン図のトリガイベントをすべて表示
import com.change_vision.jude.api.inf.model.*;
import com.change_vision.jude.api.inf.project.ProjectAccessor;
import com.change_vision.jude.api.inf.AstahAPI;

public class PrintStateTransitions {
    private static final String TARGET_FILE = "/Users/ih/Desktop/sample.asta";
    private static final String TARGET_SMD = "sampleStateMachine";

    public static void main(String[] args) {
        ProjectAccessor prjAccessor = null;
        try {
            prjAccessor = AstahAPI.getAstahAPI().getProjectAccessor();
            prjAccessor.open(TARGET_FILE, true, false, true);
            checkStateTransitions(prjAccessor.getProject());
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (prjAccessor != null) {
                prjAccessor.close();
            }
        }
    }
    private static void checkStateTransitions(IPackage iPackage) {
        INamedElement[] ine = iPackage.getOwnedElements();
        for (int ip = 0; ip < ine.length; ip++) {
            INamedElement[] idg = ine[ip].getDiagrams();
            for (int id = 0; id < idg.length; id++) {
                if (idg[id] instanceof IStateMachineDiagram && idg[id].getName().equals(TARGET_SMD)) {
                    printAllStateTransitions((IStateMachineDiagram)idg[id]);
                }
            }
            if (ine[ip] instanceof IPackage) {
                // 入れ子のパッケージ構造を再帰処理
                System.out.println();
                IPackage iChildPackage = (IPackage)ine[ip];
                checkStateTransitions(iChildPackage);
            }
        }
    }

    private static void printAllStateTransitions(IStateMachineDiagram ismd) {
        ITransition[] it = ismd.getStateMachine().getTransitions();
        System.out.print(ismd.getName() + ":");
        for (int i = 0; i < it.length; i++) {
            System.out.println("  " + it[i].getName());
        }
    }
}

(C++)constexpr & static_assertによるコンパイル時テストの用途

 これは ソフトウェアテスト Advent Calendar 2016 - Qiita の4日目の記事です。

 C++では、C++11から以下の言語仕様が追加されました。
・違反するとコンパイルを失敗させる表明構文:static_assert
・指定対象をコンパイル時に処理させる指定子:constexpr

 このstatic_assertとconstexprを組み合わせると、コンパイル時テストを柔軟に構築できるようになります。
 コンパイル時テストは、コンパイルの際に実行され、テストに失敗したらコンパイルエラーを発生させるものです。これはC++のようなコンパイラ方式言語で、テストの選択肢を広げる助けとなります。今回は組み込み向けを想定して、コンパイル時テストの用途を紹介したいと思います。

コンパイル時テストの用途1:コンパイル時処理のテスト

 constexprで記述されたコンパイル時処理なら、大抵コンパイル時にテストできます。そのテストをコンパイル時テストとして書けば、テストを失敗させるコードをコンパイル不能にして、不具合混入を防止できます。
 単純な例ですが、例えば以下のような2つの値の差の絶対値を計算する、constexpr関数get_diffを考えます。

template <typename T>
constexpr auto get_diff(T x, T y) {
    return (x < y) ? y - x: x - y;
}
//今回のエントリのコードは全体的にC++14依存です。動作確認時は「clang++ -std=c++14 ソースファイル」などで

 ここで以下のように、static_assertとconstexprで記述したテストを用意します。

constexpr void test_get_diff(void) {
    static_assert(3 == get_diff(5, 2),"test get_diff: arg_left > arg_right, int");
    static_assert(3 == get_diff(2, 5),"testget_diff: arg_left < arg_right, int");
    static_assert(0.4 == get_diff(0.6, 0.2),"test get_diff:double");
}

 すると、上記test_get_diffのテストを失敗させるようなコードはコンパイルエラーとなります。
 組み込みでは、constexprの適用範囲が充実するC++14以降を使える環境は少ないです。ただコンパイル時処理の強化はC++の言語やライブラリの時流となっています。そのため今後活用の余地が増えていくと思います。

コンパイル時テストの用途2:テストの効率性の改善

 組み込みでは「処理時間(プロセッサリソースの使用量)」「RAM/ROM使用量」「コードの可読性」がしばしばトレードオフの関係になります。ターゲット環境向けに大量のテストデータを扱うテストを実装する際には、そのトレードオフの折り合いをどうするか、悩まされる事があります(テストが遅すぎて処理タイミングが変わったり、ROM/RAM上限を超えたり、と言った感じです)。

 例えば、解説しやすい例として、sin計算テストの期待値生成の実装を考えます。
 sin計算の実装の一つとして、FPGA等でたまにある、以下のように入出力テーブルを用意する方法があります。

//sin計算テストの期待値を生成
double get_expected_sin_value(const int degree) 
{
    const double sin_table[360] = {
        0.0,
        0.017452392,
        0.034899467,
        0.052335912,
        0.069756415,
        0.087155669,
        //略。359まで続く
    };
    return sin_table[degree % 360];
}

void test_my_sin(void) //sin計算のテスト
{
    ...
    EXPECT_EQ(my_sin(5), get_expected_sin_value(5));
    ...
}

 もう一つの実装としては、アセンブラ言語等で定番の、マクローリン展開を行って実装する方法があります。以下のようなコードです。

const double pi = 3.14159;
double taylor_numer(const int n) {
    auto result {1.0};
    for (auto i = 2.0; i <= n; i++) {result *= i;}
    return result;
}

double taylor_denom(const double x, const int n) {
    auto result {x};
    for (auto i = 0; i < n; i++) {result *= x;}
    return result;
}

//sin計算テストの期待値を生成
double get_expected_sin_value(const int degree) {
    auto result {0.0};
    auto rad {(double)(degree % 360) * pi / 180.0};
    
    for (auto n = 0; n <= 8; n++) {
        auto const taylor_summand {taylor_denom(rad, 2 *n) / taylor_numer(2 * n + 1)};
        result += ((n % 2) ? -taylor_summand : taylor_summand);
        if (taylor_summand < 0.000001) {
            break;
        }
    }
    return result;
}

 上記の2つでは、処理時間(プロセッサリソースの使用量)、RAM/ROM使用量、コードの可読性がトレードオフの関係にあります。

  • テーブル方式ですと、処理時間を短くできます。RAM/ROM使用量が増大します。(sinでなくもっと複雑な計算式を使う場合に問題化しますが)テーブル格納値の導出方法がコードから読み取れず、コードの可読性が悪くなることがあります。
  • マクローリン展開方式ですと、処理時間が長くなります。RAM/ROM使用量は少ないです。sin計算の過程(マクローリン展開で、8項あるいは1項0.000001未満になるまで計算)がコードから読み取れるようになります。

 ここで、これらテストの記述をconstexpr指定すれば、処理時間、RAM/ROM使用量、コードの可読性のトレードオフを壊せる場合があります。
 例えばマクローリン展開方式を以下のようにconstexprで実装します。

constexpr double pi = 3.14159;
constexpr double taylor_numer(const int n) {
    auto result {1.0};
    for (auto i = 2.0; i <= n; i++) {result *= i;}
    return result;
}

constexpr double taylor_denom(const double rad, const int n) {
    auto result {rad};
    for (auto i = 0; i < n; i++) {result *= rad;}
    return result;
}

//sin計算テストの期待値を生成
constexpr double get_expected_sin_value(const int degree) {
    auto result {0.0};
    auto rad {(double)(degree % 360) * pi / 180.0};
    
    for (auto n = 0; n <= 8; n++) {
        auto const taylor_summand {taylor_denom(rad, 2 * n) / taylor_numer(2 * n + 1)};
        result += ((n % 2) ? -taylor_summand : taylor_summand);
        
        if (taylor_summand < 0.000001) {
            break;
        }
    }
    return result;
}

 こうすると、sin計算はコンパイル時で済まされます。実行環境ではマクローリン展開の処理時間がなくなり、テーブルのようなROM/RAM使用量も不要になります。期待値の計算過程もコードで表現できます。処理時間、RAM/ROM使用量、可読性の3方面を改善しています(テーブル方式をconstexprで実装しても同様です。テーブル生成をconstexprで行えば、テーブル値の計算過程の明示も可能になります)。

 このように、コンパイル時テストやコンパイル時処理はテスト効率化の一手段となります。
 なおコンパイル時処理を行う場合、各種パラメータはコンパイル時に確定できなければならないという制約がつきます。ただテストデータであればその制約がクリアされる場合が多いため、活用しやすいです。

コンパイル時テストの用途3:契約による設計での、事前条件の達成の確認

 コンパイル時テストは契約による設計と親和性が高いです。事前条件の契約遵守のチェックをコンパイル時テストで記述すれば、(テスト範囲限定ですが)契約違反コードをコンパイル不能にします。契約による設計のアプローチ通り、以降の実行やテストでは、事前条件が満たされた場合の処理に集中できるようになります。


 例えばオープンアーキテクチャのAUTOSARを例にとります。AUTOSARは契約による設計を全面的に取り入れています。
 その一つの例ですが、AUTOSARではリンク時、プレビルド時に確定する静的コンフィグコードと本体コードを分けています。そこでは本体コードにとって、静的コンフィグコードは正しいことが事前条件となっています(コンフィグコードが間違っていると、不正メモリアクセスやNULL参照などが発生します)。
 その事前条件では、結構複雑なコンフィグの一貫性が求められます。例えば以下のようなコードで「BlockDescriptorList::NvBlockNumの値は、Ea_BlockConfigData::EaBlockNumで定義されているものを使用しなければならない」といったようなルールがたくさん出てきます。

const NvM_BlockDescriptorType BlockDescriptorList[] = {
    {
        ...
        0x16,//NvBlockNum  
        ...
    },
    {
        ...
        0x20,//NvBlockNum
        ...
    },
};
const Ea_BlockConfigType Ea_BlockConfigData[] = {
    {
        ...
        0x01, //EaBlockNum
        ...
    },
    ...
};

 一応、コンフィグの正しさについては、DETエラーチェックによって、一般的な表明構文と同じように事前条件チェックを行う仕組みを規定しています。ただDETエラーチェックはかなり簡易的である上、以下のような制約を持ちます。

  • 製品版とDETエラーチェック版でビルドが別れる。
  • 各関数呼び出しタイミングで、入力値が事前条件を満たしているかしか見ない。実行時に事前条件違反を発生させるような条件を実現しないと、不具合を検出できない。


 そこで、チェックを以下のようにコンパイル時テストとして記述しておくと、複雑で包括的な静的コンフィグコードのチェックを行えます。静的コンフィグコードに契約違反があればそもそもコンパイル不能になるため、あとは契約による設計の意図通り、本体コードではコンフィグコードが正しい時の処理にフォーカスできます。

constexpr bool check_memory_block_num_config()
{
    auto iea {0};
    for (auto invm = 0; invm < num_nvm_block; invm++) {
        for (; iea < num_ea_block; iea++) {
            if (Ea_BlockConfigData[iea].EaBlockNum == BlockDescriptorList[invm].NvBlockNum) {
                break;
            }
        }
        if (iea == num_ea_block) {
            return false;
        }
    }
    return true;
}

constexpr void test_memory_config()
{
    ...
    static_assert(check_memory_block_num_config(), "test_memory_config: memory block number");
    ...
}

ツリーで分析するときはクラスベースでも考えよう

 以下でツリーモデルで物事を分析する難しさが少し触れられていたので、ツリーモデルの記法上の制約について書きたい(一応の前置きとして、今回書くのは主に記法上の制約のみで、ツリーによる分析の進め方にはあまり言及しない)。

http://togetter.com/li/1047939

 クラシフィケーションツリー法等を使っていて、ツリーモデルで分析を行う際は、クラスベースモデルでも考えたほうが良い場合が多い。お互いメリット・デメリット両方あり、うまく補完しあえるためだ。
 具体的には、複雑なis-aの関係性を持つもの、has-a/is-aの関係性が混在するものは、ツリーでは表現しにくいが、クラス図ではすっきり表現できることが多い。例えば具体例としては以下。

 他にも、前述の例と似てるけれど、概念モデルでも設計モデルでもよく出てくるBridgeパターンも、ツリーによる表現のしにくさが問題化する。
 これらの問題は概ね、「抽象的なhas-a/is-aの関係を、抽象的に表現できない記法上の制約」に起因して、「要素や関係性の重複が発生」した形になる。

 ツリーベースのモデリングにはこういう制約があるので、モデリングのやりにくさを感じたら、クラスベースで考えるのは有効だと思う。より本質的な要素や関係性を適切に表現できて、分析がやりやすくなることがある。

 なおツリーを使ってうまくモデリングできないという問題は、ソフトウェアテストのテスト条件分析でよく見る。失礼な表現になってしまうかもしれないが、これはモデリング力不足でクラスベースのモデルをかけないことに起因している場合が多いように思う。

対象のドメインを意識してモデリングしよう

 またモデルの記法上の制約以外の原因として、プラクティスや慣習の蓄積も少しあると感じる。

 例えば、分析モデリングがごちゃごちゃになってしまう原因の一つに、複数のドメインをごちゃごちゃにまとめて扱ってしまう問題がある。
 クラスベースモデルでは、この原因を避ける慣習が一般化している。クラスベースモデルの実質的標準となっているUMLで、ドメインモデルと設計モデルの区別、システムモデルとサブシステムモデルの区別を明確にしてモデリングする方法論やプロセス、プラクティスが普及しているためだ。
 対してツリーベースモデルではUMLとくらべてそのような慣習が弱くて、適切なモデリング・不適切なモデリングの基準が曖昧なまま、各人自由にやることが多くなっていると思う。

 そのため、ツリーベースでモデリングする際も、「ドメインごとに分けてモデリングする」「ドメインモデルと設計モデルを分けてモデリングする」といった一般的なクラスベースモデリングの心がけを適用すると、やりやすくなる場合があると感じる。