インタフェースによる実装の隠蔽
公開サンプル
このページで説明する設計プラクティスを盛り込んだサンプルを GitHub で公開しています。
外部リンク: ArchitectureSample
実装の分離
インタフェースによる実装の隠蔽を徹底することで、ドメイン層のクラス実装は原則internal化できます。これにより設計変更に非常に強いアーキテクチャを実現できます。
- サービスのインタフェースのみを公開し、実装クラスは非公開にします。
- 利用する側はインタフェースだけでサービスを利用し、実装クラスを参照しないようにします。
実装クラスを変更しても、利用している側は一切影響を受けなくなります。 ただし、利用する側でサービスのインスタンスを取得する必要があるので、ファクトリを公開する必要があります。
このページではこの設計について説明します。
全体像
プロジェクトとクラス、インタフェースの関連は以下のようになります。
- ArchitectureSample - プレゼンテーション層のプロジェクト
- CreateUseCasesCommandクラス - サービスを利用します。
- ArchitectureSample.Core - ドメイン層のプロジェクト
- IUseCaseCreationServiceインタフェース - サービスのインタフ ェース定義を公開します。
- UseCaseCreationServiceクラス - IUseCaseCreationServiceインタフェースを実装します。プロジェクト外に公開しません。
- SampleServiceFactoryクラス - プロジェクト外からサービスのインスタンスを取得するためのファクトリです。
以下でそれぞれを詳しく説明します。
サービスの実装とインタフェースの公開
ドメイン層のプロジェクトで、以下のようなインタフェースを公開します。
using System.Collections.Generic;
using NextDesign.Core;
namespace ArchitectureSample.Core.Services
{
/// <summary>
/// ユースケースを作成するサービスのインタフェース定義
/// </summary>
public interface IUseCaseCreationService
{
/// <summary>
/// ユースケースを作成する
/// </summary>
/// <param name="owner">作成したユースケースを所有させるモデル</param>
/// <param name="names">作成するユースケースにつける名前</param>
/// <returns>作成したユースケースのコレクション</returns>
IEnumerable<IModel> CreateUseCases(IModel owner, IEnumerable<string> names);
}
}
インタフェースを実装するクラスはアクセシビリティをinternalにして外部から参照できないようにします。
using System.Collections.Generic;
using NextDesign.Core;
namespace ArchitectureSample.Core.Services.Impl
{
/// <summary>
/// ユースケースを作成するサービスの実装
/// </summary>
internal class UseCaseCreationService : IUseCaseCreationService
{
/// <inheritdoc/>
public IEnumerable<IModel> CreateUseCases(IModel owner, IEnumerable<string> names)
{
var createdModels = new List<IModel>();
foreach (var name in names)
{
var model = owner.AddNewModel("UseCases", "UseCase");
model.SetField("Name", name);
createdModels.Add(model);
}
return createdModels;
}
}
}
サービスのファクトリを公開
外部からサービスを利用するにはインスタンスを取得できなければなりま せん。 インタフェースを実装するインスタンスを取得できるファクトリを用意します。
このクラスは ArchitectureSample.Core プロジェクトにあるので、
アクセシビリティがinternalな UseCaseCreationService
クラスのインスタンスを作成できます。
using System;
using System.Collections.Generic;
using ArchitectureSample.Core.Services;
using ArchitectureSample.Core.Services.Impl;
namespace ArchitectureSample.Core
{
/// <summary>
/// サービスファクトリの実装です。
/// </summary>
public static class SampleServiceFactory
{
/// <summary>
/// サービスのインタフェースに対する実装クラス
/// </summary>
private static IDictionary<Type, Type> m_Types = new Dictionary<Type, Type>();
/// <summary>
/// サービスを登録します
/// </summary>
/// <typeparam name="I">インタフェース</typeparam>
/// <typeparam name="T">実装クラスの型</typeparam>
public static void Register<I, T>()
{
m_Types.Add(typeof(I), typeof(T));
}
/// <summary>
/// デフォルトのサービスを登録します
/// </summary>
public static void InitializeDefaults()
{
Register<IUseCaseCreationService, UseCaseCreationService>();
}
/// <summary>
/// サービスインタフェースを要求してインスタンスを取得します。存在しない場合は例外を送出します。
/// </summary>
/// <typeparam name="I">要求インタフェース</typeparam>
/// <returns>サービスインスタンス</returns>
/// <exception cref="ArgumentException"></exception>
public static I Get<I>() where I : class
{
if (!m_Types.TryGetValue(typeof(I), out var type))
{
throw new ArgumentException($"{typeof(I).Name}は対応していません。");
}
var service = Activator.CreateInstance(type) as I;
return service;
}
}
}
プレゼンテーション層(利用側)
プレゼンテーション層では UseCaseCreationService
クラスを参照できません。
公開されているファクトリで IUseCaseCreationService
インタフェースのインスタンスを取得して呼び出します。
using ArchitectureSample.Core;
using ArchitectureSample.Core.Services;
using NextDesign.Desktop;
using NextDesign.Desktop.ExtensionPoints;
namespace ArchitectureSample.Commands
{
/// <summary>
/// ユースケースを作成します
/// </summary>
public class CreateUseCasesCommand : CommandHandlerBase
{
/// <summary>
/// コマンドの実行
/// </summary>
/// <param name="c"></param>
/// <param name="p"></param>
protected override void OnExecute(ICommandContext c, ICommandParams p)
{
// ファクトリからサービスを取得します
var service = SampleServiceFactory.Get<IUseCaseCreationService>();
// サービスを呼び出します
service.CreateUseCases(CurrentModel, new[] { "ユースケース1", "ユースケース2", "ユースケース2" });
UI.ShowMessageBox("ユースケースを作成しました。", ExtensionName);
}
/// <summary>
/// コマンド実行可否の実装(任意です)
/// </summary>
/// <returns></returns>
protected override bool OnCanExecute()
{
return true;
}
}
}
この仕組みにすることでプレゼンテーション層はサービスの実装に依存しなくなります。サービスの実装クラスが変更されても、インタフェース定義が変わらない限りは何も変更する必要がなくなります。