#はじめに
オブジェクト指向の設計原則を説明します
勉強内容のアウトプットです
#単一責任の原則
単一責任の原則は非常にシンプルな内容で、「1つのクラスに1つの役割(機能)」と言うものです。
これはカプセル化で強く言われる「小さなカプセル」ということに通じます。
ただ、この「1つのクラスに1つの役割」という考え方は正しいのですが、コレを正確に運用するのは非常に難しいです。運用が難しいというのは「1つのクラスに1つの役割」という言葉を指針としてしまうと思わぬ間違いを生むということです(言葉自体は正しいが、人間はアホなので間違っちゃうのです)
そこで、クラスの妥当性をチェックするために別の角度から眺める必要が出てきます。この時の言葉が
「クラスを変更する理由が2つ以上存在してはならない」
という言葉です。この言葉は単一責任の原則の話で重要となる言葉ですので、覚えておいてください。
さて、あなたはあるシステムAとシステムBを仲介するクラスを作成しているとします。ここで「1つのクラスに1つの役割」という考えに基づいて「システムAからのクエリをシステムBに通知する」というクラスを作成しました。
しかしながら、このクラスが実際には「システムAからのクエリを分析する」「システムBに通知する」という機能が複合したものとなっていました。
なぜこのようなことが起きたのでしょう。それは「システムAからのクエリをシステムBに通知する」という複数の手順からなる作業を一つの概念にしてしまったからです。人間は複数の役割をまとめ上げて(ラップして)一つの役割としてしまいがちです。なので「1つのクラスに1つの役割」という言葉自体は完全に正しいのですがそれを指針としてしまうと、間違いを起こしやすくなってしまうのです。
なので先ほどの「クラスを変更する理由が2つ以上存在してはならない」という言葉を元にそのクラス設計が妥当であるかを確認しなければならないのです。
この言葉でチェックをすれば「クエリのプロトコルが変わるかもしれない」「通知のプロトコルが変わるかもしれない」と言うように複数の変更理由が考えられるために、単一責任の原則に違反していることが分かるのです。
もちろん、この言葉自体も万能ではないですし、この言葉を使っても間違いは起こります(人間だから)。
重要なことは、同じことを説明する複数の表現を知っておくことで、間違いを減らすことが出来るということです。
#オープン・クローズドの原則
この原則は「クラスは拡張に対して開いていて、修正に対して閉じていなければならない」という表現が用いられます。
これは、クラスに対して「拡張が出来る」「修正する場合はそのクラスだけ修正すればいい」という2つの条件を要求しているものです。
言わんとしていることはわかりますよね。では、問題は「どうやってそれを実現するか」と言う点です。
実現するためのテクニックの王道があります。それは
「クラスはオブジェクトではなくインターフェースに依存せよ」
というものです。
これだけではなんのこっちゃわからないですよね。ですので、以下に例を示します。
例えば、あるクラスから受け取った情報をDBに保存する機能をもつクラスを作ったとします。例えばこんなクラスです。
class pushData
{
int Push()
{
dataPackage object = new dataPackage();
int A = object.CalcA(pushCode);
//以降、処理が続く
}
}
このクラスの問題点はどこでしょうか。それはこのPushメソッドがもはやdataPackage型しか受け入れられないことです。
もし、dataPackage型ではなくnewDataPackage型を使いたい場合、あるいはそれらを動的に切り替えたい場合にこのようなコードだとそれが出来なくなってしまいます。
なぜこのような事態が発生するのでしょうか。それはこのPushメソッドが具体的なインスタンスを使っている(=依存している)からです。
具体的なインスタンスを使ってしまうとそのメソッドは生成したインスタンスとべったりと糊でくっついたようなものとなってしまい、拡張性が失われてしまいます。
そこで、先ほどのクラスを以下のように修正します。
class pushData
{
int Push()
{
iDataPackage object = iDataPackageFactory.create();
int A = object.CalcA(pushCode);
//以降、処理が続く
}
}
interface iDataPackage
{
int CalcA(pushCode code);
}
class iDataPackageFactory
{
iDataPackage create()
{
//条件に応じてiDataPackageインターフェースを継承したクラスを生成して返す処理
}
}
ここではpushメソッドは、iDataPackageFactoryのcreateメソッドを通じてiDataPackageインターフェースを継承したクラスを受け取っています。
このコードの重要な点は、Pushメソッドは、iDataPackageインターフェースを継承したクラスであれば何でも受け入れられるということです。
このように、クラスががインターフェースのメソッドを使用することで、「そのインターフェースを継承したクラスならなんでもOK」というようになります。これが「インターフェースに依存せよ」と言う意味です。
ここまでで「拡張に対して開いている」を実現する方法はわかったかと思います。では、「修正に対して閉じている」とはどのようにして実現すればよいのでしょうか。
実は、上に書いたようにインターフェースに依存するような形にしておくことで自然と修正に対して閉じたクラスが出来上がります。
それはなぜかと言うと、先ほどのPushメソッドがインターフェースに依存しているためiDataPackageインターフェースを継承した各クラスの個別の処理についてはいくらでも変更できるからです。
別の表現を使うと、PushメソッドはiDataPackageインターフェースで規定したI/Oに則った形式のクラスであること以外は求めていないからです。
ここまでがオープン・クローズドの原則の説明です。
ちなみに、ここで書いた「インターフェースに依存せよ」という言葉は「newを隠蔽せよ」という考えと非常に近いです。
コレについては私が過去の記事で書いてますので、見てみてください(宣伝)。
#リスコフの置換原則
この原則は「基本クラスを使っている場所で基本クラスの代わりにサブクラスを使っても問題なく動かなけらばならない」というものです。
ここで、あなたがあるメソッドを作成したとしましょう。そして、そのメソッドでsample型の引数を取っているものとします。
int hogehoge(sample Object)
このhogehogeメソッドを使う人は「sample型を引数に取っているということはこのサブクラスnewSampleクラスを引数として渡しても問題なく動作するはずだ」と言うことを期待しますし、そのような使い方をしてもコンパイルエラーとはなりません。
ところが、このあなたが作ったメソッドがもし「newSample型を引数として渡すと問題が発生する」と言うメソッドであった場合どうなるでしょうか。
その場合、あなたはこのhogehogeメソッドに
//このメソッドはsample型しか受け入れられません。サブクラスは引数として渡せません。
int hogehoge(sample Object)
という注意書きを書かなければなりません。「注意書きぐらい書けばいいじゃん。使う人が気を付ければいいじゃん」と思う人もいるかもしれませんが、そもそもオブジェクト指向では「クラスを使う人」「クラスを作る人」と言うのを明確に分離して考えます。
このとき「クラスを使う人はわざわざ使うクラスの詳細を意識しなくても良い」というようにしなければならないのです。
「使う人が気を付ければいいじゃん」という言葉は「クラスを使う人は使うクラスの詳細を知っていなければならない」と言うことと同義です。
これでは良いクラスとは言えませんね。
ですので、あなたはメソッドを作るとき(あるいはクラスを作るとき)にあるクラスを用いている場合、そのクラスの代わりにその派生クラスを使っても問題なく動くようにしなければならないのです。
###契約による設計との関係について
リスコフの置換原則は契約による設計と非常に面白い関係があります。
まず、契約による設計について説明します。
契約による設計とはクラスと、そのクラスを使う人にに対して以下の要請をするものです。
1.メソッドの利用者は使うメソッドのインプットのルールを守らなければならない。
2.メソッドは、インプットが正常であれば必ずルールに則ったアウトプットをしなければならない。
3.メソッドは、インプットが正常であればそのメソッドが属するクラスの変数を不正な値にしてはいけない。
この観点から基本クラスとサブクラスの関係についてみていきます。
「基本クラスを使っている場所で、代わりにサブクラスを使っても大丈夫」と言う状態であるということは、以下のような状態となっています。
- 1.サブクラスのメソッドは、基本クラスのメソッドよりもインプットの条件が緩い
例えば基本クラスのあるメソッドのインプットの条件が「整数である」と言うことに対してサブクラスの対応するメソッドのインプットの条件が「正の整数である」というより厳しい条件だったとします。
契約による設計は、正しいインプットを行う責任はメソッドの利用者側にあります。そのため基本クラスを使用している場所では「インプットに整数を使う」という責任があります。しかしながら責任はあくまで「整数」という範囲でしかありません。もし、サブクラス側の条件が「正の整数」というより厳しい条件である場合、基本クラスを使っている箇所をサブクラスに置き換えることが出来なくなってしまいます。
いっぽうで、基本クラスの条件が「整数である」と言うことに対してサブクラス側の条件が「実数である」というより緩い条件である場合はこのような問題が起こりません。
- 2.サブクラスのメソッド、基本クラスのメソッドよりもアウトプットの条件が厳しい
例えば基本クラスのあるメソッドのアウトプットの条件が「整数である」と言うことに対してサブクラスの対応するメソッドのアウトプットの条件が「実数である」というより緩い条件だったとします。
この時、基本クラスのメソッドを使用している場所では使用者側は「整数が返ってくる」と言うことを期待しています。もし、ここでサブクラス側の条件が「実数である」というより緩い条件であった場合、使用者側が予想していない状態となってしまいます。一方でサブクラス側の条件が例えば「正の整数」というより厳しい条件である場合、使用者側の予想の範囲内に収まってしまうので問題が発生しません。
このように、契約による設計からリスコフの置換原則を見た場合に上位-下位の関係が継承関係と等価な場合に満たさなければいけない条件が見えてきますね。
#依存関係逆転の原則
依存関係逆転の原則についてよく使われる言葉は以下のようなものです
上位のモジュールは下位のモジュールに依存してはならない。どちらのモジュールも「抽象」に依存すべきである。
これはどういう意味なのでしょうか。
例えば、あなたがCarクラスを作っていたとします。
この時、車が発進するときの動作を以下のような形で実装したとします。
void Departure
{
Engine normalEngine = new Engine();
//スイッチを押す
normalEngine.switchA_push();
//エンジンの状態を変える
normalEngine.State = WaitingMode;
//レバーを引く
normalEngine.lever_pull();
//エンジンの状態を変える
normalEngine.State = WaitingMode;
//ピストンを動かす
normalEngine.piston_move();
//エンジンの状態を変える
normalEngine.State = DepartureMode;
}
このメソッドの問題点は、もしEngineクラスのswitchA_pushやlever_pullメソッドが変更された場合に、CarクラスのDepartureメソッドまでその変更の影響がないかを調査しなければならないということです。
なぜこのようなことが起きたのでしょうか。
そもそも、CarクラスとEngineクラスでは、明らかにCarクラスが上位のクラスのはずです。しかしながら、Carクラスのメソッドをよく見てみると
Engine normalEngine = new Engine();
この1文が存在しているために、CarクラスがEngineクラスに依存してしまっています。これが上位のモジュールが下位のモジュールに依存しているということです。
CarクラスがEngineクラスのいくつものメソッドをピコピコ叩いたり、変数をいじるのは避けるべきですし、具体的なインスタンスを使うのも良くありません(要するに、newを隠蔽する必要があるということです。)
よって、このようなコードは以下のように書き換えるべきなのです。
class Car
{
void Departure()
{
iEngine engineObject = iEngineFactory.create();
engineObject.Departure();
}
}
//状況に応じてどのエンジンを使うかを決め、そのエンジンのインスタンスを渡すためのクラス
class iEngineFactory
{
public static iEngine create()
{
//普通のエンジンが必要な状況なら
return new normalEngine();
//特別なエンジンが必要な状況なら
return new hogehogeEngine();
}
}
//エンジンが持つ機能(Departure)を保証するインターフェース
interface iEngine
{
void Departure();
}
//普通のエンジン
class normalEngine
{
//iEngineインターフェースを継承
implement iEngine;
Mode State;
void Departure()
{
//スイッチを押す
switchA_push();
//エンジンの状態を変える
State = WaitingMode;
//レバーを引く
lever_pull();
//エンジンの状態を変える
State = WaitingMode;
//ピストンを動かす
piston_move();
//エンジンの状態を変える
State = DepartureMode;
}
}
ここでのポイントの1つ目は、「Carクラス側でファクトリを使っているということ」です。これによってCarクラスはEngineクラス側の具体的な実装に影響されることはありません。
2つ目は「発進の際の詳細な動作をEngineクラス側に記述した」と言うことです。コレによって、Engineクラスが外部に対して細かい動作を公開する必要がなくなり、Engineクラスの抽象度が高くなっています。その結果、CarクラスがiEngineインターフェースという抽象的な存在に依存するようになったのです。
さて、ここでiEngineインターフェースに求められることは何なのでしょうか。それは「Carクラス側が使いやすいインターフェースであること」です。
先ほど言ったようにCarクラスとEngineクラスではCarクラスの方が上位の存在です。「Carクラス側が使いやすいインターフェースであること」と言うことはEngineクラスがCarクラスと言うより上位の存在に依存するようになっているということです。
下位のモジュール(=Engine)が上位のモジュール(=Car)に依存するようになったわけですね。
では、依存関係逆転の原則の「逆転」とは何を指しているのでしょうか。
この項で一番最初に書いたこのコード
void Departure
{
Engine normalEngine = new Engine();
//スイッチを押す
normalEngine.switchA_push();
//エンジンの状態を変える
normalEngine.State = WaitingMode;
//レバーを引く
normalEngine.lever_pull();
//エンジンの状態を変える
normalEngine.State = WaitingMode;
//ピストンを動かす
normalEngine.piston_move();
//エンジンの状態を変える
normalEngine.State = DepartureMode;
}
実はコレ、手続き型言語ではよく見られた記述の方式ですよね。この書き方はここまでで書いたように「上位のモジュール(=Car)が下位のモジュール(=Engine)に依存している」というものになっています。
一方で、今回の原則に沿った書き方をすると
「下位のモジュール(=Engine)が上位のモジュール(=Car)に依存している」
と言う状態になります。コレを「逆転」と言っているわけです。
#インターフェース分離の法則
インターフェース分離の法則は以下のようなものです
「クライアントに、クライアントが利用しないメソッドへの依存を強制してはならない。」
これは何を指しているのでしょうか。
簡単に言えば「インターフェースはシンプルにしなさい」と言うことです。
例えば、以下のようなコードがあったとします。
interface ultraSuperDeluxeGreateBigSugoiEngine
{
void Departure();
void PlayMusic();
void CallYourGirlFriend();
//その他いろんな機能
}
このインターフェースの問題点は「このインターフェースを継承したクラスのPlayMusicメソッドを変更した場合に他のメソッドも修正する必要がある」というものです(修正する必要が無かったとしても、修正が必要かどうかのチェックは必要です。チェックをするということは労力がかかります)。
なぜこのような問題が起こるのでしょうか。それはインターフェースが過剰に大きいからです。
そもそも、エンジンに音楽をかける機能や彼女を呼び出す機能が必須でしょうか?そのオブジェクトをそのオブジェクトたらしめる最低限の機能をインターフェースの単位とすべきです。
今回の例であれば明らかに過剰であることが分かりますが、なかなかわかりにくい場合もありますし、頭の固いおっさんが
「1か所にまとめといた方が便利」だとか「いちいち何個もインターフェースを継承するのはめんどくさい」などと言いだしたりする場合もあるので、騙されないようにしましょう。
#おわりに
とりあえず、ここまでがオブジェクト指向の設計原則の内のクラス設計に関わる部分です。
クラス設計に関わるもの以外の設計原則についてはそのうち書きます。