シリーズ: 要件定義とはそもそも何か
- 要件定義の目的とゴールとは
- 要件定義の重要ポイント〜要望・要求・要件を見極める
- 事業・業務・システムの3階層で要件を捉える
- 業務フロー図で見える化する業務プロセスからシステム要件への道筋
- ユースケースとロバストネス図によるシステム要件定義
- システム要件定義の成果物〜設計へのインプットを作成する
- 要件定義とソフトウェアアーキテクチャ設計
- 要件定義とクラス設計(本記事)
TRACERYプロダクトマネージャーの haru です。
ソフトウェア開発では、異なる役割を持つ処理を組み合わせてアプリケーションを構築します。
たとえば「金額の計算」「データの保存」「画面への表示」「入力チェック」「ログの記録」「セキュリティチェック」などです。
これらの処理を行うプログラムがすべて一つのソースコードに詰め込まれていたら、どうなるでしょうか。ある機能を修正しただけで、まったく関係のない機能に不具合が発生したり、テストに想定外の工数がかかったりする──そのような不安定な状態に陥ってしまいます。
このような状態を解消するための設計原則が、「関心の分離」と「高凝集」です。
どちらも、複雑なシステムを理解しやすく、変更しやすいものにするための重要な指針です。
本記事ではまず、なぜこれらの概念が重要なのか、解説します。
そのうえで、「関心の分離」と「高凝集」を実現する具体的な設計手法として「オブジェクト指向設計」を取り上げ、とくにその中核をなす「クラス設計」の考え方を紹介します。
クラス設計をクラス図として表現することで、ソフトウェア全体の構造と責務の関係性を俯瞰しやすくなり、設計上の抜けや重複を早期に発見しやすくなります。
さらに、クラス設計が要件定義の成果物とどのように連携し、設計に落とし込まれていくのか、という視点についても具体的に解説します。
関心事の分離と高凝集
ソフトウェアにおいて、「関心事」とは、ソフトウェアを設計・開発・運用する際に、それぞれの関係者が注目し、配慮すべき対象や視点のことです。
たとえば、「保存」「表示」「入力チェック」は、それぞれ独立した関心事です。本来は別々に扱うべきものが同じコード上に混在していると、コードの見通しが悪くなり、変更もテストも困難になります。
この問題を解決するための設計原則が「関心事の分離」です。関心事ごとにコードを分けておけば、ある処理を変更しても他の処理には影響が及びにくくなります。
「高凝集」は、密接に関連する処理やデータを、ひとつのまとまりとして構成する設計方針です。
たとえば、ユーザー情報の管理に関する処理(登録、更新、バリデーションなど)を分散させず、同じ場所に集約することで、「責務」が明確になり、コード全体の一貫性が保たれます。
このように、関心事の分離と高凝集を両立させることで、コードの整合性が保ちやすくなり、理解・修正・再利用のしやすさが飛躍的に高まります。
ソフトウェアの関心事には何があるか
ソフトウェア*1の関心事は以下のように、大きくわけて、機能的な関心事と非機能的な関心事に分かれます。
機能的な関心事
ソフトウェアの本来の目的や振る舞いに関することがらです。主に「機能要件」に対応します。
関心事 | 説明 |
---|---|
振る舞い | 処理ロジック、処理フロー、アルゴリズムなど |
データ | 入出力データ、永続化データ、スキーマ、整合性ルールなど |
状態 | アプリケーションやコンポーネントの一時的な状態。UIの現在位置、ステータス値など |
ユーザーとのやりとり | 入力フォーム、画面表示、ユーザーフィードバック、操作フローなど、利用者とのインターフェースに関わる要素 |
エラー・例外処理 | 想定外入力の処理、リカバリ処理など |
非機能的な関心事
機能そのものではなく、「どのように機能するか」に関することがらです。主に「非機能要件」に対応します。
実行時・関心事
どう動作させるか、どれくらい良く動くかという非機能要件や運用時の制約に関わる関心事です。ユーザー体験やシステムの信頼性に影響します。
関心事 | 説明 |
---|---|
タイミング | イベント駆動、スケジューリング、非同期処理、リアルタイム性など |
パフォーマンス | 応答速度、処理量、並列処理、キャッシュなど |
可用性・スケーラビリティ | 障害時の回復、負荷対応、冗長化など |
横断的関心事
ソフトウェア全体の機能や構造にまたがって現れる共通的・非局所的な関心事です。
関心事 | 説明 |
---|---|
ロギング | 各所で必要になるがロジックと混ざりがち。AOPやミドルウェアで分離可能。 |
セキュリティ | 認証・認可、暗号化、データ保護など。複数レイヤにまたがる。 |
トランザクション | DBや他サービスとの一貫性確保のための仕組み。 |
設定・構成 | 実行環境による振る舞いの切替(configファイル、環境変数など) |
非機能的な関心事の多くは、フレームワークやライブラリ、ミドルウェアなどの仕組みを活用することで効果的に対応できます。
一方で、ビジネスロジックをはじめとする機能的な関心事は、アプリケーション開発者が自らの手でコードとして具体的に実装していく必要があります。
関心事の分離と高凝集を実現するオブジェクト指向設計
「関心事を分け、責務ごとにまとめる」という考え方を、特に機能的な関心事の整理において効果的に実現できる手法が「オブジェクト指向設計」です。
オブジェクト指向設計では「クラス」や「属性」「メソッド」という構成要素を使って、処理やデータ、状態などの関心事を整理・分離します。
たとえば、顧客に関する情報を扱う場合、データ(名前、住所など)とそれに関する処理(登録、更新など)を「顧客」クラスとしてひとつにまとめます。
「顧客に関するロジックはこのクラスに書く」といった構造が明確になり、保守性や再利用性が格段に高まります。
このように、関心事ごとにコードを構造化できる点が、オブジェクト指向設計の大きな強みです。
下表では、ソフトウェアにおける主要な機能的な関心事と、それに対応するオブジェクト指向設計の構造を対応づけて示しています。
機能的な関心事 | 説明 | オブジェクト指向設計との対応 |
---|---|---|
振る舞い | 処理ロジック、処理フロー、アルゴリズムなど | メソッドとして定義し、責務を持った処理単位として表現 |
データ | 入出力データ、永続化データ、スキーマ、整合性ルールなど | 属性としてクラスに内包し、データ構造と整合性のルールを保持 |
状態 | アプリケーションやコンポーネントの一時的な状態。UIの現在位置、ステータス値など | オブジェクトの内部状態として管理し、状態遷移も含めて設計対象とする |
ユーザーとのやりとり | 入力フォーム、画面表示、ユーザーフィードバック、操作フローなど、利用者とのインターフェースに関わる要素 | UI部品をオブジェクト化し、ユーザー操作に応じてメソッドを呼び出す。UIと業務ロジックを分離するためにMVC等のパターンを活用 |
エラー・例外処理 | 想定外入力の処理、リカバリ処理など | 例外クラスによりエラーの種類を明示し、ポリモーフィズムで柔軟に対応。共通のインターフェースを通じて例外処理を統一的に扱う |
このように、オブジェクト指向設計は「関心事の整理」に非常に有効な設計手法です。
「どこに何を書くか迷わない」「変更の影響範囲が小さい」「再利用しやすい」といった保守性の高いソフトウェアの背後には、ほぼ例外なく、関心事を適切に分離した設計が存在します。
オブジェクト指向設計は、こうした設計を実現するための重要な指針となります。
要件定義の成果物とオブジェクト指向設計の連携
概念モデルからクラスと属性を抽出する
クラスを設計する際のベースとなるのは、要件定義の成果物である「概念モデル」です(下図)*2。
概念モデルは、クラスやその関連を用いて構造化されているため、その構造をそのままクラス図に展開できます。
概念モデルに、既に属性が定義されている場合は、クラスの属性として抽出します。
このとき、クラス同士の関連*3も含めて、どのクラスがどのように他のクラスと関係しているかを明確に定義します。
クラス図は、ソースコードと構造的に一致するため、クラス名や属性名は英語で記述するのが適切です。実際のコードは英語で書かれることが多く、図とコードの表記が一致していると実装しやすくなります。
また、クラス図とコードは相互に変換可能な関係にあるため、コードを先に書いてツールで図を生成する方法も一般的です。どちらを先に作るかは、設計の進め方やチームの慣習に応じて選んでください。
機能要件からクラスと属性を抽出する
要件定義の成果物である「機能要件」に含まれる名詞を分析することで、クラスや属性を抽出できます。
たとえば下図の例では、顧客クラスに対応する属性として、氏名、メールアドレス、パスワード、配送先住所などが挙げられます。
配送先住所は、1人の顧客が複数登録する可能性があるため、顧客クラスとは別に「配送先住所クラス(ShippingAddress)」として切り出すのが適切です。
ロバストネス図からメソッドを抽出する
要件定義の成果物である「ロバストネス図*4」のコントロールオブジェクト名をもとにクラスのメソッドを抽出できます。
DailySalesSummaryクラスのメソッドとして以下のように定義します(下図の赤字)。
コントロールオブジェクト | クラス | メソッド |
---|---|---|
売上データ集計 | DailySalesSummary | aggregate |
売上データ抽出 | DailySalesSummary | get |
売上レポートPDF出力 | DailySalesSummary | generate_sales_report |
状態遷移図から属性とメソッドを抽出する
オブジェクトの状態遷移からも、クラスの属性とメソッドを抽出できます。
例えば「注文」の場合、「申込済」「入金済」「キャンセル」「発送済」の状態が考えられます。
このように、状態遷移があるようなソフトウェアを開発する際は、要件定義の段階で「状態遷移図」を作成しておくとよいでしょう。
状態遷移図とは、オブジェクトが取り得る状態と、それらの間を移動する条件やイベントを整理した図です。
下図は注文の状態遷移を示した状態遷移図です。
以下のように、状態遷移図で整理した内容をもとに、状態を属性として保持し、状態を変更するためのイベントをメソッドとしてクラスに定義します。
状態遷移図の要素 | クラス | 属性 / メソッド |
---|---|---|
注文状態 | Order | status属性 |
入金を確認する | Order | confirm_paymentメソッド |
配送する | Order | ship_orderメソッド |
注文をキャンセルする | Order | cancel_orderメソッド |
オブジェクトの状態は、そのインスタンスが持つ属性を、専用のメソッドで更新することで管理します。これにより、状態の変更を制御しやすくなり、不正な遷移や処理の漏れを防ぐことができます。
たとえば「注文クラス(Order)」の場合、注文の状態は「status」属性で表現され、これを変更するためのメソッドとして、以下のような設計が考えられます。
- confirm_payment() メソッド:支払いの確認を行い、状態を 「申込済」から 「入金済」へ遷移させる
- ship_order() メソッド:商品の発送処理を行い、状態を 「入金済」から「発送済」へ遷移させる
このように、状態の遷移を明示的なメソッドに集約することで、業務ルールとコードの整合性が保たれ、設計の意図が明確になります。
クラス設計のさらなる洗練
ここまで設計してきたクラス図を下図に示します。
設計は一度して終わりではありません。
ここまでの説明では、メソッドや属性をある程度機械的にマッピングしましたが、それは設計の出発点にすぎません。設計を深めていく中で、業務の本質をより的確にとらえた構造を見出し、モデルを洗練させていくことが重要です。
そのためのヒントとして、ドメイン駆動設計(DDD:Domain Driven Design)の考え方が有効です。DDDでは、業務の意味をコードで正確に表現することに価値を置きます。
たとえば、金額をただの「int」で扱うのではなく、通貨単位や計算処理、範囲チェックなどを内包した 「Money」 クラスとして定義したりします。
このような「値オブジェクト(Value Object)」を使うことで、コードは業務の意図を自然に表現できるようになります。
型そのものがビジネスルールを語るようになり、バリデーションやロジックの重複も防げます。
同様に、注文のステータスを単なる 「Enum(列挙型)」 で済ませるのではなく、「OrderStatus」という専用クラスにして、状態遷移のルールや表示文言の管理を一元化する設計も考えられます。
このように、プリミティブ型のままでは埋もれてしまう業務知識を「型」として定義することで、業務の用語やビジネスルールがモデルの構造に明示的に表れるようになります。
その結果、クラス図は単なる技術的な構造図ではなく、業務のしくみや判断基準が読み取れる「業務理解の地図」として機能するようになります。
画面やDBの処理はドメインモデルから切り離す
ここまで説明してきたクラス群は、事業や業務の構造やルール、振る舞い(たとえば業務処理における判断やアクション)をソフトウェア上で表現したものであり、これをドメインモデルと呼びます。
ドメインモデルは、業務の意味を反映したクラス(エンティティや値)やその振る舞いを明示的に設計することで、ソフトウェアが業務と整合したかたちで動作する基盤をつくります。
一方で、画面表示やデータベースへの保存といった入出力処理に関わる要素は、ドメインモデルとは異なる関心を持つため、ドメインモデルから切り離して設計することが重要です。
たとえば、画面表示用の構造や、DBとの直接やり取りを担う処理をドメインのクラスに含めてしまうと、業務ロジックと技術的な詳細が混在し、コードの見通しが悪くなります。
責務を明確に分離し、ドメインモデルを業務に特化した純粋なモデルとして保つことで、構造が整理され、変更への追従や再利用も容易になります。
レイヤーの分離方針については、DDDのアーキテクチャなどを参考にしながら、どこまでレイヤーを分割するか、また各レイヤーにどのような責務を持たせるかを、プロジェクトの規模や複雑性、将来的な拡張要件などに応じて適切に判断するとよいでしょう。
レイヤーについては、前回の記事( 要件定義とソフトウェアアーキテクチャ設計 - TRACERY Lab.(トレラボ))の「コンポーネント構成の設計」を参照してください。
まとめ
下図に、要件定義の成果物とクラス設計の関係をまとめました。
ソフトウェア設計の根本にあるのは、「関心事を見極め、密接に関連する処理やデータを、ひとつのまとまりとして構成する」というシンプルな考え方です。
関心事の分離と高凝集は、設計の基盤となる原則であり、複雑さを制御し、理解しやすく壊れにくい構造を実現します。
この設計の思想は、要件定義の段階から始まっています。
要件を洗い出す際に、設計に必要な情報を見据え、関心事を意識して構造化しておくことで、後工程の設計作業をスムーズに進めることができます。
次回は、データ設計について解説します。
*1:ソフトウェアには、OSなどの基本ソフトウェア,ミドルウェア、アプリケーションソフトウェアなどの種類があるが、ここではアプリケーションソフトウェアのことを指す
*2: システム要件定義の成果物〜設計へのインプットを作成する - TRACERY Lab.(トレラボ)の「概念モデル」を参照のこと
*3:1対1、1対多、多対多など
*4: ユースケースとロバストネス図によるシステム要件定義 - TRACERY Lab.(トレラボ)の「ロバストネス図によるシステムを構成する要素の抽出」を参照のこと