モチベーション
会社にあった「.NETのエンタープライズアプリケーションアーキテクチャ第2版」がなかなか興味深いし勉強になるので読書メモ。
本当は、本に書き込みたい派なんだけど、会社のものに書き込むわけにはいかないので、読書メモとして残しておく。
概要
内容としては、オブジェクト指向の基礎や、DDDについてのことなど、個人的に興味を引く内容の本です。
ただ、高い!技術書って高い!
基本、備忘録として残すのを前提としているので、メモの順番には規則性がない可能性があります。
第3章 ソフトウェアの設計原則
Code Contract ライブラリ(3.4.2 ソフトウェア契約)については必読!!
VS2017では Code Contract ライブラリが非対応とのこと1。もちろん .NET Core でも非対応2。
こんな素晴らしい機能がなんで非対応になってんの・・・。
ってことで、後半部分は、2017/09現在では事実上役に立ちません。
3.2.2 インターフェイスに対するプログラミング
当たり前だけど、クラスの呼び出し元とクラスの呼ばれた側の間には依存関係が生まれる。
ログ機能などの横断的関心事については、依存関係が発生しないようにしたほうがいい。
横断的関心事とは・・・
クラスに設定された要件に対して厳密には関係のない機能のこと。
例えばログ、エラー処理、など。
要はいろいろなクラスで共通的に使われるものが該当すると思う。
依存関係が発生しないようにする手段として、DI や Service Locator パターンを使用して、インターフェイスに依存するようにして依存性を解消する方法がある。
3.2.3 合成と継承
クラスの再利用の手段として、合成(ブラックボックス)と継承(ホワイトボックス)がある。
継承では、基底クラスと最強度の密結合になる。
対して、合成では、基底クラスのインスタンスを保持する新しいクラスを作る。こちらのほうが依存関係はあるが、疎結合になる。(たぶん委譲による結合)
基本的には、クラスを再利用する場合は、継承するよりも合成にしたほうが、リスクが低い。
3.3.1 SOLID原則
ソフトウェアデザインのための基本原則。これは最低限覚えておきたい。
単一責任の原則(SRP:Single Responsibility Principle)
クラスを変更する理由は常に1つでなければならない
ただしこれを突き詰めると貧血クラス(プロパティだけで振る舞いがないデータクラス)になるので、程ほどに。
開放/閉鎖の原則(OCP:Open-Closed Principle)
モジュールは、拡張に対して開いていなければならず、修正に対して閉じていなければならない
- 開放→関連する機能の拡張に対しては開いている必要がある
- たとえばログ出力クラスに出力レベルErrorとInfoがあった場合、Fatalを追加することに開いておく必要がある。
- 閉鎖→拡張時に、関連する既存コードを変更しなくてもよいようにする
- たとえば上記のログ出力クラスに関しては、Fatalを追加したからと言って、ErrorやInfoのログ出力メソッドに変更が発生しないようにする
リスコフの置換原則(LSP:Liskov Substitution Principle)
サブクラスをその基底クラスと置き換えることができなければならない
継承に関する原則。
LSPの本質は、派生クラスが基底クラスの実行条件を制限できないことにある。
↑どゆこと?
基底クラスが持つ事前条件に、派生クラスが事前条件を追加するようなことはNG。
ここは、言っていることはわかるが、なんでなのかがよくわかってないので勉強する必要がある。
インターフェイス分離の原則(ISP:the Interface Segregation Principle)
クライアントが使用しないインターフェイスにクライアントを強制的に依存させてはならない
インターフェイスを肥大化させないようにする。
つまり、インターフェイスも機能ごとに分離させましょうねっとこと。
依存関係逆転の原則(DIP:Dependency Inversion Principle)
上位モジュールを下位モジュールに依存させるのではなく、両方のモジュールを抽象化に依存させるべきである。
まんまその通り。対応方法には DI パターンや Service Locator パターンがある。前にまとめたのでこちらを参照。
3.4.1 If-Then-Throw パターン
防御的プログラミングの一環で、引数チェックをして、以上なら例外を返すという方法がある。
- コンストラクタで適用すると効果的
- 基本的にはパブリックなメソッドに対して行う。プライベートなメソッドはクラスの内側でしか呼ばれないので、作成者が把握しているはずだから引数チェックは不要。
- しかしこれはメソッドの事前条件の評価のみで、不変条件や事後条件については何もできない。
- それらに対しては、「契約による設計」の考え方を導入する必要がある。
ここでもやはり、契約による設計が言及されている
3.4.2 ソフトウェア契約
.NET Framework 4 で追加された Code Contracts
ライブラリでソフトウェア契約が作成できるらしい。
ソフトウェア契約の実装では、構文の詳細に関係なく、クラスメソッドを記述する際は常に以下の質問に答えられるようにする必要がある。
・どのような条件下でメソッドを呼び出せるか(事前条件)
・メソッドの終了後にどのような条件が検証されるか(事後条件)
・メソッドの実行の前と後で変化しない条件は何か(不変条件)
事前条件
事前条件とは、メソッドの実行を開始する前に検証しなければならない Boolean 条件のこと。
以下の2つのこと。
・入力パラメータに対する条件
・メソッドが定義されているクラスの現在の状態に対する条件
概念的には、事前条件は If-Then-Throw パターンと同じ。
以下のように、Contract.Requires<T>(bool 事前条件)
という構文で、事前条件を表現できる。
事前条件が満たされない場合、<T>
で指定した例外がスローされる。
public FooClass(string id, string name)
{
Contract.Requires<ArgumentException>(id.IsAlphaNumetric(5, 5); // idは5桁の数値であること
Contract.Requires<ArgumentException>(!name.IsNullOrWhitespace()); // nameがNULLまたはスペースのみでないこと
}
Code Contracts
ライブラリの表現方法によるメリットは以下がある
- 通常の If-Then-Throw パターンでは、「条件に合致しない場合」というように否定で表現するが、
Contract.Requires<T>
では、事前条件を肯定で表現できるので直感的である- → テストコードが書きやすい
- ちゃんとコードを記述すると、コードが説明になるため、引数のコメントが不要になる
- 引数コメントはコードと同期がとれなかったりしてコードレビュー等で無駄な指摘を受けるので、これは大きい!
事後条件
事後条件とは、メソッドによって生成される出力と、オブジェクトの状態に対する変更のこと。
メソッドから制御を戻すときに Boolean 条件で表現される。
以下のように、 Contract.Ensures
と Contract.Result<T>
メソッドで表現することができる。
public int Abs(int x, int y)
{
// 事後条件の表明
Contract.Ensures(
Contract.Result<int>() >= 0 // 事後条件
);
if(x == y)
{
return 0;
}
return x > y ? x - y : y - x;
}
メソッドの最初に事後条件を表明することができる。
また、もし事後条件を満たせなかった場合は、 ContractException
例外がスローされる。
不変条件
不変条件とは、コンストラクタやセッターなど、パブリックメソッドの実行中に変化しないクラスメンバーに適用される Boolean 条件のこと。
オブジェクトが論理的に矛盾した状態になるのを絶対に避けなければならないシナリオでは、常に不変条件が使用される。
以下のように、 Contract.Invariant
メソッドで表現することができる。
class News
{
public string Title { get; set; }
public string Body { get; set; }
[ContractInvariantMethod]
void InvaliantMethod()
{
Contract.Invariant(!string.IsNullOrEmpty(Title));
Contract.Invariant(!string.IsNullOrEmpty(Body));
}
}
と、ここまで書いてきたが、 Code Contract が VS2017 に非対応ということがわかった。
なんやねんそれ・・・
こういう機能こそ、求められてるものだと思うんだけど。C# 7.1 なんて作ってる(ry