This project aims to demonstrate why composition should be prefered to inheritance. I first present two common ways inheritance is used and show the corresponding issues with testability that are resolved by using composition. In the final scenario, I show a common way inheritance causes subtle issues that do not exist with composition.
One way inheritance is generally used is to perform something like the template method pattern. Template method is where a base class implements the template method, which consists of a sequence of steps that need to be performed in a certain order. The steps are implemented by derived classes in order to tailor to specific needs. The example given is to make a beverage. In this contrived example, to make a beverage you must first brew then add the condiments. See the Composition.Template and CompositionUnitTests.Template namespaces.
BasedefinesMakeBeverage()method, which invokes the abstractBrew()and thenAddCondiments()methods.DerivedimplementsBrew()andAddCondiments(). The thing to notice is thatBrew()is very involved, whileAddCondiments()is simple.DerivedTestsdemonstrates what it would take toVerifyOnlyAddsChocolate()AddCondiments()is not accessible, soMakeBeverage()is called.- However, to make the beverage,
Brew()is called, which is very involved. - To write a simple test for
AddCondiments(), a bunch of work has to be done to makeBrew()work. - You may be thinking to simply make
AddCondiments()public. However, there still is no way of writing a good, simple test to ensureMakeBeverage()works correctly. A bad test might mock the very class you're testing. However, that's dangerous, because it's easy to mock the class you're testing into accidentlly passing your test even though it should fail.
Change inheritance to composition.
BasebecomesCommonDerivedbecomesSpecializedCommonis composed withSpecialized
SpecializedTests.VerifyOnlyAddsChocolate()- There is no setup required to make
Brew()work! - If
Brew()is modified or any of the brewers' APIs change, this test can remain unchanged and still work!
- There is no setup required to make
CommonTests: I can now write simple tests forCommon.MakeBeverage(), likeVerifyAddsCondimentsToWhatsBrewed()because everything inSpecializedis mocked to return simple values!
Another common scenario where inheritance is used is to share common code. The common code resides in the base class and is inherited by derived classes in order to consume that functionality. The contrived example given is to fit a gaussian curve in order to add a certain meaningful (because I say it is) offset. See the Composition.Share and CompositionUnitTests.Share namespaces.
Baseimplements a 3-point gaussian fit. The idea here is that it's difficult to do this.DerviedinheritesBaseto calculate the mean to simply add its meaningful offset.DerviedTests: The goal is toVerifyOffsetsMeanByFive().- I have to do a lot of work to fabricate a set of numbers to give a specified mean to be used in the subsequent calculation I'm actually trying to test.
- One should not simply create fake numbers and set a breakpoint to grab the mean to use in that test. That's a dangerous way of accidentally passing the test. For the reason you passed the test may be because the numbers you picked happened to work out.
- If the base class gets modified, it can break this unrelated test.
Change inheritance to composition.
BasebecomesCommonDerivedbecomesSpecialized- And this time,
Specializedis composed withCommon
SpecializedTests.VerifyOffsetMeanByFive()
Commoncan be easily mocked to return simple results!- Modifications to the
Fit()alorithm will not impact this test!
The final scenario (based on this answer on Stack Overflow) is when a derived class inherits from a base class in order to fulfil some interface. It wants to leave one method in the interface implemented with common functionality from the base class. But it wants to replace the other method with specialized functionality. The example given modifies state in the specialized method from the base class. See the Composition.Shared and CompositionUnitTests.Shared namespaces.
Baseimplements the interface- It manipulates public state (
Stuff) inDoSpecializedThings(), but not inDoCommonThings()
- It manipulates public state (
UserOfBasecallsDoSpecializedThings()and then usesStuffDerivedinherits fromBaseto fulfil the interface, but overridesDoSpecializedThings()with its own implementation.UserOfBaseis now broken becauseStuffis not correct after invokingDoSpecializedThings().- This may be an easier issue to catch at first, but what happens if
Basestarted out not modifyingStuffand then is changed to modifyStuffwithout knowledge thatDerivedneeds to be changed. Also,Derivedshould never have to care aboutBase.Stuffto begin with. - It may also not be possible to modify
StufffromDerivedas in this scenario because the setter is private and Stuff is immutable.
Change inheritance to composition.
BasebecomesCommonDerivedbecomesSpecializedUserOfBasebecomesUserOfCommon- BOTH
SpecializedandCommonimplement the interface Specializedis composed withCommonto use it to implementDoCommonThings()
It is now impossible for UserOfCommon to get handed a Specialized, where DoSpecializedThings() does not properly mutate Common.Stuff in a statically-typed language. In effect, using composition prevented this bug.
I demonstrated that using composition instead of inheritance increases testability and decreases bugs thereby increasing maintainability. You will find that composition is also more versatile as now injected implementations can be swapped out for other ones.
However, it should be noted that you can't always use composition. The one place I've found where you need inheritance is in the UI. This is specifically due to the following combination of requirements:
- Empty constructors are needed.
- The responsibility of the view is to create its own components (instead of being injected with them).
- Lots of common functionality is required in each component.
So the only way to get this functionality is to inherit it. It's not that testable (not by unit tests, coded UI tests are more like integration tests). However, the UI should be limited to only the UI by using an MV* design pattern like MVVM. You can also create objects that you are composed with to help you do your job or even implement common functionality and those classes might be testable.