javainheritanceinterfacemodularityopen-closed-principle

Reduce code duplication without subclass inheritance


I'm playing around with subclassing vs Interfaces and composition. I end up getting confused about a few things when it comes to the code duplication. As we all know there are alot of scenarios where subclassing and inheritance is just not the way to go, however it's effective in terms of reducing code duplication.

Interfaces are strong and gives great readability if done properly but i cant wrap my head around the fact that it's really not helping me with the reducing of code duplicaton. We can end up in scenarios where subclassing is not effective.. But the possibilities to extend the program is big and whenever we do so, trying to maintain the Open closed principal, we end up doing Realizations / Implementations of Interfaces in absurd amounts of copy paste code, where it's probably avoidable with subclassing (in terms of code duplication).

How do we build up great strategies with Interfaces and Composition where we avoid writing the same methods over and over again? In such a way that we can keep the modularity and stick with the open closed principle at the same time. Do we have any guidelines to how we can fast and effective decide if the code duplication in fact will be worth it in the end?

Cheers

< /wallOfText>


Solution

  • Object Oriented modelling is very subjective, but the only thing I can thing of here is the old Inheritance vs. Composition discussion: https://www.thoughtworks.com/insights/blog/composition-vs-inheritance-how-choose

    Based on your argumentation, I believe you often try to extract a superclass from two or more classes with similar code, so they can all share the same inherited methods instead of just duplicate them. Although technically this gives you exactly what you want, you should also take care of the inheritance semantics since it will denote an is-a relationship (i.e., car is-a vehicle, dog is-a mammal, report-screen is-a read-only-screen). Since Java doesn't offer multiple inheritance, you may end up limited and confused if you class hierarchy grows.

    So, before starting to extract superclasses for reuse, have in mind that you can also extract this code-i-want-to-reuse units to be part-of other classes (composition).

    Sorry for my conceptual example, but here it goes: Both Dog and Lion are mammals and hunters. They should naturally inherit Mammals superclass (with a lot of reusable code among mammals). As not all mammals hunt, we don't want to define a new hunt() method on Mammals class.

    At this very point, you may be thinking of creating a new inheritance level: Mammals <- HuntingMammals. But think: if you continue doing this for every distinctive aspect of animals, you'll have dozens of classes in a tricky and puzzled hierarchy. Besides that, we also know that some reptiles and birds also hunt, so, we'd better isolate all the hunting thing elsewhere.

    As a healthy alternative to inheritance, we can define a separate Hunter class. To reuse it's contents, all we need to do is put a Hunter object as member of both Dog and Lion (a field). If we need to treat dogs and lions as hunters together (polymorphically), we can define a CanHunt interface to group them.

    Check the example below:

    class Hunter {
       void hunt(){
           System.out.println("i'm hunting...");
       }
    }
    
    interface CanHunt{
       Hunter getHunter();
    }
    
    class Dog extends Mammals implements CanHunt{
       ...
       Hunter hunter = new Hunter();
       
       @Override
       Hunter getHunter(){
           return hunter;
       }      
       ...
    }
    
    
    class Lion extends Mammals implements CanHunt{
       ...
       Hunter hunter = new Hunter();
       
       @Override
       Hunter getHunter(){
           return hunter;
       }      
       ...
    }
    

    And here we have a polymorphic sample code that ask both dog and lion to do their hunting stuff:

    ...
    List<CanHunt> hunters = new LinkedList();
    hunters.add(new Dog());
    hunters.add(new Lion());
    
    for(CanHunt h:hunters){
      h.getHunter().hunt(); //we don't know if it's a dog or a lion here...
    }
    ...
    

    I hope this simple example gives you some inspiration. And it can get quite complex if we keep evolving it towards a more detailed though flexbile design. For instance, Hunter class could be abstract with different implementations, since dog hunts differentlty from lions, but they share some common behaviour.