Hiding the implementation with an interface
Public sample
We have published a sample on GitHub that incorporates the design practices described on this page.
External link: ArchitectureSample
Implementation isolation
By thoroughly hiding the implementation by the interface, the class implementation of the domain layer can be internalized in principle. This makes it possible to realize an architecture that is extremely resistant to design changes.
- Only expose the interface of the service and keep the implementation class private.
- The user side should use the service only with the interface and should not refer to the implementation class.
Even if you change the implementation class, the side using it will not be affected at all. However, the consumer needs to get an instance of the service, so we need to expose the factory.
This page describes this design.
Overall picture
The relationships between projects, classes, and interfaces are as follows.
- ArchitectureSample - presentation layer project
- CreateUseCasesCommand class - use the service.
- Architecture.Core - Domain layer project
- IUseCaseCreationService interface - exposes the service's interface definition.
- UseCaseCreationService class - Implements the IUseCaseCreationService interface. It is not published outside the project.
- SampleServiceFactory class - A factory for retrieving service instances from outside the project.
Each is detailed below.
Service implementation and interface exposure
In the domain layer project, expose the following interface.
using System.Collections.Generic;
using NextDesign.Core;
namespace ArchitectureSample.Core.Services
{
/// <summary>
/// Interface definition for the service that creates the use case
/// </summary>
public interface IUseCaseCreationService
{
/// <summary>
/// create a use case
/// </summary>
/// <param name="owner">Model to own the created use case</param>
/// <param name="names">Name to give to the use case to create</param>
/// <returns>Collection of use cases created</returns>
IEnumerable<IModel> CreateUseCases(IModel owner, IEnumerable<string> names);
}
}
A class that implements an interface should have internal accessibility so that it cannot be seen externally.
using NextDesign.Core;
namespace ArchitectureSample.Core.Services.Impl
{
/// <summary>
/// Implementation of the service that creates the use case
/// </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;
}
}
}
Expose the service factory
To use the service from outside, you must be able to get an instance. Provide a factory that can get instances that implement the interface.
This class is in the ArchitectureSample.Core project, so
You can create an instance of the UseCaseCreationService
class whose accessibility is internal.
using System;
using System.Collections.Generic;
using ArchitectureSample.Core.Services;
using ArchitectureSample.Core.Services.Impl;
namespace ArchitectureSample.Core
{
/// <summary>
/// Service factory implementation.
/// </summary>
public static class SampleServiceFactory
{
/// <summary>
/// Implementation class for service interface
/// </summary>
private static IDictionary<Type, Type> m_Types = new Dictionary<Type, Type>();
/// <summary>
/// register the service
/// </summary>
/// <typeparam name="I">Interface</typeparam>
/// <typeparam name="T">implementation class type</typeparam>
public static void Register<I, T>()
{
m_Types.Add(typeof(I), typeof(T));
}
/// <summary>
/// register the default service
/// </summary>
public static void InitializeDefaults()
{
Register<IUseCaseCreationService, UseCaseCreationService>();
}
/// <summary>
/// Get an instance by requesting the service interface. Raise an exception if it does not exist.
/// </summary>
/// <typeparam name="I">request interface</typeparam>
/// <returns>service instance</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} is not supported");
}
var service = Activator.CreateInstance(type) as I;
return service;
}
}
}
Presentation layer (consumer side)
The presentation layer cannot reference the UseCaseCreationService
class.
Get an instance of the IUseCaseCreationService
interface on the exposed factory and call it.
using ArchitectureSample.Core;
using ArchitectureSample.Core.Services;
using NextDesign.Desktop;
using NextDesign.Desktop.ExtensionPoints;
namespace ArchitectureSample.Commands
{
/// <summary>
/// create a use case
/// </summary>
public class CreateUseCasesCommand : CommandHandlerBase
{
/// <summary>
/// execute command
/// </summary>
/// <param name="c"></param>
/// <param name="p"></param>
protected override void OnExecute(ICommandContext c, ICommandParams p)
{
// get the service from the factory
var service = SampleServiceFactory.Get<IUseCaseCreationService>();
// call the service
service.CreateUseCases(CurrentModel, new[] { "UseCase1", "UseCase2", "UseCase2" });
UI.ShowMessageBox("Use case created.", ExtensionName);
}
/// <summary>
/// Implementation of command execution permission (optional)
/// </summary>
/// <returns></returns>
protected override bool OnCanExecute()
{
return true;
}
}
}
By using this mechanism, the presentation layer does not depend on the implementation of the service. Even if the implementation class of the service changes, nothing needs to be changed as long as the interface definition remains the same.