Search code examples
c#polymorphismabstractionvisitor-pattern

How to address the need to access to specific implementations in a scenario involving polymorphism?


I stumbled on this problem that I am not able to solve properly. Here is some explanation.

Code

I have these Product classes:

public abstract class Product
{
    public int BaseParam {get;set;}
}

public class SpecificProductA : Product
{
    public int ParamA {get;set;}
}

public class SpecificProductB : Product
{
    public int ParamB {get;set;}
}

And I have these Consumer classes:

public interface IConsumer
{
    void Consume(Product product);
}

public class ConcreteConsumerA : IConsumer
{
    public void Consume(Product product)
    {
        /* I need ParamA of SpecificProductA */
    }
}

public class ConcreteConsumerB : IConsumer
{
    public void Consume(Product product)
    {
        /* I need ParamB of SpecificProductB */
    }
}

Problem

I need the concrete implementations of the IConsumer interface to access specific parts of the Product. ConcreteConsumerA will only be able to consume ProductA and ConcreteConsumerB can only consume ProductB. This breaks the nice abstraction that I had with Consumer & Product.

Solution 1: Casting

The first and obvious thing that can be done is casting the product instance to the specific product. It works but it is not ideal as I rely on the runtime to throw any errors if anything is wrong with the type.

Solution 2: Breaking the inheritance of the product classes

The other solution has been to break the Product inheritance to something like this:

public class Product
{
    public int BaseParam {get;set;}

    public SpecificProductA ProductA {get;set;}

    public SpecificProductB ProductB {get;set;}
}

public class SpecificProductA
{
    public int ParamA {get;set;}
}

public class SpecificProductB
{
    public int ParamB {get;set;}
}

Solution 3: Generics

I can also make the IConsumer interface generic like this:

    public interface IConsumer<TProduct> where TProduct: Product
    {
        void Consume(Product product);
    }

    public class ConcreteConsumerA : IConsumer<SpecificProductA>
    {
        public void Consume(SpecificProductA productA)
        {
            /* I now have access to ParamA of SpecificProductA */
        }
    }

    public class ConcreteConsumerB : IConsumer<SpecificProductB>
    {
        public void Consume(SpecificProductB productB)
        {
            /* I now have access to ParamA of SpecificProductB */
        }
    }

However, like cancer, this generic interface is now spreading into the whole program which is not ideal either.

I am not certain what is wrong here and which rule has been broken. Maybe it is a design issue that needs to be changed. Is there a better solution that the ones provided to solve this problem?


Solution

  • I have found a solution that solves my problem: the Visitor Pattern. The trick was to find another abstraction (called here ICommonInterface) between my IConsumer and my Product and let the visitors deals with the details.

    public interface IProductVisitor
    {
          ICommonInterface Visit(SpecificProductA productA);
    
          ICommonInterface Visit(SpecificProductB productB);
    }
    
    /* The purpose of this abstract class is to minimize the impact of the changes if I had to support another SpecificProductC. */ 
    public abstract class ProductVisitor : IProductVisitor
    {
          public virtual ICommonInterface GetCommonInterface(SpecificProductA productA)
          {
              throw new NotImplementedException();
          }
    
          public virtual ICommonInterface GetCommonInterface(SpecificProductB productB)
          {
              throw new NotImplementedException();
          }
    }
    
    public sealed class SpecificProductAVisitor : ProductVisitor
    {
          public override ICommonInterface GetCommonInterface(SpecificProductA productA)
          {
              /* This guy will deal with ParamA of SpecificProductA */
              return new ImplACommonInterface(productA);
          }
    }
    
    public sealed class SpecificProductBVisitor : ProductVisitor
    {
          public override ICommonInterface GetCommonInterface(SpecificProductB productB)
          {
              /* This guy will deal with ParamB of SpecificProductB */
              return new ImplBCommonInterface(productB);
          }
    }
    

    Then I have to allow the new IProductVisitor on the Product classes:

    public abstract class Product
    {
        public int BaseParam { get; set; }
    
        public abstract ICommonInterface Visit(IProductVisitor productVisitor);
    }
    
    public class SpecificProductA : Product
    {
        public int ParamA {get;set;}
    
        public override ICommonInterface Visit(IProductVisitor productVisitor)
        {
            /* Forwards the SpecificProductA to the Visitor */
            return productVisitor.GetCommonInterface(this);
        }
    }
    
    public class SpecificProductB : Product
    {
        public int ParamB {get;set;}
    
        public override ICommonInterface Visit(IProductVisitor productVisitor)
        {
            /* Forwards the SpecificProductB to the Visitor */
            return productVisitor.GetCommonInterface(this);
        }
    }
    

    Each IConsumer implementations can now do the following without having the need to cast anything:

        public interface IConsumer
        {
            void Consume(Product product);
    
            ICommonObject Visit(IProductVisitor productVisitor);
        }
    
        public class ConcreteConsumerA : IConsumer
        {
            public void Consume(Product product)
            {
                /* The logic that needs for ParamA of SpecificProductA is now  
     pushed into the Visitor. */
                var productAVisitor = new SpecificProductAVisitor();
                ICommonInterface commonInterfaceWithParamA = product.GetCommonInterface(productAVisitor); 
            }
        }
    
        public class ConcreteConsumerB : IConsumer
        {
            public void Consume(Product product)
            {
            /* The logic that needs for ParamB of SpecificProductB is now  
     pushed into the Visitor. */
                var productBVisitor = new SpecificProductBVisitor();
                ICommonInterface commonInterfaceWithParamB = product.GetCommonInterface(productBVisitor); 
            }
        }