Eritey
Eritey

Reputation: 143

Is this an incorrect use of interfaces?

Take the following example, I have multiple 'Integration' classes that inherit from the same 'IChannelIntegration' interface.

public interface IChannelIntegration
{

}

public class IntegrationA : IChannelIntegration
{

}

public class IntegrationB : IChannelIntegration
{

}

public IChannelIntegration CreateIntegration(string letter)
{
   if(letter = "A")
   {
     return new IntegrationA();
   }
   else if (letter = "B")
   {
     return new IntegrationB();
   }
   else
   {
     return null;
   }
}

One of my integrations provides some functionality that other integrations don't. I need a way of calling this functionality on the IChannelIntegration object returned from the CreateIntegration function.

I created the following collection of classes for dealing with these custom IntegrationFeatures. The IntergrationFeature interface is what all features would derive from.

public interface IIntegrationFeature {}

public interface ICustomLogic : IIntegrationFeature 
{
  void DoSomething() {}
} 

//Provides a concrete implementation of the logic.
public class CustomLogic : ICustomLogic 
{
  public void DoSomething() {}
}

Another interface was then created that would attach to Integration classes to support that the integration provides custom functionality.

public interface ISupportsFeature<T>  where T : Interfaces.IIntegrationFeature
{
    T GetFeature();
}

It would be implemented as followed;

public class IntegrationC : IChannelIntegration, ISupportsFeature<ICustomLogic>
{
    public ICustomLogic GetFeature()
    {
        return new CustomLogic ();
    }
}

So that would be how you'd give the Integration it's specific logic and then you'd call it using the following;

public static IIntegrationFeature GetIntegrationFeature<IIntegrationFeature>()
        where IIntegrationFeature : Interfaces.IIntegrationFeature
    {
        IIntegrationFeature integrationFeature = null;

        IChannelIntegration service = CreateIntegration();

        //Attempt to cast the integration too determine if it supports the Integration Feature.
        ISupportsFeature<IIntegrationFeature> supportsFeature = service as ISupportsFeature<IIntegrationFeature>;

        if(supportsFeature != null)
        {
            integrationFeature = supportsFeature.GetFeature();
        }

        return integrationFeature;
    }

public void TestMethod()
{
    ICustomLogic customLogic = GetIntegrationFeature<ICustomLogic>();
    if (customLogic != null)
    {
        customLogic.DoSomething();
    }
}

I'm unsure how I feel on this implementation, part of me thinks I'm massively over complicating the situation but I also think it's quite easy to use as many of my integrations will require custom logic providing for them.

Another issue is that if an integration provides multiple IntegrationFeatures you'll have to explicitly call the interface function due to them having the same GetFeature name, which could cause more confusion for other developers.

Upvotes: 2

Views: 151

Answers (1)

Adrian Iftode
Adrian Iftode

Reputation: 15673

I would start from top to bottom, from your need to the implementation.

From what I see you need to access at runtime a feature provided by an integration created by a certain system from a given value. So your first actors would be

interface IIntegrationFeature {}

and something that would provide you this feature

interface IIntegratedSystem
{
  T GetFeature<T>(string integrationType) where T : IIntegrationFeature;
}

A client of this setup would be using the actors like this

IIntegratedSystem system = .... // system implementation

T feature = system.GetFeature<T>("Which integration type?");

if (feature != null)
{
    //the feature exists and I'm using it
}

The IIntegratedSystem implementation's responsibility is to provide the IIntegrationFeature from an existing IChannelIntegration chosen or created at real time. You considered to inform any client that an IChannelIntegration implementation has implemented a feature by implementing the ISupportFeature<IIntegrationFeature>. What happens when the integration has about 20 features? You will have a class which implements the integration interface itself and another 20 ISupports interfaces. How do you add another feature? You modify the class by adding this new ISupports<INewFeature> violating the Single Responsibility Principle. And you don't need to know at compile time that an integration fulfills a certain feature, since the client may or may not use it. Basically IChannelIntegration sounds more like a composite of integrations which should be able to give an IIntegrationFeature and thus the IChannelIntegration would have a definition similar to IIntegratedSystem

interface IChannelIntegration 
{
    T GetFeature<T>() where T : IIntegrationFeature;
}

The way how you build the implementation IChannelIntegration depends on you but I prefer to let the creation of features on the root of your application. Mine would be like this

class ChannelIntegration : IChannelIntegration
{
    private Dictionary<Type, IIntegrationFeature> features;
    public ChannelIntegration()
    {
        features = new Dictionary<Type, IIntegrationFeature>();
    }
    public void RegisterFeature<T>(IIntegrationFeature feature) where T:IIntegrationFeature
    {
        features.Add(typeof(T), feature);
    }
    public T GetFeature<T>() where T : IIntegrationFeature
    {
        IIntegrationFeature feature = features.TryGetValue(typeof(T), out feature) ? feature : null;
        return (T)feature;
    }
}

The ChannelIntegration class maintains the features list. You use RegisterFeature to add more features and to make sure is a feature, you constrain them to implement the IIntegrationFeature interface. So when you add a new feature you don't change the ChannelIntegration class, you use it.

The IIntegratedSystem will access a feature from IChannelIntegration based on a runtime values. This asks, as you said, to use an Abstract Factory. This would look like

interface IChannelIntegrationFactory 
{
    IChannelIntegration CreateIntegration(string integrationType);
}

The IIntegratedSystem will use this factory to create the IChannelIntegration and to achieve this I would use the constructor injection mechanism.

   class IntegratedSystem : IIntegratedSystem
{
    private IChannelIntegrationFactory integrationFactory;
    public IntegratedSystem(IChannelIntegrationFactory integrationFactory)
    {
        this.integrationFactory = integrationFactory;
    }
    public T GetFeature<T>(string integrationType) where T: IIntegrationFeature
    {
        T integrationFeature = default(T);
        IChannelIntegration integration = integrationFactory.CreateIntegration(integrationType);
        if (integration != null)
        {
            integrationFeature = (T)integration.GetFeature<T>();    
        }
        return integrationFeature;
    }
}

Now the IntegratedSystem class is using the IChannelIntegrationFactory to create an IChannelIntegration based on the runtime value and then it will retrieve from it the implemented IIntegrationFeature if any.

What we need now is to implement this factory which will create the IChannelIntegrations with their IIntegrationFeatures

Let's create first a new feature

interface ICustomFeature : IIntegrationFeature {}

class CustomFeature : ICustomFeature
{       
}

And based on this the Abstract Factory implementation is like

class ChannelIntegrationFactory : IChannelIntegrationFactory
{
    public IChannelIntegration CreateIntegration(string integrationType)
    {
        // use integrationType to decide witch IChannelIntegration to use
        IChannelIntegration integration = new ChannelIntegration();
        integration.RegisterFeature<ICustomFeature>(new CustomFeature());

        return integration;
    }
}

We created here a ChannelIntegration with its single feature. Sure you can have multiple paths like you started where you create different IChannelIntegrations with their own ICustomFeatures and retrieve the one based on integrationType parameter.

So how you consume this?

    IIntegratedSystem system = new IntegratedSystem(new ChannelIntegrationFactory());
    ICustomFeature customFeature = system.GetFeature<ICustomFeature>("Which Integration?");
    if (customFeature != null)
    {
        //use this custom feature

    }
    else
    {
        // that's OK, will wait until is registered.
    }

You want to add a new feature? Register it in the Abstract Factory implementation.

So would you expect to create a new abstract factory of type ChannelIntegrationFactory for each different type of ChannelIntegration?

No. The factory picks one of from a list of concrete implementations. Here is one example

class ChannelIntegrationFactory : IChannelIntegrationFactory
{
    public IChannelIntegration CreateIntegration(string integrationType)
    {
        IChannelIntegration integration = null;
        switch (integrationType)
        {
            case "MobileIntegration":
                integration = new ChannelIntegration();
                integration.Register<ITapGestureTrack>(new TapGestureTrack());
                break;
            case "DesktopIntegration":
                integration = new ChannelIntegration();
                integration.Register<IClickTrack>(new ClickTracker());
                integration.Register<ILargeImagesProvider>(new LargeImagesProvider());
                break;
        }
        return integration;
    }
}

You can even make the ChannelIntegration as abstract and create specialized integrations:

 class MobileIntegration : ChannelIntegration
    {
       public MobileIntegration()
       {
            Register<ITapGestureTrack>(new TapGestureTrack());
       }
    }

    class DesktopIntegration : ChannelIntegration
    {
       public DesktopIntegration()
       {
            Register<IClickTrack>(new ClickTracker());
            Register<ILargeImagesProvider>(new LargeImagesProvider());
       }
    }

And the Abstract Factory becomes

class ChannelIntegrationFactory : IChannelIntegrationFactory
    {
        public IChannelIntegration CreateIntegration(string integrationType)
        {
            IChannelIntegration integration = null;
            switch (integrationType)
            {
                case "MobileIntegration":
                    integration = new MobileIntegration();
                    break;
                case "DesktopIntegration":
                    integration = new DesktopIntegration();
                    break;
            }
            return integration;
        }
    }

Just see this class as your system's bootstrap class. This is the only class which creates integrations with the features. Btw this is how you can easily enable Dependency Inversion, using your preferred IoC. Fiddle

Upvotes: 2

Related Questions