Facadeパターン

デザインパターン【C#】:Facade 07_一連の手順があるロジックをファサードで吸収する

これまでのレクチャーでは、複雑なサブシステム(Machineプロジェクト)をMachineFacadeというひとつの窓口にまとめ、クライアント側はMachineFacade経由でしか各機能を使えないようにしてきました。今回は、その「窓口」であるFacadeに、単なる機能の中継役以上の役割を持たせます。すなわち、

「必ずこの順番で呼ばないといけない」「呼ぶ前にこのチェックが必要」といった、処理の手順そのものをFacadeの中に吸収してしまう、という使い方です。こうしておけば、Facadeを呼び出す全員が自動的に同じ手順・同じチェックを踏むことになり、使い方が統一されます。

チェックの手順をFacadeに集約する

まずはMachineFacadeにあるCameraTake()メソッドを例に見ていきます。変更前は、次のようにCameraクラスのTake()をそのまま呼び出すだけの実装になっています。

public static void CameraTake()
{
    new Camera().Take();
}

 

図1 変更前のCameraTake()。Boxの内部温度に関するチェックはまだ入っていない

ここに「Box内部の温度が70度を超えていたらカメラは使用できない(撮影前に必ずこのチェックを通す)」というルールを追加します。BoxInternalTemperature()を使って現在の内部温度を取得し、70度を超えていたら例外をthrowするようにします。

public static void CameraTake()
{
    if (BoxInternalTemperature() > 70)
    {
        throw new Exception("高温時はカメラを使用できません!!");
    }
 
    new Camera().Take();
}

 

図2 CameraTake()の先頭に温度チェックを追加した状態

この状態でクライアント側(Form1.cs)からMachineFacade.CameraTake()を呼び出しても、コード自体は今までと変わりません。

FanEntity entity = MachineFacade.FanSpin(4);
MachineFacade.CameraTake();

 

図3 クライアント側のコードは変更なし。呼び出し方はこれまで通り

ポイントは、Cameraクラス自体がinternalとして公開範囲を絞られている(前回のレクチャーで解説したカプセル化)ため、クライアント側からはCameraを直接newすることができず、必ずMachineFacade.CameraTake()を経由するしかない、という点です。つまりこのチェックは

「入れておけば絶対に通る」チェックとして機能します。誰かがうっかりチェックを忘れる、ということが構造的に起こり得ません。

このように、手順やチェックをFacadeの中に書いておくことで、呼び出し側の書き方に依存せず、使い方そのものを統一・強制できるというのが、Facadeパターンのもうひとつの効能です。

複数の処理をひとつの手順としてまとめる

チェックだけでなく、「複数の処理を決まった順番で組み合わせる」というのもFacadeに任せられます。例として、Box内部の温度を測定する際に、事前にファンを止めてから測る、という手順を新しいメソッドとして作成します。

まず、既存のBoxInternalTemperature()をコピーし、ファンを止めてから測るバージョンとしてBoxInternalTemperatureFunStop()という名前で用意します(メソッド名の末尾に「FunStop」を付けています)。

public static int BoxInternalTemperatureFunStop()
{
    FanStop(0);
 
    try
    {
        System.Threading.Thread.Sleep(30000);
        return new Box().GetInternalTemperature();
    }
    finally
    {
        FanStart(0);
    }
}

 

図4 BoxInternalTemperatureFunStop()の完成形。ファンを止めて30秒待ち、温度取得後は必ずファンを再開する

FanStop(0)でファン(fanIdは0番=全ファン扱い)を止めた後、System.Threading.Thread.Sleepで30秒待ってから温度を取得しています。ここで重要なのはtry/finallyの構造です。途中で例外が起きた場合でも、finally内のFanStart(0)は必ず実行されるため、「ファンを止めたまま戻ってこない」という事故を防いでいます。

このように、複数ステップからなる一連の処理を1つのメソッドにまとめてFacade側に持たせておけば、クライアント側は複雑な手順を意識することなく、メソッドを1回呼ぶだけで正しい手順を踏めるようになります。

図5 クライアント側でMachineFacade.と入力すると、BoxInternalTemperatureとBoxInternalTemperatureFunStopがまとまって候補に表示される

似た系統のメソッドの命名規則

同じBoxInternalTemperatureの「仲間」となるメソッドは今後も増える可能性があります。例えば、「毎回30秒待つのは面倒なので、前回ファンを止めて測定した際の値をメモリ上に保持しておき、それを返すだけのメソッド」も欲しくなったとします。まずは仮の実装として、次のようなメソッドを追加してみます。

public static int BoxInternalTemperatureInMemory()
{
    return 0;
}

 

図6 3つ目のバリエーションとして追加したメソッドの仮実装(この時点ではまだ固定値0を返すだけ)

最終的には、FanStop時に測定した値を_fanStopValueというフィールドに保持しておき、それを返す実装にします。ここで注目したいのは、

メソッド名の付け方

です。動画内では、いったん名前の前側(プレフィックス)に修飾語を付けるパターンを試しています。

public static int InMemoryBoxInternalTemperature()
{
    return _fanStopValue;
}

 

しかし、これをクライアント側でMachineFacade.と入力してIntelliSenseの候補を見てみると、アルファベット順に並ぶ都合上、本来「仲間」であるはずのBoxInternalTemperature系のメソッドから離れた場所(FanStart/FanStopの近く)に表示されてしまい、関連性が一目でわからなくなってしまいます。

図7 名前の前側に修飾語を付けてしまうと、IntelliSenseの候補一覧でBoxInternalTemperature系から離れた位置(FunStopBoxInternalTemperature/InMemoryBoxInternalTemperatureなど)に表示されてしまう

そこで、修飾語は名前の

後ろ(サフィックス)

に付けるように統一します。ベースとなるBoxInternalTemperatureに対して、FunStopを付けた版、さらにInMemoryを付けた版、という形で末尾に積み重ねていきます。

private static int _fanStopValue;
 
public static int BoxInternalTemperatureFunStop()
{
    FanStop(0);
 
    try
    {
        System.Threading.Thread.Sleep(30000);
        var result = new Box().GetInternalTemperature();
        _fanStopValue = result;
        return result;
    }
    finally
    {
        FanStart(0);
    }
}
 
public static int BoxInternalTemperatureFunStopInMemory()
{
    return _fanStopValue;
}

 

図8 最終的な命名。ベース名BoxInternalTemperatureの末尾に用途を表す語(FunStop、FunStopInMemory)を積み重ねる形にリネームした

この状態で改めてクライアント側からMachineFacade.と入力すると、BoxInternalTemperature/BoxInternalTemperatureFunStop/BoxInternalTemperatureFunStopInMemoryの3つがIntelliSenseの候補一覧上できれいに並んで表示されるようになります。

図9 サフィックス方式で命名し直した結果、同系統の3メソッドがIntelliSense上で隣り合って表示されるようになった

最近のIntelliSenseは検索性が高く、前方に修飾語を付けても目的のメソッドを見つけること自体は難しくありません。ですが、一覧の並び順という観点では、ベースとなる名前を先頭に置き、バリエーションを表す語を後ろに続けていく方が、「これは同じ系統の類似メソッドである」ということが一目で伝わりやすくなります。

まとめ

今回は、Facadeパターンの役割として次の2点を紹介しました。

・ 「必ずこの順番で使ってほしい」「使う前にこのチェックが必要」といった手順やチェックをFacade側にまとめておくことで、呼び出し側の書き方によらず、使い方を統一・強制できる。

・ 同じ対象に対する複数のバリエーションのメソッドを用意する場合は、ベースとなる名前を先頭に置き、用途を表す語を末尾(サフィックス)に積み重ねていくと、IntelliSenseの候補一覧上でも関連するメソッドがまとまって見やすくなる。

Facadeは単なる「窓口」にとどまらず、こうした運用ルールごと吸収してしまえる場所でもある、というのが今回のポイントです。

C#文法:コレクション

■非公開コース「C#14新機能」プレゼント:
非公開コース「C#14新機能」(80分)をご覧になりたい方は
こちらからURLとパスワードを発行していますので、ご覧になってみてください。
非公開コース「C#14新機能」を観る

A01_はじめに
A02_プロジェクトの作成
B01_配列とは
B02_配列の生成とアクセス
B03_生成と同時に値を設定する
B04_型推論による生成
B05_メソッドの引数などにする場合の注意点
B06_Length
B07_IndexOfでの検索
B08_FindIndexでの検索
B09_Find
B10_Exists
B11_FindAllとFindLast
B12_誤ったコピー
B13_Array.Copy
B14_範囲指定のコピー
B15_Resize
C01A_ArrayList
C02_List
C03_List 動的な要素の変更
C04_Listのコンストラクタ
C05_Listのコンストラクタ_Capacity
C06_ListTからArrayクラスのメソッドが呼ばれている
D01_コレクションインタフェースとは
D02_異なるコレクションクラスに互換性を持たせる
D03_インタフェースの階層構造
D04_クライアントができる操作を制限させる
D05_クライアントができる操作を制限させる_後半
D06_Enumerableの拡張メソッドに関して
D07_ReadOnly系のコレクションインタフェース
D08_AsReadOnly
D09_ToListでコピーする
D10_ListTはprivateで使う

■非公開コース「C#14新機能」プレゼント:
非公開コース「C#14新機能」(80分)をご覧になりたい方は
こちらからURLとパスワードを発行していますので、ご覧になってみてください。
非公開コース「C#14新機能」を観る