Ugrás a tartalomhoz

Öröklődés helyett objektum-összetétel

A Wikipédiából, a szabad enciklopédiából
A lap korábbi változatát látod, amilyen Hkoala (vitalap | szerkesztései) 2021. március 28., 12:04-kor történt szerkesztése után volt. Ez a változat jelentősen eltérhet az aktuális változattól. (összevonandó: GoF 2 alapelv)
Ez a diagram azt mutatja be, hogy miként lehet az állatok repülési és hangviselkedését rugalmasan megtervezni az öröklődés helyett objektum-összetétel tervezési elv alkalmazásával[1]

Az objektumorientált programozásban (OOP) öröklődés helyett objektum-összetétel (más néven összetétel újrafelhasználási elv) alapja, hogy az osztályoknak polimorf viselkedést és kód-újrafelhasználást kell megvalósítania a kompozíciójukkal (azáltal, hogy saját példányokat tárolnak más osztályokból, melyek implementálják a kívánt funkcionalitást) ahelyett, hogy egy ősosztályból öröklődnének. Más néven GOF2 néven ismert.

Alapok

Az elv implementációja tipikusan azzal kezdődik, hogy különféle interfészeket készítünk, melyek leírják a viselkedéseket, melyeket a rendszernek tartalmaznia kell. Az interfészek teszik lehetővé a polimorfizmust. Az osztályok implementálják az interfészeket, így öröklődés nélkül megadhatjuk a rendszer egyes részeinek a működési viselkedését.

Az osztályok lehetnek ősosztályok bármiféle öröklődés nélkül. Egy alternatív implementációja a rendszer viselkedéseinek megvalósítható úgy, hogy új osztályt írunk, amely implementálja a kívánt viselkedés interfészét. Egy osztály, amely tartalmaz referenciát egy interfészhez segítheti az interfész implementálását, egy döntés, amelyet a futási időig meghozhatunk.

Példa

Öröklődés

Egy példa C++11-ben:

#include <vector>

class Model;

class GameObject {
 public:
 virtual ~GameObject() = default;

 virtual void Update() {}
 virtual void Draw() {}
 virtual void Collide(const std::vector<GameObject*>& objects) {}
};

class Visible : public GameObject {
 public:
 void Draw() override {
  // Draw |model_| at position of this object.
 };

 private:
 Model* model_;
};

class Solid : public GameObject {
 public:
 void Collide(const std::vector<GameObject*>& objects) override {
  // Check and react to collisions with |objects|.
 };
};

class Movable : public GameObject {
 public:
 void Update() override {
  // Update position.
 };
};

Vannak konkrét osztályaink:

  • Játékososztály - amely Test, Mozgatható és Látható
  • Felhő osztály - amely Mozgatható és Látható, de nem Test
  • Épület osztály - amely Test és Látható, de nem Mozgatható
  • Csapda osztály - amely Test, de se nem Látható, se nem Mozgatható

Figyelembe kell venni, hogy az öröklődéssel egyes esetekben súlyos problémákba ütközhetünk, ha nincs kellő gondossággal implementálva. Például a gyémántproblémához[2] vezethet. Egyik megoldás ennek elkerülésére, hogy ilyen osztályokat készítsünk: LáthatóÉsTest,LáthatóÉsMozgatható, LáthatóÉsTestÉsMozgatható, és mindezt az összes lehetséges kombinációra létrehozzuk. Ez viszont rengeteg kódismétléshez vezet. A C++ megoldja a gyémántproblémát azzal, hogy engedi a virtuális öröklődést.

Kompozíció és interfészek

A következő C # példa bemutatja a kompozíció és az interfészek használatának elvét, hogy megvalósuljon a kód újrafelhasználás, és a polimorfizmus.

class Program {
  static void Main() {
    var player = new GameObject(new Visible(), new Movable(), new Solid());
    player.Update(); player.Collide(); player.Draw();

    var cloud = new GameObject(new Visible(), new Movable(), new NotSolid());
    cloud.Update(); cloud.Collide(); cloud.Draw();

    var building = new GameObject(new Visible(), new NotMovable(), new Solid());
    building.Update(); building.Collide(); building.Draw();

    var trap = new GameObject(new Invisible(), new NotMovable(), new Solid());
    trap.Update(); trap.Collide(); trap.Draw();
  }
}

interface IVisible {
  void Draw();
}

class Invisible : IVisible {
  public void Draw() {
    Console.Write("I won't appear.");
  }
}

class Visible : IVisible {
  public void Draw() {
    Console.Write("I'm showing myself.");
  }
}

interface ICollidable {
  void Collide();
}

class Solid : ICollidable {
  public void Collide() {
    Console.Write("Bang!");
  }
}

class NotSolid : ICollidable {
  public void Collide() {
    Console.Write("Splash!");
  }
}

interface IUpdatable {
  void Update();
}

class Movable : IUpdatable {
  public void Update() {
    Console.Write("Moving forward.");
  }
}

class NotMovable : IUpdatable {
  public void Update() {
    Console.Write("I'm staying put.");
  }
}

class GameObject : IVisible, IUpdatable, ICollidable {
  private readonly IVisible _visible;
  private readonly IUpdatable _updatable;
  private readonly ICollidable _collidable;

  public GameObject(IVisible visible, IUpdatable updatable, ICollidable collidable) {
    _visible = visible;
    _updatable = updatable;
    _collidable = collidable;
  }

  public void Update() {
    _updatable.Update();
  }

  public void Draw() {
    _visible.Draw();
  }

  public void Collide() {
    _collidable.Collide();
  }
}

Előnyök

Ezen tervezési minta előnye, hogy nagyobb fokú flexibilitást nyújt. Sokkal természetesebb felépíteni az osztályokat különböző komponensekből, mint közös tulajdonságokat keresve felépíteni azokat egymásból. Például a gázpedál és a kormány nagyon kevés közös tulajdonsággal rendelkezik, mégis mindkét komponens létfontosságú egy autóban. A kompozíció sokkal stabilabb osztályszerkezetet biztosít, átláthatóbb lesz tőle a rendszer felépítése. Tehát sokkal jobb megfogalmazni, hogy egy osztály mire képes (HAS-A), mint hogy maga az osztály micsoda (IS-A).[1]

A kezdeti tervezés egyszerűsíthető azzal, hogy megfogalmazzuk a rendszer objektumainak viselkedéseit különféle interfészekben, ahelyett, hogy hierarchikus viszonyt építenénk fel az osztályok között öröklődés használatával. Ezzel a megközelítéssel a később felmerülő követelménybeli változtatások sokkal egyszerűbben kivitelezhetőek, amiket egyébként az osztályok teljes átalakításával oldhatnánk meg. Ezekmellett, megoldást kínál arra a problémára is, mikor viszonylag kis változtatások szükségesek egy öröklődésalapú osztályszerkezetben, mely több osztálygenerációt is tartalmaz.

Néhány nyelv, nevezetesen a Go, kizárólag az objektum-összetételt használja.[3]

Hátrányok

Egy gyakori hátránya ezen módszer alkalmazásának, hogy adott komponensek által nyújtott metódusokat implementálni kell az örököltetett osztályban, még akkor is ha ezek csak a metódusokat továbbítják (ez igaz a legtöbb programozási nyelvre, de nem az összesre, lásd a lent kifejtett bekezdésben). Ezzel szemben az öröklődéskor nem szükséges az összes ősosztály metódusát újraimplementálni az örököltetett osztályban. Ehelyett az örököltetett osztály csak azokat a metódusokat implementálja (felülírja), melyek működése nem egyezik meg az osztály metódusaival. Ez sokkal kevesebb munkával jár, ha az ősosztály rengeteg metódust tartalmaz az alapműködéssel, és csak néhányat kell átírni az örököltetett osztályban.

Például az alábbi C# kódban a

Dolgozó

ősosztály változói és a metódusai öröklésre kerülnek az

ÓrabérDolgozó

és

FixFizetésDolgozó

alosztályban. Csak a

Fizetés()

metódust kell implementálni (egyénileg) az összes örököltetett alosztályban. A többi metódust az ősosztály implementálja, és megosztja az összes alosztállyal; nem szükséges ezeket újraimplementálni (felülírni) vagy megemlíteni az alosztály definíciójakor.

// Base class
public abstract class Employee
{
  // Properties
  protected string Name { get; set; }
  protected int ID { get; set; }
  protected decimal PayRate { get; set; }
  protected int HoursWorked { get; }

  // Get pay for the current pay period
  public abstract decimal Pay();
}

// Derived subclass
public class HourlyEmployee : Employee
{
  // Get pay for the current pay period
  public override decimal Pay()
  {
    // Time worked is in hours
    return HoursWorked * PayRate;
  }
}

// Derived subclass
public class SalariedEmployee : Employee
{
  // Get pay for the current pay period
  public override decimal Pay()
  {
    // Pay rate is annual salary instead of hourly rate
    return HoursWorked * PayRate / 2087;
  }
}

A hátrányok elkerülése

Egyes nyelvek speciális eszközöket kínálnak ezek mérséklésére:

  • Raku a handles kulcsszóval teszi lehetővé a metódustovábbítást.
  • A Java a Project Lombok[4] szolgáltatást nyújtja, amely lehetővé teszi a delegálás végrehajtását egyetlen @Delegate annotáció használatával a mezõn, ahelyett, hogy a delegált mezõbõl az összes módszer nevét és típusát másolná és megtartaná.[5] A Java 8 lehetővé teszi az interfészben alapértelmezett metódusok írását, hasonlóan a C#-hoz stb.
  • A Julia makrók felhasználhatók továbbítási módszerek előállítására. Számos megvalósítás létezik, mint például a Lazy.jl[6] és a TypedDelegation.jl.[7]
  • A Swift kiterjesztések felhasználhatók egy protokoll alapértelmezett megvalósításának meghatározására magán a protokollon, nem pedig az egyedi típus megvalósításán belül.[8]
  • Kotlin tartalmazza a delegálási mintát a nyelvi szintaxisban.[9]
  • A Go típus beágyazódás elkerüli a továbbítási módszerek szükségességét.[10]
  • D egy "alias this" deklarációt nyújt egy típuson belül, továbbítva benne minden metódust és egy másik tartalmazott típus tagját.[11]
  • A C # alapértelmezett metódusokat biztosít a 8.0 verzió óta az interfészekben, amely lehetővé teszi a interfész metódusok törzsének meghatározását.[12]

Gyakorlati tanulmányok

A 93 (különböző méretű) nyílt forráskódú Java programról szóló 2013. évi tanulmány megállapította, hogy:

„Miközben nincs nagy lehetősége az öröklődést lecserélni a kompozícióval, mégis jelentős lenne.”

(Az öröklődés használatának csupán 2%-a belső és további 22% kizárólag külső vagy belső újrafelhasználásra van használva.)

Az eredményeink azt sugallják, nincs szükség az öröklődés támadására (legalábbis a nyílt forráskódú Java-szoftverekben), de mégis felmerül a kompozíció vs. öröklődés kérdése. Ha jelentős költség az öröklődés számlájára írható fel, ott ahol kompozíciót lehetne használni, az aggodalomra ad okot.[13]

Jegyzetek

  1. a b Freeman, Eric. Head First Design Patterns. O'Reilly, 23. o. (2004). ISBN 978-0-596-00712-6 
  2. http://info.berzsenyi.hu/programozas/feladatok/gyemant-oeroeklodes
  3. Pike: Less is exponentially more, 2012. június 25. (Hozzáférés: 2016. október 1.)
  4. http://projectlombok.org/
  5. @Delegate. Project Lombok. (Hozzáférés: 2018. július 11.)
  6. https://github.com/MikeInnes/Lazy.jl
  7. https://github.com/JeffreySarnoff/TypedDelegation.jl
  8. Protocols. The Swift Programming Language. Apple Inc. (Hozzáférés: 2018. július 11.)
  9. Delegated Properties. Kotlin Reference. JetBrains. (Hozzáférés: 2018. július 11.)
  10. '(Type) Embedding. The Go Programming Language Documentation. Google. (Hozzáférés: 2019. május 10.)
  11. Alias This. D Language Reference. (Hozzáférés: 2019. június 15.)
  12. What's new in C# 8.0. Microsoft Docs. Microsoft. (Hozzáférés: 2019. február 20.)
  13. (2013) „What programmers do with inheritance in Java”. ECOOP 2013–Object-Oriented Programming: 577–601. 

Fordítás

Ez a szócikk részben vagy egészben a Composition over inheritance című angol Wikipédia-szócikk ezen változatának fordításán alapul. Az eredeti cikk szerkesztőit annak laptörténete sorolja fel. Ez a jelzés csupán a megfogalmazás eredetét és a szerzői jogokat jelzi, nem szolgál a cikkben szereplő információk forrásmegjelöléseként.

Kapcsolódó szócikkek