システムは必ず変化しつづけます。それ由に常に変更が必要となります。従って、変化に対して柔軟な対応ができ、変更に対するコストが低いものが良いソフトウェアと言えます。
変化ヲオソレルナ ← XP(アジャイルプロセス)の基本をなしています。
その為には、ソフトウェアは状況に応じて柔軟に変化し続けることができ、それに伴なう変更を最小限におさえる事ができる必要があります。
モジュールはその開発中及び稼動後を通して変更が発生する事を前題として考える必要があり、変更に対して最小限の修正ですむように設計されなければなりません。
良いソフトウェアを作る為の設計指針としてロバート・C・マーチン氏が10の原則としてまとめた「Object Oriented Design Principle」が上げられます。その中でもOpen-Closed Principle(OCP)は重要で中心となる法則と言えます。
もともとはバートランド・メンヤー(Bertrand Meyer)氏(「Design By Contract(契約による設計)」に基づくオブジェクト指向言語「Eiffel」の考案者)が提唱したものです。
OCPとは「モジュールは拡張に対して開いて(open)おり、修正に対して閉じて(closed)いなければならない」という原理です。
「拡張に対して開いている」(open)とはモジュール(或るいはコンポーナント)が拡張可能であること、つまり機能の追加(取扱うデータ種類の追加)が可能である事を意味し、「修正に対して閉じている」(closed)とは機能の追加に対して、既存のモジュールの修正を行う必要がない事を意味します。
言い換えれば、データ型の追加(新たなる種類のオブジェクト)に対して既存のコンポーネントに修正を加える必要がなく、追加となるデータ型に対する新たなオブジェクトの作成で対応可能であり、またその新たなオブジェクトは既存のコンポーネントに簡単に組み込めるようになっている必要があるわけです。
ABC記譜法の音符を愛け取り楽譜として保存する機能と、保存している楽譜で演奏する機能を持ったクラスになります。
(※ 便宜上音程は1オクターブとし音符は 1,2,4,8 のいずれかとします。また、指定した周波数と時間で音を鳴らすSound.beep()メソッドがあるものとします)
class MusicNotation {
private List<String> _notes = new ArrayList<String>();
// 音符の保存
public void addNote(String note) {
if (note == null || note.length() != 2) return;
_notes.add(note);
}
// 演奏
public void play() {
for (String note : _notes) {
double len = 60 / Integer.parseInt(note.substr(1));
char pitch = note.charAt(0);
switch (pitch) {
case 'C': Sound.beep(261.6256, len);
break;
case 'D': Sound.beep(293.6648, len);
break;
case 'E': Sound.beep(329.6276, len);
break;
case 'F': Sound.beep(349.2282, len);
break;
case 'G': Sound.beep(391.9954, len);
break;
case 'A': Sound.beep(440.0000, len);
break;
case 'B': Sound.beep(493.8833, len);
break;
}
}
}
}
かなり単純化してありますが、概ねこのようなコードになると思われます。 インターフェースを利用した方法で上記と同様の機能を持ったクラス群を考えてみます。
// 音符を規定したインターフェース
public interface Note {
public double getLength();
public double getPitch();
}
// 楽譜クラス:Noteインターフェースによる保存、演奏を行う
class MusicNotation {
privare List<Note> _notes = new ArrayList<Note>();
public void addNote(Note note) {
_notes.add(note);
}
public void play() {
for (Note note : _notes) {
Sound.beep(note.getPitch(), note.getLength());
}
}
}
// C音の音符クラスの実装
public class NoteC inplements Note {
private final double BASE_FREQUENCY = 261.6256;
private int _note;
private int _octave;
public NoteC(int note, int octave) {
_note = note;
_octave = octave;
}
public double getLength() {
return (60 / note);
}
public double getPitch() {
if (_octave < 0) return (BASE_FREQUENCY / ((_octave * -1) + 1));
return (BASE_FREQUENCY * (_octave + 1));
}
}
サンプルとしてはC音のクラスのみですがその他の音階のクラスも容易に作成可能だと思います。 OCPを満たすようなモジュールの設計は、基本的には次のような考え方になります。
1.変更の可能性のあるところ(ホット・スポット)を見つける
2.その部分を抽象化してクラスにする。
3.抽象クラスのサブクラスで変化に対応する。
1. 変更の可能性のあるところを見つける。
→取扱をデータの種類が増加する、条件によって使用する値や処理が異なるところを見つける。
(if文やswitch文を中心にチェックする)
2. その部分を抽象化してクラスにする。
→変更の可能性がある処理に必要な機能を洗い出し、インターフェースとして定義する。必要に応じて抽象クラス(abstract class)にする。
元の処理をインターフェースを利用して行うように変更する。
3. 抽象クラスのサブクラスで変化に対応する。
→条件による部分を条件毎にインターフェースを実装したクラスとして作成する。
(※ 同じような処理が必要な場合の考察)
・ 同一処理部分をもったスーパークラスを作成し、各処理はそれを継承する型で作成する。
→継承を利用するオブジェクト指向では普通の対応方法
・ 同一処理部分と基本となる処理をもったベースクラスを作成し、各処理は委譲(Decorator , Wrapperパターン)で拡張する。
→複数の機能に対して拡張が必要な場合、あるいは元の機能に対してほんの一部のみの拡張に対して有効
Software entities should be open for extension,but close for modification.
ソフトウェアの構成要素は拡張に対して開かれてなければならない、しかし、修正に対しては閉じてなければならない。
Function that use pointers or refrences to base classes must be able to use oblects of derived classes without knowing it.
基底クラスの操作は使用するオブジェクトから利用可能でなければならない。
(サブクラスは基底クラスの代わりとして振舞えなければならない。)
A.High level modeles should not depend upon low level modules. Both should depend upon abstractions.
B.Abstracions should not depend upon details. Details should depend upon abstractions.
A.高位のモデルは低位のモデルに依存してはいけない。いずれも抽象に依存しなければならない。
B.抽象は実像に依存してはいけない。実像は抽象に依存すべきである。
(抽象に依存すること、具象に依存してはならない。)
Clients should not be forced to depend upon interfaces that they do not use.
クライアントはインターフェースに依存することを無理に作ってはならない。
(Interfaceは分離すること。1つのInterfaceにつめこんではならない。)
The granule of reuse is the granule of release. Only componenties that are released through a tracking system can be effectivery reused. This granule is the package
再利用の粒度は公開する粒度である。追跡システムを通して公開されたコンポーネントのみが有効に再利用できる。この粒度はパッケージである。
(クラス単位での再利用はありえない。リリース可能なパッケージがバージョン管理をされている場合のみ再利用できる)
The classes in a package are reuse together. If you reuse one of the classes in a package, you reuse them all.
パッケージの中のクラスは一緒に再利用される。パッケージの中の一つのクラスを再利用するならば、全てを再利用しなければならない。
(パッケージ内のクラスは、パッケージ外のクラスに依存してはならない。パッケージは独立していること。)
The classes in a package should be closed together against the same kinds of changes. A change that affects a package affects all th classes in that package.
変更のあったクラスのみでなくパッケージ内のクラスは一緒にクローズされる。パッケージ内のクラスの変更はパッケージ内の全てのクラスへの影響となる。
(修正はパッケージ内で閉じているべきである。変更に対して修正が発生するクラスは同一パッケージとする。)
The dependency structure between packages must be a directed acyclic graph (DAG). That is there must be no cycles in the dependency structure.
パッケージ間の依存関係は非循環の図式にならなければならない。パッケージ間の依存関係は、依存構造の中で循環してはならない。
(依存関係は循環してはならない。)
The dependencies between packages in a design should be in the direction of the stability of the packages. A package should only depend upon packages that are more stable that it is.
パッケージ間の依存は安定の方向に向かわなければならない。パッケージはより安定したパッケージに依存すべきである。
(パッケージの依存関係はより安定したパッケージに向かうべきである。)
*安定性とは「変更する難易度」(変更する必要性が低い)と言える
Packages that are maximally stable should be maximally abstract. Instable packages should be concrete. The abstraction of a package should be in proportion to its stability.
最も安定したパッケージは最も抽象的である。不確定なパッケージは非抽象的である。パッケージの抽象度は安定性に比例する。
(パッケージの依存関係は抽象度の高い方向へ進むべきである。)