Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to Maximize Code Reuse in this Interface vs Inheritance C# Example

Inspired by a great video on the topic "Favor object composition over inheritance" which used JavaScript examples; I wanted to try it out in C# to test my understanding of the concept, but it didn't go as well as I'd hoped.

/// PREMISE
// Animal base class, Animal can eat
public class Animal
{
    public void Eat() { }
}
// Dog inherits from Animal and can eat and bark
public class Dog : Animal
{
    public void Bark() { Console.WriteLine("Bark"); }
}
// Cat inherits from Animal and can eat and meow
public class Cat : Animal
{
    public void Meow() { Console.WriteLine("Meow"); }
}
// Robot base class, Robot can drive
public class Robot
{
    public void Drive() { }
}

The problem is that I want to add RobotDog class that can Bark and Drive, but not eat.


First solution was to create RobotDog as subclass of Robot,

public class RobotDog : Robot
{
    public void Bark() { Console.WriteLine("Bark"); }
}

but to give it a Bark function we had to copy and paste the Dog's Bark function so now we have duplicate code.


The second solution was to create a common super class with a Bark method that then both the Animal and Robot classes inherited from

public class WorldObject
{
    public void Bark() { Console.WriteLine("Bark"); }
}
public class Animal : WorldObject { ... }
public class Robot : WorldObject { ... }

But now EVERY animal and EVERY robot will have a Bark method, which most of them don't need. Continuing with this pattern, the sub-classes will be laden with methods they don't need.


The third solution was to create an IBarkable interface for classes that can Bark

public interface IBarkable
{
    void Bark();
}

And implement it in the Dog and RobotDog classes

public class Dog : Animal, IBarkable
{
    public void IBarkable.Bark() { Console.WriteLine("Bark"); }
}
public class RobotDog : Robot, IBarkable
{
    public void IBarkable.Bark() { Console.WriteLine("Bark"); }
}

But once again we have duplicate code!


The fourth method was to once again use the IBarkable interface, but create a Bark helper class that then each of the Dog and RobotDog interface implementations call into.

This feels like the best method (and what the video seems to recommend), but I could also see a problem from the project getting cluttered with helpers.


A fifth suggested (hacky?) solution was to hang an extension method off an empty IBarkable interface, so that if you implement IBarkable, then you can Bark

public interface IBarker {    }
public static class ExtensionMethods
{
    public static void Bark(this IBarker barker) { 
        Console.WriteLine("Woof!"); 
    }
}

A lot of similar answered questions on this site, as well as articles I've read, seem to recommend using abstract classes, however, wouldn't that have the same issues as solution 2?


What is the best object-oriented way to add the RobotDog class to this example?

like image 600
chriszumberge Avatar asked Feb 12 '16 17:02

chriszumberge


2 Answers

At first if you want to follow "Composition over Inheritance" then more than half of your solutions don't fit because you still use inheritance in those.

Actually implementing it with "Composition over Inheritance" there exists multiple different ways, probably each one with there own advantage and disadvantage. At first one way that is possible but not in C# currently. At least not with some Extension that rewrites IL code. One idea is typically to use mixins. So you have interfaces and a Mixin class. A Mixin basically contains just methods that get "injected" into a class. They don't derive from it. So you could have a class like this (all code is in pseudo-code)

class RobotDog 
    implements interface IEat, IBark
    implements mixin MEat, MBark

IEat and IBark provides the interfaces, while MEat and MBark would be the mixins with some default implementation that you could inject. A design like this is possible in JavaScript, but not currently in C#. It has the advantage that in the end you have a RobotDog class that has all methods of IEat and IBark with a shared implementation. And the this is also a disadvantage at the same time, because you create big classes with a lot of methods. On top of it there can be method conflicts. For example when you want to inject two different interfaces with the same name/signature. As good as such an approach looks first, i think the disadvantages are big and i wouldn't encourage such a design.


As C# doesn't support Mixins directly you could use Extension Methods to somehow rebuilt the design above. So you still have IEat and IBark interfaces. And you provide Extension Methods for the interfaces. But it has the same disadvantages as a mixin implementations. All methods appear on the object, problems with method names collision. Also on top, the idea of composition is also that you could provide different implementations. You also could have different Mixins for the same interface. And on top of it, mixins are just there for some kind of default implementation, the idea is still that you could overwrite or change a method.

Doing that kind of things with Extensions Method is possible but i wouldn't use such a design. You could theoretically create multiple different namespaces so depending on which namespace you load, you get different Extension Method with different implementation. But such a design feels more awkward to me. So i wouldn't use such a design.


The typical way how i would solve it, is by expecting fields for every behaviour you want. So your RobotDog looks like this

class RobotDog(ieat, ibark)
    IEat  Eat  = ieat
    IBark Bark = ibark

So this means. You have a class that contains two properties Eat and Bark. Those properties are of type IEat and IBark. If you want to create a RobotDog instance then you have to pass in a specific IEat and IBark implementation that you want to use.

let eat  = new CatEat()
let bark = new DogBark()
let robotdog = new RobotDog(eat, bark)

Now RobotDog would Eat like a cat, and Bark like a Dog. You just can call what your RobotDog should do.

robotdog.Eat.Fruit()
robotdof.Eat.Drink()
robotdog.Bark.Loud()

Now the behaviour of your RobotDog completely depends on the injected objects that you provide while constructing your object. You also could switch the behaviour at runtime with another class. If your RobotDog is in a game and Barking gets upgraded you just could replace Bark at runtime with another object and the behaviour you want

robotdog.Bark <- new DeadlyScreamBarking()

Either way by mutating it, or creating a new object. You can use a mutable or immutable design, it is up to you. So you have code sharing. At least me i like the style a lot more, because instead of having a object with hundreds of methods you basically have a first layer with different objects that have each ability cleanly separated. If you for example add Moving to your RobotDog class you just could add a "IMovable" property and that interface could contain multiple methods like MoveTo, CalculatePath, Forward, SetSpeed and so on. They would be cleanly avaible under robotdog.Move.XYZ. You also have no problem with colliding methods. For example there could be methods with the same name on each class without any problem. And on top. You also can have multiple behaviours of the same type! For example Health and Shield could use the same type. For example a simple "MinMax" type that contains a min/max and current value and methods to operate on them. Health/Shield basically have the same behaviour, and you can easily use two of them in the same class with this approach because no method/property or event is colliding.

robotdog.Health.Increase(10)
robotdog.Shield.Increase(10)

The previous design could slightly be changed, but i don't think it makes it better. But a lot of people brainlessly adopt every design pattern or law with the hope it automatically makes everything better. What i want to refer here is the often called Law-of-Demeter that i think is awful, especially in this example. Actually there exists a lot of discussion of whether it is good or not. I think it is not a good rule to follow, and in that case it also becomes obvious. If you follow it you have to implement a method for every object that you have. So instead of

robotdog.Eat.Fruit()
robotdog.Eat.Drink()

you implement methods on RobotDog that calls some method on the Eat field, so with what did you end up?

robotdog.EatFruit()
robotdog.EatDrink()

You also need once again to solve collisions like

robotdog.IncreaseHealt(10)
robotdog.IncreaseShield(10)

Actually you just write a lot of methods that just delegates to some other methods on a field. But what did you won? Basically nothing at all. You just followed brainless a rule. You could theoretically say. But EatFruit() could do something different or do something additional before calling Eat.Fruit(). Weel yes that could be. But if you want other different Eat behaviour then you just create another class that implements IEat and you assign that class to the robotdog when you instantiate it.

In that sense, the Law of Demeter is not a dot counting Exercise.

http://haacked.com/archive/2009/07/14/law-of-demeter-dot-counting.aspx/


As a conclusion. If you follow that design i would consider using the third version. Use Properties that contain your Behaviour objects, and you can directly use those behaviours.

like image 99
David Raab Avatar answered Oct 29 '22 04:10

David Raab


I think this is more of a conceptual dilemma rather than a composition issue.

When you say : And implement it in the Dog and RobotDog classes

public class Dog : Animal, IBarkable
{
    public void IBarkable.Bark() { Console.WriteLine("Bark"); }
}
public class RobotDog : Robot, IBarkable
{
    public void IBarkable.Bark() { Console.WriteLine("Bark"); }
}

But once again we have duplicate code!

If Dog and RobotDog have the same Bark() implementation, they should inherit from the Animal class. But if their Bark() implementations are different, it makes sense to derive from IBarkable interface. Otherwise, where is the distinction between Dog and RobotDog?

like image 24
mojorisinify Avatar answered Oct 29 '22 04:10

mojorisinify