Facadeパターン

デザインパターン【C#】:Facade 05_クラスをカプセル化するということ

前回までのレクチャーで、Objectsフォルダーを作成し、ファサード以外のクラス(Box、Camera、Fan、FanEntity、Power)をすべてそこへ移動しました。プロジェクト直下にはObjectsフォルダーとMachineFacade.csだけが並ぶ、すっきりとした見た目になっています。

しかし、これで問題が解決したわけではありません。クライアント側のコード(Form1.cs)で「Machine.」と入力してみると、インテリセンスにはBox、Camera、Fan、FanEntity、MachineFacade、Powerといったクラスがすべて表示されてしまいます(図1)。つまり、フォルダーの見た目こそ整理されたものの、これらのクラスは依然として外部(Machine.UIプロジェクト)から自由にアクセスできる状態のままなのです。

図1 「Machine.」と入力した際のインテリセンス。フォルダーを整理しても、ファサード以外のクラスは依然として外部から見えてしまっている

クラスをカプセル化するという考え方

そこで今回は、「クラスをカプセル化する」という考え方を取り入れます。通常「カプセル化」というと、クラスの中にある値(フィールド)をprivateにして外から見えなくすることを指しますが、ここではもう少し粒度を上げて、Machineプロジェクト(サブシステム)というまとまりの中で、どのクラスを外部に公開し、どのクラスを公開しないかを制御することを「クラスをカプセル化する」と表現しています。

方針はシンプルです。クライアントに使ってほしいMachineFacadeクラスだけをpublicのまま公開しておき、それ以外のクラス(Box、Camera、Fan、FanEntity、Power)は、Machineプロジェクトの中に閉じ込めてしまいます。これにより、クライアントから見える型がファサードだけに絞られ、本来の意味でのカプセル化が実現します。

サブシステムのクラスをinternalにする

C#で「同じアセンブリ(プロジェクト)内からはアクセスできるが、外部のプロジェクトからはアクセスできない」という公開範囲を実現するアクセス修飾子が internal です。ここでは、Facade以外のクラス・メソッドをすべてpublicからinternalへ書き換えていきます。

まずはBox.csから始めます。もとのBox.csは次のように、クラスもメソッドもpublicで宣言されていました。

public class Box
{
    private Random _random = new Random();
 
    public int GetInternalTemperature()
    {
        return _random.Next(10, 80);
    }
 
    public int GetExternalTemperature()
    {
        return _random.Next(10, 80);
    }
}

この public を internal に書き換えます。「public」の文字を選択して「internal」と入力すると、インテリセンスの候補にもinternalが表示されます(図2)。クラス宣言だけでなく、GetInternalTemperature・GetExternalTemperatureの各メソッドもあわせてinternalに変更します。

図2 Box.csの「public class Box」を「internal」に書き換えているところ。インテリセンスにinternalの候補が表示される

書き換え後のBox.csは次のようになります。

internal class Box
{
    private Random _random = new Random();
 
    internal int GetInternalTemperature()
    {
        return _random.Next(10, 80);
    }
 
    internal int GetExternalTemperature()
    {
        return _random.Next(10, 80);
    }
}

同じ要領で、Camera、Fan、Powerの各クラスについても、クラス宣言およびすべての公開メソッドをinternalに変更していきます(FanEntityについては後述するため、いったんそのまま進めます)。Camera・Fan・Powerを変更し終えた状態は次のとおりです。

internal class Camera
{
    internal void Take()
    {
    }
}
 
internal class Fan
{
    private Random _random = new Random();
 
    internal FanEntity GetSpin(int fanId)
    {
        return new FanEntity(fanId, _random.Next(0, 3000));
    }
 
    internal void Start(int fanId)
    {
    }
 
    internal void Stop(int fanId)
    {
    }
}
 
internal class Power
{
    internal void On()
    {
    }
 
    internal void Off()
    {
    }
 
    internal void BacklightOff()
    {
    }
}

Power.csをすべてinternalに変更し終えた時点で、Box.cs・Camera.cs・Fan.cs・FanEntity.cs・Power.csのタブがすべて編集済み(タブ名にアスタリスクが付いた状態)になっており、エラーも警告も出ていないことが確認できます(図3)。

図3 Power.csを開いた状態。クラス・メソッドがすべてinternalに変更されている。他のサブシステムクラスもすべて編集済み

ここで注目したいのは、MachineFacade.cs自体は一切変更していないという点です。MachineFacadeもBoxやFanと同じMachineプロジェクトの中にあるため、internalになったこれらのクラスへ引き続きアクセスでき、コンパイルエラーは発生しません(図3の左上のPower.csタブの並びにMachineFacade.csが含まれていないのも、変更していないためです)。internalは「同じアセンブリ内であればアクセス可能」という公開範囲なので、ファサードの内部実装からの呼び出しには何の影響もないというわけです。

クライアント側でコンパイルエラーが発生する

ここまでの変更をMachine.UIプロジェクト側から見るとどうなるでしょうか。Form1.csには、以前のレクチャーで書いた次のようなコードがそのまま残っています。

private void Form1_Load(object sender, EventArgs e)
{
    var fan = new Machine.Fan();
    FanEntity entity = fan.GetSpin(4);
}

このコードをそのままにしておくと、`new Machine.Fan()` の部分に赤い波線が表示され、次のようなコンパイルエラーが発生します(図4)。

図4 Form1.csで発生するコンパイルエラー。「CS0122: ‘Fan’ はアクセスできない保護レベルになっています」

「CS0122: ‘Fan’ はアクセスできない保護レベルになっています」というエラーは、まさにFanクラスがinternalになり、Machine.UIプロジェクトからは直接アクセスできなくなったことを示しています。これは意図した挙動であり、ここから先はMachineFacade経由でしか呼び出せないように直していきます。

MachineFacade経由の呼び出しに書き換える

`var fan = new Machine.Fan();` と `FanEntity entity = fan.GetSpin(4);` の2行をコメントアウトし、代わりにMachineFacadeを使って書き直します。「MachineFacade.」と入力すると、インテリセンスにはBacklightOff、BoxExternalTemperature、BoxInternalTemperature、CameraTake、FanSpin、FanStart、FanStop、PowerOffといった、ファサードが公開しているメソッドだけが表示されます(図5)。BoxやFan、Powerといった内部クラスの名前は、もうここには出てきません。

図5 「MachineFacade.」と入力した際のインテリセンス。ファサードが公開するメソッドのみが候補に表示される

一覧からFanSpinを選択し、引数に4を渡します。もとのコードは `fan.GetSpin(4)` でFanEntityを取得していたので、置き換え後は次のように1行にまとめられます。

図6 コメントアウトした旧コードの下に、MachineFacade.FanSpin(4)を使った1行を追加したところ

private void Form1_Load(object sender, EventArgs e)
{
    //var fan = new Machine.Fan();
    //FanEntity entity = fan.GetSpin(4);
    FanEntity entity = MachineFacade.FanSpin(4);
}

戻り値の型(FanEntity)でも同じ問題が起きる

ここまでの修正でFanクラスへの直接アクセスは解消されましたが、実行してみるとまだエラーが残っています。FanSpinの戻り値の型であるFanEntityについても、他のクラスと同様にinternalへ変更していたためです(図7)。

internal sealed class FanEntity
{
    public FanEntity(int fanId, int value)
    {
        FanId = fanId;
        Value = value;
    }
 
    internal int FanId { get; }
    internal int Value { get; }
}

 

図7 「CS0122: ‘FanEntity’ はアクセスできない保護レベルになっています」というコンパイルエラー。戻り値の型がinternalだとクライアント側で受け取れない

MachineFacade.FanSpinの戻り値はpublicなメソッドを通じてクライアントに渡されるため、その型であるFanEntity自体はpublicのままにしておく必要があります。publicなメソッドの戻り値や引数に、internalな型を使うことはできない(アクセスレベルの一貫性が取れない)というC#のルールによるものです。

そこで、FanEntityクラスの宣言だけをpublicに戻します。クラス内のFanId・Valueといったプロパティ自体は、クライアント側から直接書き換える必要がないため、internalのままにしておいて問題ありません(図8)。

図8 FanEntity.csの最終状態。クラス宣言はpublicに戻すが、FanId・Valueプロパティはinternalのままにしている。エラーは解消されている

public sealed class FanEntity
{
    public FanEntity(int fanId, int value)
    {
        FanId = fanId;
        Value = value;
    }
 
    internal int FanId { get; }
    internal int Value { get; }
}

このように、「クライアントとの受け渡しに使われる型(この場合はFanEntity)」は、たとえ内部実装のためのクラスであっても、公開せざるを得ない場合があります。すべてを一律にinternalにするのではなく、実際に外部とやり取りされるものだけを見極めてpublicにする、という判断が必要になります。

最終確認

ここまでの修正で、Form1.csはコンパイルが通るようになりました。あらためてForm1.csで「Machine.」と入力してインテリセンスを確認すると、候補にはFanEntityとMachineFacadeの2つしか表示されなくなっています(図9)。Box、Camera、Fan、Powerといった内部実装のクラスは、もうクライアント側からは一切見えません。

図9 修正後に「Machine.」と入力した際のインテリセンス。FanEntityとMachineFacadeのみが表示され、内部実装のクラスは見えなくなっている

ソリューションエクスプローラーでMachineプロジェクトを見ても、直下にはObjectsフォルダーとMachineFacade.csしかなく、その中身(Objectsフォルダーの中のクラス群)はすべてinternalになっているため、外部からは実質的に「MachineFacadeというファサードと、それが返すFanEntityという型が存在する」ということしかわからない状態になりました(図10)。

図10 最終的なMachineプロジェクトの構成。フォルダー分けとinternal化の両方によって、ファサード以外のクラスがクライアントから隠蔽されている

まとめ

本レクチャーでは、前回のフォルダー整理だけでは解決していなかった「クラスが依然として外部から見えている」という問題に対して、アクセス修飾子をinternalに変更するという方法で対処しました。ポイントは次の3つです。

・クライアントに使ってほしいFacade(MachineFacade)だけをpublicのまま公開し、それ以外のサブシステムのクラス・メソッドはinternalにする

・Facade自身は同じアセンブリ内にあるため、internal化した後も内部クラスへの呼び出しはそのままコンパイルが通る

・publicなメソッドの引数・戻り値として使われる型(今回のFanEntity)は、クラスとしてはpublicのままにしておく必要がある。ただしそのプロパティまで公開する必要がなければ、プロパティ自体はinternalのままにできる

フォルダー構成の工夫(見た目の整理)とアクセス修飾子によるカプセル化(実際にアクセスできないようにする制御)を組み合わせることで、クライアントから見えるものを本当に必要なものだけに絞り込むことができました。これにより、サブシステムを使う側は「何を使えばいいのか迷う」ことなく、ファサード経由で安心して機能を呼び出せるようになります。

C#

■非公開コース「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デザインパターン ―頭とからだで覚えるデザインパターンの基本

ドメイン駆動開発
エリック・エヴァンスのドメイン駆動設計
実践ドメイン駆動設計
ドメイン駆動
現場で役立つシステム設計の原則
エンタープライズアプリケーションアーキテクチャパターン