本書ではJAVAを使用したシステム開発におけるクラス設計について説明します。
通常のクラス設計では開発、維持メンテナンスに不都合が発生することが多いため、それらの問題点などを考慮した設計を行ううえでの考え方を説明します。
一般的な設計手法と相容れない記載がありますが、あくまでも書き手独自の設計手法ですので参考程度に見ていただければと思います。
開発効率・品質、維持コスト・品質を考慮した設計とは
クラス設計の説明を行う前に、低コスト・高品質を実現するのは、どのようなプログラム(システム設計)なのかということについて説明します。
※以降の説明は、ここで説明する低コスト・高品質の考え方がベースとなっています。
低コスト・高品質を確保する方法は特に難しいことは無く、以下の2点ということが出来ます。
- 少ないコード
- 分かりやすいコード
少ないコード
1行しかないコードでは、あまり、バグが発生しません。もし、あったとしてもすぐに発見できます。
実装量が多くなれば多くなるほど、高コスト、低品質になります。
品質を議論するとき、テスト手法などで品質を確保することを言う人がいますが、非効率な実装を大量に行って、テストに莫大なコストをかけるより、コーディング量を極力減らして、テストをする必要が無いぐらいのコード量にすることのほうがはるかに品質が上がります。
開発時のテスト工数は、維持メンテ時の仕様変更の工数とほぼ同じですので、莫大なテストを行ったものは、維持メンテ時にもの莫大な費用がかかります。(もしくはテストをしないで品質を落とすか)
また、影響範囲などの分析も、コーディング量が少なければ、低コスト、高品質で行うことが出来ます。
クラス設計を行うとき、各クラスごとのコーディング量を如何に減らすかを考えることが重要ということになります。
分かりやすいコード
「コードが少ない=コードが分かりやすい」ということはありますが、コードが多くても分かりやすいものであれば、低コスト高品質が実現できます。
分かりやすいコードとはどういうものでしょうか?
人間が分かりやすいと感じるのは以下のような場合です。
- 少なく、明確
- まったく同じ、ほとんど同じ、パターンが同じ
- 説明がある
コード量が多くなってしまった場合でも、同じルール、同じ構造、同じ書き方など、パターンや構造が統一されたコードであることで、分かりやすいプログラムになります。
もちろん、コーディング規約などで実装者を縛ることも大切ですが、それよりも、「同じ書き方でしか実装できない」としたほうが確実です。
また、クラス構造や継承関係、メソッド引数などを統一することで、実装者、維持担当者が理解しやすいものになります。
クラス設計を行ううえで、如何に分かりやすいコード、構造を作るかが大切となります。
クラス設計とエンティティ設計
ここからが、クラス設計の説明となります。
まず、ここで説明するクラス設計とは、システム構築プロジェクト上で、どのような位置づけ(フェーズ)で行われるかを説明します。
クラス設計と似たものとしてエンティティ設計があります。
(一般的な定義とあまりずれていないと思いますが、)それぞれを以下のように定義します。
エンティティ設計
要求定義、要件定義からデータ構成要素を洗い出し、分類、集約等を行いエンティティとしてまとめます。
(要するにDBテーブル設計です)
クラス設計
エンティティ設計などの基本設計が終わり、詳細設計に入る段階で行います。
エンティティの構成、機能一覧などから実際のオブジェクト(クラス)をどのような構成で作成するかを設計します。
エンティティ設計はクラス設計とはまったく異なるノウハウが必要となります。
エンティティ設計がトップダウン型(要求内容→データ構成)の設計であるのに対し、クラス設計はボトムアップ型(データ構成・機能構成→実現するためのクラス構成)の設計になると思います。
今回のお題はクラス設計ですので、基本設計が完了していることを前提に、以降の説明を行います。
クラス設計の前提知識:クラスを構成する要素
まずは、クラス図を構成する要素と表現方法を説明します。
クラス設計を表現するときはUMLのクラス図の記号を使用するのが一般的です。
以下に、クラス図を構成する要素とその記号を示します。
UMLのクラス図記号はいくつかありますが、今回の説明で必要となるもののみ、以下に説明します。
クラス
以下の図でクラスを表現します。
最上部がクラス名
中断が保持データ項目(プロパティ)
下段が機能名となります。
本来は”+”(public)、”-“(private)などの細かい表現を行いますが、今回は省略します。
また、インターフェースはステレオタイプ”<>”で表現します。
汎化
JAVAで言う”extends”(継承)関係を表現します。
“汎化”という言葉の意味と混同しないように注意してください。
実現
JAVAで言う”implements”(実装)関係を表現します。
例題基本設計
以下の例題をもとにクラス設計を行ってみたいと思います。
クラス構成を考えずにそのまま作成した場合
基本設計のままクラス設計に落とし込むと以下のようになります。
(エンティティ設計上、一覧表示などの各ビュー機能などは各エンティティ機能の一部となります)
実際、このままシステム化することは可能です。
この場合、以下の冗長化による問題が発生します。
データ、プロパティ(管理項目)の冗長化
同じデータが複数のエンティティで管理されるため、データの整合性を確保するためのロジックが必要となります。
不整合やデータ上の問題が発生した場合のメンテナンスに手間がかかるため2次災害などのリスクが高くなります。
実装内容の冗長化
別クラスで同一のメソッド、同一のコードが複数存在することによる、メンテナンス性の劣化が発生します。
開発コスト、維持コストの増大と品質の低下につながります。
クラス設計に必要な観点
クラス設計は以下の観点を持って検討します。
汎化
機能やデータを共通化、または共通グループ(AbstractやInterfase)とすることで冗長化を防ぎます。
共通化によるクラス同士の密接度を疎となるようにします。
実現
汎化で共通化、共通グループ化したものを利用して各クラスの役割や機能を再構築します。
部品化
横断的に必要な機能(非機能要件など)を実現するための機能を抽出して
クラスツリーとは逸脱したモジュール(Utilなど)を作成します。
クラス設計の悪い例
上記観点に基づきクラス設計を考えるとき、汎化を中心に全てをまとめてしまうことがあります。
ずいぶん、すっきししました。
かっこいい設計に見えます。
では、実際にこの設計で実装した場合を考えて見ましょう。
入出庫予定を登録する機能を実装することを考えます。
入出庫エンティティはSTOCKエンティティを継承し、STOCKエンティティの登録メソッドを利用して登録処理を実装します。
DBテーブルはエンティティと対(つい)になっていますので、STOCKエンティティの登録メソッドは入出庫予約、入出庫履歴、在庫のテーブルを更新するロジックが必要となります。
また、登録されるデータも各データにより仕様が異なるため、整合性チェックなども全てSTOCKクラスの中で実装することになります。
にもかかわらず、マスタの整合性チェックについては派生クラスを参照することになります。
しかも、派生クラスの全てが必要としないため、AbstractMethodとすることも出来ません。
派生先でそれぞれの仕様にあわせてオーバーライドすることも可能ですが、「STOCKの変更が全てに反映されない」、「実装箇所の特定に時間がかかる」などのデメリットが発生し、このようなクラス構造にする意味がなくなってしまいます。
いずれの場合も、非常に中途半端な構造となります。
こうなってしまうと、実装はもとより、維持メンテナンスを行ううえでも、工数、品質、リスクが増大してしまいます。
汎化することで概念はすっきりしましたが、それにどれだけ意味があるのか疑問になってしまいます。
大切なことは階層と役割
なぜ、このようなクラス構造となってしまうのでしょうか?
上記の例の場合、Entityインターフェース、AbstractEntityのを継承してSTOCKクラスを作成する構造となっています。
クラス構成全体で見たとき、STOCKクラスはITEM、倉庫、ユーザと同じ層に並んでいます。
これは、同じ項目の有無で構造を作ってしまったためです。
汎化を考えるとき、階層を意識し、各階層ごとの役割を明確化しながら構造を考える必要があります。
例題の場合、以下のような層に分け、各役割を明確化します。
Entityインターフェース | エンティティ共通インターフェース |
AbstractEntity | エンティティ共通親クラス |
STOCKエンティティ | 機能共通親クラス |
入出庫予約エンティティ | エンティティクラス(DBテーブルアクセスクラス) |
上記役割を考慮して、もう一度クラス構成を考えて見ます。
Entityインターフェース、AbstractEntityクラスは特に変化していませんが、エンティティ共通という役割を担うことになっていますので、Entityインターフェースはエンティティ全体で利用できる機能を作りたい場合に利用することになります。
AbstractEntityクラスは、項目の入力チェックの共通化や整合性チェックメソッドの定型化などを行います。
STOCKクラスは機能共通親クラスに位置づけられますので、入出庫、在庫関連のエンティティが共通して実装すべき内容を実装します。
今回は各プロパティとマスタ性合成確認を持たせ、プロパティ値のマスタ確認を共通実装します。
マスタ確認は各エンティティが必要な場合のみ呼び出す形になるため、不要であっても邪魔になりません。
不要なメソッドが実装されることを嫌う人もいますが、画一的に実装されているほうが仕様変更による影響が少なくなることが多いです。
入出庫予約エンティティはDBテーブルとの連携で使用することになります。
役割から考えると、各プロパティはこのエンティティで保持するべきですが、マスタ性合成確認機能を実装する上で必要なため、STOCKに持たせています。
場合によっては入出庫予定エンティティにも同じ項目を持たせるということでも良いかと思います。
効率を考えて、あえて冗長化させることも必要です。
冗長化させる場合は、それに伴うデメリットに対する対策も同時に行ってください。
(整合性保持の仕組みなど)
クラス構成の層と役割を明確にするだけで、どこに何を配置すべきかを考えることが出来ます。
場合によって違和感(プロパティの冗長化など)があるところが発生することはありますが、逆に、違和感を感じるということはクラス設計全体が統一した思想のものに作られている証拠です。
統一した思想で設計されたシステムであることが、システムの構造をわかりやすくし、結果的に工数削減、品質向上、保守性の向上につながります。
共通機能の部品化
階層と役割を意識して、クラス設計を見直すと以下のようになります。
(細かいところや構造は考え方によって違ってきます。正解はひとつではありません。以下はその一例です)
そうすると、同じ階層に機能が冗長化していることがわかります。
出来るだけ、冗長化は避けたい。でも、仕様がそれぞれ違うので1つのクラスに集約することは出来ません。
そこで、以下のような手段をとります。
ひとつの方法としてはUTILクラスなどで共通機能のみ外出しにすることができます。(太古からの手法)
もうひとつは共通インターフェースをつくり、それを使った共通機能を実装し、インターフェースを実装したクラスで共通機能を利用できるようにします。
こうすることで、各クラスの実装をUTILクラスに集約することも可能です。
これはAbstractクラスでも同じようなことが行えます。
たとえば、機能共通Abstractクラスに機能を持たせたり、Abstractクラスを引数としたUTILクラスを実装することでも実現できます。
どちらの方向性で作るかは、利用するクラスが限定できるか否か、実相寺の手間の多い/少ない、などを考慮して決定します。
特にAbstractクラスで実現する場合は、前の工程で行った階層構造や役割などを崩さないようにする必要があります。
もし、階層構造のために共通化に支障がある場合は、もう一度、階層構造を見直すことも必要です。
また、実際の設計作業は、ロジックレベルで実現可能かどうかを、技術検証や実装検証を行い、確認しながら行う必要があります。
開発基盤(フレームワーク)の役割を意識した構造
これまで、システム内部での構造の統一性や共通機能を見てきましたが、複数システムを構築、維持メンテナンスする場合でも、同じことが言えます。
システムごとに統一されていない構造や共通化されていない機能が存在することは、開発、維持メンテナンス時のコスト・品質に大きく影響します。
システムごとのバラつきを防ぐ方法も、システム内部の構造の統一や共通化と同じ方法で行うことが出来ます。
先ほどの例を元に、全システムで統一化、共通化できるところは以下の図で示したところになります。
これらの統一化・共通化の仕組みをフレームワークとして提供することで、各システムの構造を統一することが出来、その結果、開発、維持メンテナンスの低コスト・高品質が実現できます。
まとめ
今回、クラスの構造を考えるときの、階層化、役割の明確化の手法について説明し、最終的にフレームワークへの切り出しまでの工程を説明しました。
システム構築は、はじめにフレームワークが存在し、それをベースに作成すると考えがちですが、実際はシステムの分析を無くして、フレームワークは存在し得ないということが、今回の説明でお分かりいただけたと思います。
クラス構造の階層、役割が明確でなければ、フレームワークとして、どこを切り出せば良いのかの判断が出来ません。
構築するシステムの特性や要求内容の分析を十分に行うことで、初めて、フレームワークの機能として切り出すことが出来るのです。
企業や業務ごとなどで、独自フレームワークに必要になることも、流用が難しいのも、このような背景があるためです。
フレームワークは、企業における業務ノウハウを集約した大切な資産ということが出来ます。