メインコンテンツまでスキップ

インタフェースによる実装の隠蔽

公開サンプル

このページで説明する設計プラクティスを盛り込んだサンプルを GitHub で公開しています。

外部リンク: ArchitectureSample

実装の分離

インタフェースによる実装の隠蔽を徹底することで、ドメイン層のクラス実装は原則internal化できます。これにより設計変更に非常に強いアーキテクチャを実現できます。

  • サービスのインタフェースのみを公開し、実装クラスは非公開にします。
  • 利用する側はインタフェースだけでサービスを利用し、実装クラスを参照しないようにします。

実装クラスを変更しても、利用している側は一切影響を受けなくなります。 ただし、利用する側でサービスのインスタンスを取得する必要があるので、ファクトリを公開する必要があります。

このページではこの設計について説明します。

全体像

プロジェクトとクラス、インタフェースの関連は以下のようになります。

  • ArchitectureSample - プレゼンテーション層のプロジェクト
    • CreateUseCasesCommandクラス - サービスを利用します。
  • ArchitectureSample.Core - ドメイン層のプロジェクト
    • IUseCaseCreationServiceインタフェース - サービスのインタフェース定義を公開します。
    • UseCaseCreationServiceクラス - IUseCaseCreationServiceインタフェースを実装します。プロジェクト外に公開しません。
    • SampleServiceFactoryクラス - プロジェクト外からサービスのインスタンスを取得するためのファクトリです。

以下でそれぞれを詳しく説明します。

サービスの実装とインタフェースの公開

ドメイン層のプロジェクトで、以下のようなインタフェースを公開します。

IUseCaseCreationService.cs
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にして外部から参照できないようにします。

UseCaseCreationService.cs
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 クラスのインスタンスを作成できます。

SampleServiceFactory.cs
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インタフェースのインスタンスを取得して呼び出します。

CreateUseCasesCommand.cs
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;
}
}
}

この仕組みにすることでプレゼンテーション層はサービスの実装に依存しなくなります。サービスの実装クラスが変更されても、インタフェース定義が変わらない限りは何も変更する必要がなくなります。