Search code examples
oopinheritancecompositionsolid-principles

Composition over Inheritance - code duplication


I would appreciate some clarification on how to properly implement 'Composition over Inheritance'. I think I have a grasp on the theory but in practice I struggle to understand how it doesn't lead to code duplication.

I have the following example (it's Java): suppose we have an abstract animal class:

abstract class Animal {
    protected void eat() {
        // Common eating implementation
    }

    protected void sleep() {
        // Common sleeping implementation
    }
}

And we want to build both flying and swimming animals. I understand that the best way to do this so that we follow LSP is with interfaces for each:

interface Flyer {
    void fly();
}

interface Swimmer {
    void swim();
}

So then we would have

class Salmon extends Animal implements Swimmer {
    @Override
    public void swim() {
        // Swim implementation
    }
}

class Sparrow extends Animal implements Flyer {
    @Override
    public void fly() {
        // Fly implementation
    }
}

But then we get a new requirement for a Magpie that flies the same way as a sparrow. We would create the class, not too different from a Sparrow:

class Magpie extends Animal implements Flyer {
    @Override
    public void fly() {
        // Same exact fly implementation as Sparrow.fly
    }
}

Imagine for the purpose of this exercise, that the fly implementation is very complex with db integration, logging, etc. - this leads to a load of duplicated code and if we add more birds or fishes, we'd need to duplicate the code even further.

There was also an idea of having an abstract class for the same type of flyers like

abstract class FlyingBird extends Animal implements Flyer

and have the common implementation there but what if we need to create a few of these and there's an animal that needs to extend two of them? It's a slippery slope...

Is there any way to avoid this? Or am I missing the mark somewhere?


Solution

  • In composition, which I favor over inheritance for sharing code for many reasons, you can look to share code by injecting a common implementation.

    In your example, you could have a Flyer impl that can be shared between Sparrow and Magpie. Naming it might be a little tough - I recommend using the "use before reuse" principle, and so just give it a good enough name and refactor later if needed.

    So, for example, you could have a SmallBirdFlyer implementation, inject that into both the Sparrow and Magpie implementations, then use delegation from Sparrow and Magpie to that impl. Or, instead of having Sparrow and Magpie directly implement Flyer, you could have them return a Flyer via a method, such as getFlyer(), and then Sparrow and Magpie could directly return the SmallBirdFlyer impl there.

    Here is an example using delegation:

    class Magpie implements Animal, Flyer {
        private SmallBirdFlyer myFlyer;
    
        public void setMyFlyer(SmallBirdFlyer myFlyer) {
            this.myFlyer = myFlyer;
        }
    
        @Override
        public void fly() {
            this.myFlyer.fly();
        }
    }
    

    I think of it this way - instead of pushing down functionality from the top-level and then finding ways to customize at lower levels, build up the common functionality for use at the higher level.