Click here to Skip to main content
15,881,872 members
Articles / Programming Languages / C#

The Clifton Method - Part II

Rate me:
Please Sign up or sign in to vote.
5.00/5 (7 votes)
25 Aug 2016CPOL11 min read 10.7K   128   5  
Service Manager - Instantiation by Interface Specification
This is Part 2 of a series of articles about a Clifton Method Core Component in which you will learn about Service Manager, and how to instantiate by Interface specification.

Series of Articles

Introduction

This article builds on what I call "The Clifton Method" of software development. The first article in this series is The Module Manager, and while this code in this article stands on its own, it is ultimately intended to work in conjunction with the Module Manager.

The concept of instantiating concrete instances via an interface specification should be a well known pattern of the dependency inversion principle. As that Wikipedia link summarizes:

In object-oriented programming, the dependency inversion principle refers to a specific form of decoupling software modules. When following this principle, the conventional dependency relationships established from high-level, policy-setting modules to low-level, dependency modules are reversed, thus rendering high-level modules independent of the low-level module implementation details. The principle states:

  • High-level modules should not depend on low-level modules. Both should depend on abstractions.
  • Abstractions should not depend on details. Details should depend on abstractions.

What Problem Does DIP Solve?

Similar to the Module Manager, the Service Manager provides the ability to decouple dependencies between objects. An "dependency entangled" application instantiates classes directly. One can come up with many examples of dependency entanglement:

Image 1

In these situations, the dependency between the high level component and the low level component is hard-wired in the implementation. Should the requirements change, the code requires considerable rework to either replace or abstract out the low level dependencies. The problem gets more complicated when the low level components themselves interface to higher level components, for example, when user interface events, handled by a controller, require database I/O that can affect the user interface:

Image 2

The Dependency Inversion Principle decouples the dependencies such that both high level and low level components can be changed without breaking the code. This results in an implementation that looks a bit more like this:

Image 3

Image 4

In this diagram, both high and low level components have been abstracted such that the application (not shown) can be implemented using interfaces, rather than concrete classes, achieving a high level of decoupling between "what the application wants to do" vs. "how the component does it."

Is the Extra Work Worth It?

The above diagram illustrates that more work (sometimes considerably more work) is required on the part of the programmer to create the interfaces, specify the interface behaviors, and in many cases, write wrappers that implement the interface behaviors. For example, a more accurate picture of the UI abstraction might look like this:

Image 5

The balance the architect (not the developer, unless they are the same person) must achieve between entanglement and abstraction is often determined by questions like:

  • What components am I confident that I know I'm going to stick with?
  • What components will need to vary in concrete implementation, and not just as determined by the current requirements but by unforeseen future changes?

Image 6 Unfortunately, the answers to both questions often requires a crystal ball!

Image 7 That said, when a happy medium is determined, the resulting application is more robust and amenable to change, typically resulting in a longer lifetime, and future changes are less costly to implement. One of the immediate uses of this approach is that services can be easily mocked, which facilitates unit testing and application development when either the service hasn't yet been implemented or, for example, is associated with hardware that you may not have available.

The Service Manager

The Service Manager is a lightweight implementation of the Dependency Inversion Principle. In a nutshell, components, whether high or low level, are instantiated by associating the concrete implementation with an interface.

Singleton Instantiation

Here's a minimal example:

C#
using System;

using Clifton.Core.ServiceManagement;

namespace ServiceManagerDemo
{
  public interface IAnimal : IService
  {
    void Speak();
  }

  public interface ICat : IAnimal { }
  public interface IDog : IAnimal { }

  public class Cat : ServiceBase, ICat
  {
    public void Speak()
    {
      Console.WriteLine("Meow");
    }
  }

  public class Dog : ServiceBase, IDog
  {
    public void Speak()
    {
      Console.WriteLine("Woof");
    }
  }

  class Program
  {
    static void Main(string[] args)
    {
      ServiceManager svcMgr = new ServiceManager();
      svcMgr.RegisterSingleton<ICat, Cat>();
      svcMgr.RegisterSingleton<IDog, Dog>();
      IAnimal animal1 = svcMgr.Get<ICat>();
      IAnimal animal2 = svcMgr.Get<IDog>();
      animal1.Speak();
      animal2.Speak();
    }
  }
}

Image 8

In this example, observe how:

  • IAnimal is the effectively the pure abstract specification for anything that derives from it.
  • The "concrete" interfaces, ICat and IDog, are derived from IAnimal but do not contain any specific implementation.
  • The concrete implementation of Cat and Dog derives from ICat and IDog, respectively, and implements the IAnimal behavior.
  • In this example, we tell the Service Manager that the "services" Cat and Dog are singletons--they are instantiated only once and subsequent Get calls return the one and only instance.

Image 9

The interface IService and the abstract class ServiceBase are provided by the Service Manager will be discussed later.

Image 10 What's interesting about this architecture is that the interfaces themselves are abstracted:

  1. A common interface IAnimal describes the behavior of the abstracted concept.

  2. Empty child interfaces deriving from IAnimal provide the means for mapping the "concrete" interface with a "concrete" implementor, thus determining which "animal" is to be instantiated.

  3. Typically, "concrete" interfaces do not have further behavior specification, though there may be reasons why this is useful / necessary.

Image 11 When registering a singleton, it is actually instantiated immediately by the service manager.

Non-Singleton Instantiation

Services are often singletons, but one can instantiate more than one instance of a service as well. In this example, we'll look at created multiple instances of Cat:

C#
using System;

using Clifton.Core.ServiceManagement;

namespace ServiceManagerInstanceDemo
{
  public interface IAnimal : IService
  {
    string Name { get; set; }
    void Speak();
  }

  public interface ICat : IAnimal { }

  public abstract class Animal : ServiceBase
  {
    public string Name { get; set; }
    public abstract void Speak();
  }

  public class Cat : Animal, ICat
  {
    public override void Speak()
    {
      Console.WriteLine(Name + " says 'Meow'");
    }
  }

  public static class InstanceDemo
  {
    public static void Demo()
    {
      ServiceManager svcMgr = new ServiceManager();
      svcMgr.RegisterInstanceOnly<ICat, Cat>();
      IAnimal cat1 = svcMgr.Get<ICat>();
      cat1.Name = "Fido";
      IAnimal cat2 = svcMgr.Get<ICat>();
      cat2.Name = "Morris";
      cat1.Speak();
      cat2.Speak();
    }
  }
}

Image 12

Image 13

Notice how:

  1. The service is registered with RegisterInstanceOnly instead of RegisterSingleton.
  2. An abstract class implements the Name property but specifies that Speak is still abstract, as the Name property is common to all animals now.

We can also specify a function in the Get method to assign values to properties as part of the Get call, for example:

C#
IAnimal cat2 = svcMgr.Get<ICat>(cat => cat.Name = "Morris");

Exclusive Services

Frequently, a service is exclusive of all other implementers. The application may, for example, require only one database service. Different installations may require connecting to a different database, but the scenario where the application connects to two different database services at the same time doesn't occur. This creates a simpler scenario for the interface implementation -- in the examples that I've been using, the "Animal" service, if viewed as an exclusive service, is implemented either by Cat or Dog, and we never need to implement both simultaneously:

C#
using System;

using Clifton.Core.ServiceManagement;

namespace ServiceManagerExclusiveDemo
{
  public interface IAnimal : IService
  {
    void Speak();
  }

  public class Cat : ServiceBase, IAnimal
  {
    public void Speak()
    {
      Console.WriteLine("Meow");
    }
  }

  public static class ExclusiveDemo
  {
    public static void Demo()
    {
      ServiceManager svcMgr = new ServiceManager();
      svcMgr.RegisterSingleton<IAnimal, Cat>();
      IAnimal animal = svcMgr.Get<IAnimal>();
      animal.Speak();
    }
  }
}

As the above code illustrates, the implementation is simpler.

Getting a Service Determined at Runtime

The Service Manager is not a dependency injection framework (DIF) -- you will note that the singleton and instance examples above, the code still needs to reference the concrete interface that maps to the concrete implementation:

C#
IAnimal animal1 = svcMgr.Get<ICat>();
IAnimal animal2 = svcMgr.Get<IDog>();

Here, the code is still specifying the concrete interface. Typically, you will want to instantiate the class from the concrete interface, but there are may be situations in which the concrete interface is not known at compile time, instead it is determined at runtime. This can be handled by a DIF which injects the concrete instance into a property at runtime, but in my opinion, that adds a lot of unnecessary kruft.

This problem can be solved by specifying the type, not as a generic parameter, but rather as an actual Type parameter that you acquire from somewhere else (configuration file, etc). Here's a rather simplified example in which we first assume some process is mapping concrete interfaces with concrete implementers.

C#
public static void RegisterServices()
{
  svcMgr = new ServiceManager();
  svcMgr.RegisterSingleton<ICat, Cat>();
  svcMgr.RegisterSingleton<IDog, Dog>();
}

Let's say we want to call an operation that is going to use the IAnimal service, but it doesn't know what concrete interface to use. We have two options. We can make the call like this (here IDog and ICat are implemented in my demo namespace):

C#
ByTypeDemo.ByTypeParameter(typeof(ServiceManagerByTypeDemo.ICat));
ByTypeDemo.ByTypeParameter(typeof(ServiceManagerByTypeDemo.IDog));

and implement the method like this (please note that the use of static is only for the convenience of the demo):

C#
public static void ByTypeParameter(Type someAnimal)
{
  IAnimal animal = svcMgr.Get<IAnimal>(someAnimal);
  animal.Speak();
}

This however has the disadvantage of losing type consistency -- we know longer guarantee that someAnimal implements IAnimal, which means that if it doesn't, we'll get a runtime error.

A better call and implementation would look like this:

C#
public static void ByGenericParameter<T>() where T : IAnimal
{
  IAnimal animal = svcMgr.Get<T>();
  animal.Speak();
}

The second approach is much better because it enforces at compile time the type of the generic parameter. The implementer needs to only know the generic interface IAnimal, not the concrete interfaces ICat and IDog, but none-the-less, it is guaranteed that the generic parameter is of type IAnimal.

Implementation Details

Image 14 The Service Manager is itself implemented as a service:

C#
public class ServiceManager : ServiceBase, IServiceManager

This means that, in conjunction with the Module Manager, the Service Manager can be loaded as a service, and that the service provided by the Service Manager is itself abstracted and replaceable (as long as you implement IServiceManager!)

Thread Safety

The Service Manager is intended to be thread safe, so the three pieces of information that it manages use ConcurrentDictionary:

C#
protected ConcurrentDictionary<Type, Type> interfaceServiceMap;
protected ConcurrentDictionary<Type, IService> singletons;
protected ConcurrentDictionary<Type, ConstructionOption> constructionOption;

The only time a lock is required is when creating or returning a previously created singleton -- we need the lock here to prevent two threads from simultaneously either attempting to create a singleton, or one thread creating the singleton while another thread is attempted to acquire the singleton:

C#
/// <summary>
/// Return a registered singleton or create it and register it if it isn't registered.
/// </summary>
protected IService CreateOrGetSingleton<T>(Action<T> initializer)
where T : IService
{
  Type t = typeof(T);
  IService instance;

  lock (locker)
  {
    if (!singletons.TryGetValue(t, out instance))
    {
      instance = CreateAndRegisterSingleton<T>(initializer);
    }
  }

  return instance;
}

Instance Registration

Instance registration involves adding an entry to the interface service map and preserving the fact that the type is intended to be an instance:

C#
public virtual void RegisterInstanceOnly<I, S>()
where I : IService
where S : IService
{
  Type interfaceType = typeof(I);
  Type serviceType = typeof(S);
  Assert.Not(interfaceServiceMap.ContainsKey(interfaceType), 
             "The service " + GetName<S>() + " has already been registered.");
  interfaceServiceMap[interfaceType] = serviceType;
  constructionOption[interfaceType] = ConstructionOption.AlwaysInstance;
}

Singleton Registration

Singleton registration involves adding an entry to the interface service map and instantiating the singleton:

C#
public virtual void RegisterSingleton<I, S>(Action<I> initializer = null)
where I : IService
where S : IService
{
  Type interfaceType = typeof(I);
  Type serviceType = typeof(S);
  Assert.Not(interfaceServiceMap.ContainsKey(interfaceType), 
             "The service " + GetName<S>() + " has already been registered.");
  interfaceServiceMap[interfaceType] = serviceType;
  constructionOption[interfaceType] = ConstructionOption.AlwaysSingleton;
  RegisterSingletonBaseInterfaces(interfaceType, serviceType);

  // Singletons are always instantiated immediately so that they can be initialized
  // for global behaviors. A good example is the global exception handler services.
  CreateAndRegisterSingleton<I>(initializer);
}

Creation and Registration

For both singletons and instance services, the instance, when created, is created, registered, and an initialization method is called (discussed below):

C#
protected virtual IService CreateAndRegisterSingleton<T>(Action<T> initializer = null)
  where T : IService
{
  IService instance = CreateInstance<T>(initializer);
  Register<T>(instance);
  instance.Initialize(this);

  return instance;
}

protected IService CreateInstance<T>(Action<T> initializer)
where T : IService
{
  Type t = typeof(T);
  IService instance = (IService)Activator.CreateInstance(interfaceServiceMap[t]);
  initializer.IfNotNull((i) => i((T)instance));

  return instance;
}

If we were using C# 6.0, we could replace the IfNotNull extension method with:

C#
initializer?.((T)instance));

but I haven't upgraded the code base to C# 6.0 yet!

Getting the Service Instance

When we request a service, the Service Manager checks whether we want, based on the registration, a singleton or an instance:

C#
public virtual T Get<T>(Action<T> initializer = null)
  where T : IService
{
  IService instance = null;
  VerifyRegistered<T>();
  Type interfaceType = typeof(T);

  switch (constructionOption[interfaceType])
  {
    case ConstructionOption.AlwaysInstance:
    instance = CreateInstance<T>(initializer);
    instance.Initialize(this);
    break;

    case ConstructionOption.AlwaysSingleton:
    instance = CreateOrGetSingleton<T>(initializer);
    break;

    default:
      throw new ApplicationException("Cannot determine whether the service " + 
      GetName<T>() + " should be created as a unique instance or as a singleton.");
  }

  return (T)instance;
}

Getting an Instance or a Singleton, Depending on Application Requirements

If we didn't register the service ahead of time as a singleton or instance service, we have to explicitly state what type of instance we want by calling either of these methods:

C#
/// <summary>
/// If allowed, returns a new instance of the service implement interface T.
/// </summary>
public virtual T GetInstance<T>(Action<T> initializer = null)
  where T : IService
{
  VerifyRegistered<T>();
  VerifyInstanceOption<T>();
  IService instance = CreateInstance<T>(initializer);
  instance.Initialize(this);

  return (T)instance;
}

/// <summary>
/// If allowed, creates and registers or 
/// returns an existing service that implements interface T.
/// </summary>
public virtual T GetSingleton<T>(Action<T> initializer = null)
  where T : IService
{
  VerifyRegistered<T>();
  VerifySingletonOption<T>();
  IService instance = CreateOrGetSingleton<T>(initializer);

  return (T)instance;
}

This mechanism allows us to acquire either a singleton or an instance, as the application requires. The Service Manager allows us to have a singleton as well as multiple instances, but this is an unusual case, and typically, we specify the intended usage at registration so that the Service Manager can assert that the application is using the service as intended.

Exclusive Services

Even if a service implements a concrete interface derived from the abstract interface, the "magic" of being able to use the abstract interface is handled by walking the interface hierarchy and registering all interfaces in the hierarchy.

C#
/// <summary>
/// Singletons are allowed to also register their base type 
/// so that applications can reference singleton services by the common type
/// rather than their instance specific interface type.
/// </summary>
protected virtual void RegisterSingletonBaseInterfaces(Type interfaceType, Type serviceType)
{
  Type[] itypes = interfaceType.GetInterfaces();

  foreach (Type itype in itypes)
  {
    interfaceServiceMap[itype] = serviceType;
    constructionOption[itype] = ConstructionOption.AlwaysSingleton;
  }
}

This is a bit dangerous because no assertion is made that there is one and only one generic interface associated with the exclusive service. To do this, we might resort to using an attribute on the generic interface to indicate that any derived concrete interfaces are exclusive, but I haven't implemented that.

IService

All services must be derived from IService, which provides compile-time validation that the service is being registered and instances of the service are acquired correctly.

C#
namespace Clifton.Core.ServiceManagement
{
  public interface IService
  {
    IServiceManager ServiceManager { get; }
    void Initialize(IServiceManager srvMgr);
    void FinishedInitialization();
  }
}

A service must either implement the ServiceManager property and the two methods above, or derive from ServiceBase which provides a default implementation:

C#
namespace Clifton.Core.ServiceManagement
{
  /// <summary>
  /// A useful base class for a default implementation of IService methods.
  /// </summary>
  public abstract class ServiceBase : IService
  {
    public IServiceManager ServiceManager { get; set; }

    public virtual void Initialize(IServiceManager svcMgr)
    {
      ServiceManager = svcMgr;
    }

    public virtual void FinishedInitialization()
    {
    }
  }
}

Why is this necessary or useful, especially since the Service Manager itself never calls these methods?

  1. In my approach, a service is typically used in conjunction with the Module Manager discussed in the previous article. Services are implemented as modules, and a module can implement (though not usually) more than one service. Therefore, it needs the ability to register services, which requires an instance of the Service Manager.
  2. Services cannot be used until all the services have been registered. This is somewhat different from a dependency injection framework (DIF), where all the services are injected before the application "runs." Because my intention is to avoid the additional complexity and often the debugging difficulty of a DIF, I prefer a more "to the metal" approach. This means that registration is a two step process:
    1. Initialize the Service Manager instance in each module, allowing the modules to register their services.
    2. Calling FinishedInitialization, which tells the module that all services have been registered, and it can now do any final initialization with services on which it depends.

We will see in the next article how the Module Manager and Service Manager work together.

Conclusion

Using a service manager is one of those essential tools in the programmer's toolbox for implementing the dependency inversion principle. Applications implemented using a service manager are well on their way toward adhering to the SOLID principles of object oriented programming:

  • Single responsibility principle: a class should have only a single responsibility (i.e., only one potential change in the software's specification should be able to affect the specification of the class)
  • Open/closed principle: “software entities … should be open for extension, but closed for modification.”
  • Liskov substitution principle: “objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.”
  • Interface segregation principle: “many client-specific interfaces are better than one general-purpose interface.”
  • Dependency inversion principle: one should “Depend upon Abstractions. Do not depend upon concretions.”

The Service Manager presented here begins to create a firm footing in the last three principles (L, I, and D.) Using the code presented here and its use of generic parameters, or a similar implementation of your own can provide compile-time type checking while decoupling concrete implementations, resulting in a flexible application exhibiting good independence of both high level and low level objects.

History

  • 25th August, 2016: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect Interacx
United States United States
Blog: https://marcclifton.wordpress.com/
Home Page: http://www.marcclifton.com
Research: http://www.higherorderprogramming.com/
GitHub: https://github.com/cliftonm

All my life I have been passionate about architecture / software design, as this is the cornerstone to a maintainable and extensible application. As such, I have enjoyed exploring some crazy ideas and discovering that they are not so crazy after all. I also love writing about my ideas and seeing the community response. As a consultant, I've enjoyed working in a wide range of industries such as aerospace, boatyard management, remote sensing, emergency services / data management, and casino operations. I've done a variety of pro-bono work non-profit organizations related to nature conservancy, drug recovery and women's health.

Comments and Discussions

 
-- There are no messages in this forum --