Az öröklődés az objektumorientált nyelvek beépített eszköze és sok mindent meg lehet valósítani, de nem mindig a legjobb ötlet. Ha egy alkalmazás objektummodelljét tervezzük és öröklést használunk, akkor a típusaink tervezésénél a fő motiváció általában az, hogy mik is pontosan a típusok, míg a composition over inheritance azt helyezi előtérbe, hogy mit csinálnak ezek a típusok.
Nézzünk egy példát az öröklés hátrányaira. Tételezzük fel, hogy egy játékot készítünk, amiben lesznek kutyák. Nyilvánvalóan ezt a Dog osztály implementálja majd és nyilvánvalóan a kutya ugatni fog a Bark() metódus meghívásakor.
Fejlesztés közben jön az ötlet, ha már kutyák vannak a játékban, akkor legyenek macskák is. Hasonlóan a Dog osztályhoz elkészítjük a Cat osztályt.
Jön a következő ötletünk. Legyen survival a játék, vagyis az idő múlásával legyenek egyre éhesebbek az állatkák. Ehhez bevezethetnénk mind a két osztályba egy Eat() metódust, de ez azt jelentené, hogy kód duplikálásunk van, ami nem jó. Éppen ezért kiemeljük a kódot egy közös ős Animal osztályba.
Eddig szép és jó minden. Jön a következő ötletünk. Legyenek robotok is a játékban, mert azok is menők. A robotokat valahogy így tudnánk modellezni:
Ezzel sincs semmi probléma egészen addig, amíg mondjuk nem akarunk ellenséges robot kutyákat beleprogramozni a játékunkba. Ebben az esetben a robot kutya nem örököltethető az Animal osztályból mert, az rendelkezik az evés képességével. Ha pedig a RobotEnemy osztályból származna, akkor duplikálnunk kellene a nyávogás funkcióját. Ez nem egy áthidalhatatlan probléma, mondhatnánk, mert ténylegesen kivitelezhető a dolog, de nem lesz egy szép a megoldás, ami végső soron majd azt eredményezi, hogy karbantarthatatlan lesz az egész.
Ennek a problémának az áthidalásában segít a composition over inheritance. Ez alapján a játékunk objektumait az alábbi módon bonthatjuk fel:
Dog = Eater + Barker
Cat = Eater + Meower
RobotEnemy = RobotMovable + Attacker
RobotDogEnemy = RobotMovable + Attacker + Barker
A felbontás minden eleme egy osztály lesz. A komplexebb osztályaink pedig a viselkedést és funkcionalitásukat tartalmazással (compositon) fogják megvalósítani.
Vagyis a Dog osztály tartalmazni fog egy Eater és Barker példányt is.
Azért, hogy az objektumainkról el lehessen dönteni, hogy egy adott viselkedést megvalósítanak-e, bevezetjük még az IEater, IBarker, IMeower, IRobotMovable és IAttacker interfészeket.
Ezeket a modell típusaink implementálni fogják, méghozzá olyan módon, hogy a bennük tárolt implementációk felé továbbítják a kéréseket. Vagyis, ha a Cat osztály Meow() metódusát hívom, akkor az a benne tárolt Meower osztály Meow() metódusát fogja hívni.
Ez a „metódus továbbító” implementáció szükségessége a compositon over inheritance legnagyobb hátránya, ami végső soron nem teszi egyértelműen jobb megoldássá az örökléssel szembeállítva. Itt megjegyzem, hogy a „metódus továbbító” implementációk implementálása nem biztos, hogy minden esetben szükséges. Ez leginkább alkalmazásfüggő, hogy hogyan szeretnénk használni az osztályunkat.
A kérdés akkor már csak az, hogy mikor érdemes használni? A composition over inheritance akkor jó, amikor a fejlesztés elején sok az ismeretlen. Az öröklés megköveteli, hogy sok részletet előre ismerjünk, hogy ki tudjuk alakítani a modellt. Ha ennek nagy része változás tárgyát képezi, akkor a jövőbe kellene látnunk, és általában itt mennek félre a dolgok.
Ilyen esetekben jön jól a composition over inheritance, mert ugyan több kódot eredményezhet, de cserébe flexibilisebb lesz a modell kialakításunk és nem korlátoznak be bennünket az alkalmazás készítésének elején meghozott döntéseink.
A C# 8-as változatától fogva az interfészek tartalmazhatnak kódot is, ami lehetővé teszi, számunkra, hogy a viselkedéseket magában az interfészben definiáljunk, így megspóroljuk azoknak az osztályoknak az implementálását, amelyek a composition részét képeznék. További előny, hogy a továbbító metódusokat sem kell implementálnunk.
Ezt az úgynevezett trait (jellemvonás) alapú programozás, mivel viselkedés típusokat implementálunk alapvetően és ezek kompozíciójából alakul ki az objektummodellünk.
A C# default interfész implementációja alapvetően nem erre lett kitalálva. Ebből adódóan ugyan meg lehet vele valósítani trait-szerű viselkedést, de ez bizonyos esetekben nehézkes és kényelmetlen.