5

BEFORE:

I have an interface that has one method definition

public interface IDockable
{
    void Dock(DockerContainerConfig config);
}

Everything is ok for my first implementation

public class DockerContainer : IDockable
{
    public void Dock(DockerContainerConfig config)
    {

    }
}

public class DockerContainerConfig
{ 
    //machine configuration properties here 
}

On my second implementation however, I realized that I need to use a different set of configuration.

public class DockerMachine : IDockable
{
    public void Dock(DockerMachineConfig config)
    {

    }
}

public class DockerMachineConfig
{
    //machine configuration properties here 
}

AFTER:

In order to make the Configuration Classes polymorphic, I created a parent class and let the Configuration Classes inherit from it.

public class DockerConfig
{

}

public class DockerMachineConfig : DockerConfig
{
    //machine configuration properties here 
}

public class DockerContainerConfig : DockerConfig
{
    //container configuration properties here 
}

With this I also changed the method definition for the interface.

public interface IDockable
{
    void Dock(DockerConfig config);
}

And in my concrete classes, I just did a cast on the configuration class objects. Please note that the two configuration classes are totally different, nothing is common. One deals with the machine so it needs machine configuration and the other deals with the container so it needs container configuration. But either way you do it, DockerContainer and DockerMachine does the same thing. "dock a container inside a machine". It's something like DockerMachine does things from the outside and DockerContainer does things from the inside. You get the idea :)

public class DockerContainer : IDockable
{
    public void Dock(DockerConfig config)
    {
        DockerContainerConfig container = config as DockerContainerConfig;
    }
}

public class DockerMachine : IDockable
{
    public void Dock(DockerConfig config)
    {
        DockerMachineConfig machine = config as DockerMachineConfig;
    }
}

There are two things that I think I made a mistake here:

  1. I changed the method definition in my interface from void Dock(DockerContainerConfig config); to void Dock(DockerConfig config);
  2. I created an empty parent class public class DockerConfig{} just to be able to make my Configuration Classes polymorphic

I would like to know if the changes I made conforming to good design and best practices.

  • 7
    Your final example breaks Liskov Substituion Principle, as now your code must pass a specific type into the Dock method of IDockable depending on which implementation. This completely negates any point in having the IDockable in common, as it would now actually be better for each class to have a more strongly typed Dock method that accepts the correct config type. – Mr Cochese Oct 14 '16 at 10:24
  • I don't know what the C# conventions are, but in general, this is fine, and in fact I would say it's good design. This particular "design pattern" you are using is actually just a way to mimic sum types in languages that lack direct support for them. Polymorphism is one of the most powerful programming tools available. – gardenhead Oct 14 '16 at 15:23
  • Why have a Dockable interface at all? Why not have the Dock() method be on DockerConfig? – Winston Ewert Oct 17 '16 at 5:13
17
public interface IDockable<T>
{
    public void Dock(T config);
}

By having a type parameter, you can specify the parameter you want to accept within the Dock method in your classes. Example:

public class DockerMachine : IDockable<DockerMachineConfig>
{
    public void Dock(DockerMachineConfig config)
    {
    }
}

public class DockerContainer : IDockable<DockerContainerConfig>
{
    public void Dock(DockerContainerConfig config)
    {
    }
}

That would be my personal preferred way to do this, but there are plenty of other ways which you'll be able to come up with. What you're currently doing could definitely be simplified by having a type parameter on the IDockable interface which would be a little bit cleaner in my opinion.

  • This is very good, but it would also be nice to see a solution without generics. – Mike Supports Monica Oct 14 '16 at 14:03
  • 10
    @Mike Why, when generics solve the problem well? – Mason Wheeler Oct 14 '16 at 15:06
  • @MasonWheeler Because this design dilemma can come up in languages without generics also. – Mike Supports Monica Oct 14 '16 at 19:33
  • 4
    @Mike so do these other languages have dynamic typing? Support unions? Not have classes, etc, etc? The OP has tagged the question with C# and posted C# code. So this answer supplies a good way of solving the problem in that language. For other languages, the solution could be very different, and are beyond the scope of expected answers here. – David Arno Oct 15 '16 at 10:54
9

Implementation specific parts shouldn't be part of the interface. Instead take every specific config as a constructor parameter.

public interface IDockable
{
    void Dock();
}

public class DockerContainer : IDockable
{
    private DockerContainerConfig config;

    public DockerContainer(DockerContainerConfig config)
    {
        this.config = config;
    }

    ...
}

public class DockerMachine : IDockable
{
    private DockerMachineConfig config;

    public DockerMachine (DockerMachineConfig config)
    {
        this.config = config;
    }

    ...
}
  • That's my preference too. Because each docker machine can have different config so it makes lot of sense to initialize a machine with its specific configuration values, and thus accepting config in constructor. – Nikhil Vartak Jun 7 '18 at 8:22

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.