C#でドメイン駆動開発 PR

C#でドメイン駆動設計UIとインフラストラクチャーを実装する方法⑧

記事内に商品プロモーションを含む場合があります

前回まででプロジェクトの作成とテストコードの作成Moqを使った外部接触部分のテストの方法を学びました。これからは後回しになっていたユーザーインターフェースと外部データとのアクセス部分であるインフラストラクチャーの部分の実装方法を解説していきます。

UIとインフラストラクチャー

次は,後回しにしていたUIとインフラストラクチャーの部分を作成しましょう。後回しになっていた部分はテストコードでカバーできていない部分ということになります。テストコードでカバーできていないということはコードはできるだけシンプルで少なく実装することが望ましいです。

UI部分の作成

それではまずUI部分を作成します。WinFormプロジェクトに新規フォルダ「Views」を作りましょう。その中に「MeasureView」という名前のFormを作成します。それとは別に「BaseForm」という名前のFormも作成しましょう。デザイン画面にはなにもおかずに画面のコードを表示します。コンストラクタに次のコードを入れておきます。

     public BaseForm()
     {
            InitializeComponent();
            StartPosition = FormStartPosition.CenterScreen;
     }

MeasureViewのデザイン画面を開き,ラベルとボタンを設置します。ラベルのNameは「MeasureValueLabel」,Fontサイズを36くらいにしましょう。ボタンはNameを「MeasureButton」,Textを「Measure」としておきます。

MeasureViewのコードを開き次のように書きます。

public partial class MeasureView : BaseForm
{
    public MeasureView()
    {
        InitializeComponent();
        var viewModel = new MeasureViewModel();
        MeasureValueLabel.DataBindings.Add("Text",viewModel, nameof(viewModel.MeasureValue));
        MeasureButton.Click += (sender, e) => viewModel.Measure();
    }
}

MeasureViewModelの生成ではコンパイルエラーになっていると思います。とりあえずおいておきます。

一行目は先ほど作成したBaseFormを継承しています。これで必ず中央に画面表示されるようになります。そのほかにも共通で動作させるようなものをBaseFormに書きます。

コンストラクタにはMeasureViewModelをNewして画面項目にデータバインディングしています。

nameofは少し前の.NETFrameworkだど使えないのでその場合は文字列にしても同様に動作します。

MeasureValueLabel.DataBindings.Add("Text",viewModel, “MeasureValue”);

ただこれだとプロパティ名が変更されてもコンパイルエラーにならないので,メンテナンスが少し大変になります。次の関数を作って代用してもよいでしょう。

public static class Util
{
    public static string NameOf<T>(Expression<Func<T>> e)
    {
        var member = (MemberExpression)e.Body;
        return member.Member.Name;
    }
}

Utilという静的クラスを作成し,Domain層のHelpersフォルダに入れておきます。MeasureViewからは次のように呼び出します。

MeasureValueLabel.DataBindings.Add("Text", viewModel, 
Util.NameOf(() => viewModel.MeasureValue));

これでnameofとほとんど同じ効果が得られます。さて,MeasureViewModel生成のコンパイルエラーを取り除きましょう。現在ViewModelのコンストラクタにはISensorRepositoryの指定を強制しています。

ただISensorRepositoryの具象クラスは存在しておらず,テストではMoqで代用しているだけです。

Infrastructureの実装

Infrastructure層にISensorRepositoryの具象クラスを作りましょう。といっても本当のセンサーに接続するクラスはまだ作りません。これから作るのはFakeといわれるテスト用の値を操作するダミークラスです。Infrastructureに新規フォルダ「Fake」を作ります。

その中に新規クラス「SensorFake」を作りましょう。

internal sealed class SensorFake : ISensorRepository
{
    private Random _random = new Random();
    public float GetData()
    {
        return _random.Next(0, 3) + _random.NextDouble().ToSingle();
    }
}

アクセス修飾子はInternalです。理由は後述します。ISensorRepositoryを実装しています。これによりfloat GetData()の実装が強制され,MeasureViewModelのprivate変数である_sensorRepositoryにはめ込むことができます。

GetDataの中身は適当なランダム値を返しています。ToSingle()の部分はDomain層のHelpersフォルダにExtensionsという静的クラスを作成し,次の拡張メソッドを追加しています。

public static float ToSingle(this double value)
{
    return Convert.ToSingle(value);
}

Convert.ToSingleと書けばよいのですが,コード量が増えて面倒なのでこうしています。それに文脈的にもvlaue.ToSingle()のほうが頭の構造上自然かと思います。

拡張メソッドはこんな風にピンポイントで便利コード的に定義しています。そしてどのような拡張メソッドがあるかがわかりやすいようにExtensionsという静的クラスにすべて入れるようにしています。といっても拡張メソッドはあまり大量にはならないと思います。大量になるようなら用途ごとにクラス分けも必要かもしれません。

これでISensorRepositoryの具象クラス「SensorFake」が完成しました。しかしアクセス修飾子がInternalなのでWinFormからは見えません。これをpublicにすればWinFormから呼び出せます。

すると次のようなコードになります。

【MeasureView】

var viewModel = new MeasureViewModel(new SensorFake());

これでよいでしょうか?これでは必ずFakeクラスが生成されてしまいます。将来的には「SensorDriver」というセンサーと接続する本番コードを作る想定です。そうなった場合どういうコードになるでしょうか?

ISensorRepository repository;
if(Configuration.IsFake)
{
     repository = new SensorFake();
}
else
{
     repository = new SensorDriver();
}

var viewModel = new MeasureViewModel(repository);

画面のコードに書かれていることを置いておいたとしてもこのコードはよくありません。なぜでしょうか?

この書き方だとクライアントコードに知識があります。

Badコードの紹介でも解説しましたが,クライアントコードは知識をもってはいけません。

なぜならクライアントコードが知識を持つと,確実にビジネスロジックが散らばり始めます。

今回の例ではConfiguration.IsFakeの時,つまりFakeモードの時にSensorFakeを生成するという決まり事です。ISensorRepositoryのインスタンス生成は各所で行われる可能性があります。そうなれば他の画面でも同じようにFakeにするかDriverにするかの判断が行われます。UIが判断していいのはUIの事柄だけです。「CheckBoxにチェックが入っているか?」などというUI事情の事柄だけです。UIの事柄はUIで判断するしかありません。しかしシステム全体の決め事はどこか一カ所で行わないとシステムは安定しません。

今はFakeモード,今は本番モードと,1カ所を変えるだけで,アプリケーション全体がその規則にのっとって動作し始めないといけません。

インスタンスの生成はファクトリークラスに任せます。ファクトリーを作るのはこういったインターフェースを実装しているクラスを特定の判断基準で分岐されるために使います。

Factoriesの作成

Infrastuctureの直下に「Factories」というクラスを作りましょう。

public static class Factories
{
    public static ISensorRepository CreateSensorRepository()
    {
        return new SensorFake();
    }
}

ISensorRepositoryのインスタンスの生成はCreateSensorRepositoryのみで行うと決めておきます。

このためにSensorFakeのアクセス修飾子はInternalにしています。WinFormからは決してNewされない,ファクトリーでしかNewできないようにするためです。

分岐が必要になったら,ファクトリーで分岐します。さてそれではCreateSensorRepositoryを呼び出しましょう。どこから呼び出しましょうか?

MeasureViewで次のようにしても呼び出せます。

var viewModel = new MeasureViewModel(Factories.CreateSensorRepository());

ただ私はMeasureViewModelに記述したほうがスマートに思います。

public MeasureViewModel():this(Factories.CreateSensor())
{
}

public MeasureViewModel(ISensorRepository sensorRepository)
{
    _sensorRepository = sensorRepository;
}

MeasureViewModelをなにも指定せず呼び出せばFactoriesによって適切なインスタンスが生成されます。これも使う側にあまり知識を持たせないという意味ではよいと思います。それでは実行してみましょう。ProgramクラスのMain()のなかはつぎのように変更しておいてください。

[STAThread]
static void Main()
{
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);
    Application.Run(new MeasureView());
}

それでは実行してみましょう。画面が開いたら測定値の初期値は「–」になっていると思います。

「Measure」ボタンを押下するとランダムな値が表示されるはずです。

Measureボタンを押下するとランダムな値が表示される

ドメイン駆動開発
Udemyで販売しているC#のコースを 1つプレゼントします!