前回のレクチャーでは、クラスをカプセル化するという考え方について解説しました。今回はその内容を少しおさらいしながら、サブシステム内のクラスの公開・非公開についてまとめていきます。
クライアントから見えないほうがよいもの
Machineプロジェクトのようなサブシステムが持つクラスやメソッドは、できるだけクライアント側から見えないようにしておくのが望ましい設計です。
画面側のコードでドットを入力してIntelliSenseを表示したときに、候補がたくさん出てきてしまうと、利用する側は「どれを使えばいいのか」「これは使ってよいものなのか」と迷ってしまいます。
図1 Form1.csでMachine.と入力し、これまでの実装内容を振り返る
そのため、極力そのプロジェクト(サブシステム)内でしか使わないものについては、internalアクセス修飾子を活用して内部に閉じ込めてしまうのが基本方針になります。Facadeパターンであれば、外部からはFacadeクラスだけが見えるようにする、というのが理想的な形です。
こうしておくことで、クライアントはFacade経由でしか処理を呼び出せなくなります。その結果として、次の2つのメリットが生まれます。
・使いやすい……余計な選択肢が見えないため、迷わず使うことができる
・処理を統一できる……サブシステムの利用方法がFacade経由の一本道になる
GoFの書籍における「サブシステム内のクラスの公開・非公開」
このクラスをカプセル化するという話は、GoF(Gang of Four)の『デザインパターン』にも「サブシステム内のクラスの公開・非公開」という形で説明されています。
図2 GoFの『デザインパターン 改訂版』の書影
同書によれば、クラスというのは値とオペレーション(操作)をカプセル化したものですが、サブシステムはさらにその一段上のレベルで、クラス自体をカプセル化することができます。サブシステム内でしか使わないクラスは非公開にすることが有効である、という趣旨の記述があります。
ただし、この本が書かれた当時は、サブシステム単位でアクセスレベルを制御できるオブジェクト指向言語はほとんど存在しませんでした。もちろん当時はC#もまだ存在していません。
その後継として、C#にはこうした用途のためにinternalというアクセスレベルが用意されています。これをうまく活用することで、サブシステム内でしか使わないクラスを非公開にすることができます。
基本方針:internalで作り、必要になったらpublicに昇格する
以上を踏まえると、サブシステム内でしか使わないクラスやメソッドは、まずinternalにしておくのが基本方針になります。
実際に、Machineプロジェクト内のBox.csを見てみると、クラス自体もメソッドもinternalで定義されています。
図3 Box.cs:クラス(internal class Box)もメソッド(GetInternalTemperature/GetExternalTemperature)もinternalで定義されている
一方、これらのサブシステム内クラスをまとめて外部に公開する窓口であるMachineFacadeクラスは、public staticなメソッドとして各サブシステムの処理を橋渡ししています。例えば扇風機(Fan)を回転させるFanSpinメソッドは、次のようにFanEntityを戻り値として返します。
図4 MachineFacade.cs:public static FanEntity FanSpin(int fanId)
internalのままではコンパイルエラーになる
ここで、FanSpinメソッドの戻り値の型であるFanEntityクラス自体を見てみると、internal sealed class FanEntityとして定義されています。
図5 FanEntity.cs:internal sealed class FanEntityのまま
この状態のままだと、MachineFacade.FanSpinメソッドの定義箇所にエラーの波線が表示されます。カーソルを合わせると、次のようなエラーメッセージが表示されます。
| CS0050: アクセシビリティに一貫性がありません。
戻り値の型 ‘FanEntity’ のアクセシビリティは メソッド ‘MachineFacade.FanSpin(int)’ よりも 低く設定されています |
図6 CS0050エラー:戻り値の型のアクセシビリティが低いという指摘
これは、publicなメソッドの戻り値としてinternalなクラスを外部に返そうとしていることが原因です。publicなFanSpinメソッドの外側(クライアント側)からは、internalなFanEntityクラスを参照できないため、C#コンパイラーがこの矛盾を「アクセスレベルの一貫性がない」として検出してくれるわけです。
必要になった時点でpublicに昇格させる
このエラーが出たときにやるべきことは単純で、「あ、ここはpublicにしておかないといけないんだな」と気づいて、該当のクラスをinternalからpublicに昇格させるだけです。
今回の例では、FanEntityクラスの宣言をinternal sealed class FanEntityからpublic sealed class FanEntityに変更します。
// 変更前 internal sealed class FanEntity // 変更後 public sealed class FanEntity
図7 public sealed class FanEntityに変更し、エラーが解消された状態
修正後は画面下部に「問題は見つかりませんでした」と表示され、エラーが解消されていることが確認できます。
このように、基本はすべてinternalで作っておき、コンパイルエラーが出た時点で「本当に公開が必要かどうか」を判断しながらpublicに昇格させていく、という流れで実装を進めていくのがポイントです。最初からなんでもpublicにしてしまうのではなく、必要最小限の公開範囲に留めることで、サブシステムの見通しがよくなります。
フォルダー構成の工夫
最後に、以前のレクチャーでも触れたフォルダー構成について改めて確認しておきます。Machineプロジェクトでは、サブシステム内部の各クラス(Box.cs、Camera.cs、Fan.cs、FanEntity.cs、Power.cs)をObjectsフォルダーにまとめてあり、外部に公開する窓口であるMachineFacade.csだけをプロジェクト直下に置いています。
図8 ソリューションエクスプローラー:Objectsフォルダーを畳むとMachineFacade.csだけが直置きされて目立つ
Objectsフォルダーを畳んだ状態にしておくと、プロジェクト直下にはMachineFacade.csだけが見えるようになります。こうしておくことで、「このサブシステムはFacade経由で使うんだな」ということが、コードを見る前の見た目の時点で伝わるようになります。
サブシステムを設計・実装する側は、極力こうした形で、外部から使う人にとって余計なものを見せず、「こういうときにはこう使う」ということが自然に伝わるような実装を心がけていくとよいでしょう。
■非公開コース「C#14新機能」プレゼント:
非公開コース「C#14新機能」(80分)をご覧になりたい方は
こちらからURLとパスワードを発行していますので、ご覧になってみてください。
非公開コース「C#14新機能」を観る
参考にした書籍
実戦で役立つ C#プログラミングのイディオム/定石&パターン
プログラミングC# 第8版
オブジェクト指向
・アジャイルソフトウェア開発の奥義 第2版 オブジェクト指向開発の神髄と匠の技
すばらしい本です。オブジェクト指向が詰まっています。
・オブジェクト脳のつくり方―Java・UML・EJBをマスターするための究極の基礎講座
サンプルコードはjavaですが,最初にオブジェクト指向に目覚めるのに最適な本です。
・Head Firstオブジェクト指向分析設計 ―頭とからだで覚えるオブジェクト指向の基本
マンガみたいな感じで読めて,オブジェクト指向について学べます。
・Clean Code アジャイルソフトウェア達人の技
・Clean Architecture 達人に学ぶソフトウェアの構造と設計
・Adaptive Code ~ C#実践開発手法
・.NETのエンタープライズアプリケーションアーキテクチャ
・実装パターン
廃盤のため異常に高くなりすぎなので,様子を見た方がいい気がします。一万円のボリュームがあるかどうかは微妙です。
・オブジェクト指向でなぜつくるのか
・レガシーコード改善ガイド
・リファクタリング 既存のコードを安全に改善する
デザインパターン
・オブジェクト指向における再利用のためのデザインパターン
・オブジェクト指向のこころ
・Head Firstデザインパターン ―頭とからだで覚えるデザインパターンの基本
ドメイン駆動開発
・エリック・エヴァンスのドメイン駆動設計
・実践ドメイン駆動設計
・ドメイン駆動
・現場で役立つシステム設計の原則
・エンタープライズアプリケーションアーキテクチャパターン