Általános szoftver ismeretek
  A polimorfizmusról általában

Mi a polimorfizmus (többalakúság)?



A 'többalakúság' kifejezés valójában két dolgot is jelent:

  1. Egyazon objektum többfélének látszódhat.
  2. Több különböző fajta objektum ugyanolyannak látszódhat.

Ne ijedjen meg az Olvasó, nem optikai tananyagra tévedt böngészés közben – rögtön elmagyarázzuk, vagy, hogy stílszerűek legyünk: megvilágítjuk a fenti két, első ránézésre bizony rejtélyesnek tűnő kijelentést.

1.) Egyazon objektum többfélének látszódhat

A két legfontosabb eszköz ezen jelenség létrehozására
  1. az örököltetés a különféle objektumorientált nyelvekben és
  2. a Java nyelvben az interface.
Az örökléssel kapcsolatos a polimorfizmus talán legelterjedtebb használata, ezért itt kezdjük a vizsgálódásainkat.

1a.) Öröklés

Vegyünk egy Java nyelvű kis mintapéldát!

Adott egy Kutya nevű osztályunk, mely egyetlen tulajdonsággal bír: tud ugatni.

  class Kutya{
    public void Ugat(){
      System.out.println("Vau-vau!");
    }
  }

Ebből az osztályból örököltetünk egy Puli nevű osztályt, mely tud terelni is:

  class Puli extends Kutya{
    public void Terel(){
      System.out.println("Gyerünk, indulás, lusta banda!");
    }
  }

Tudjuk, hogy az öröklésnél olyan osztály jön létre, mely a szülő minden tulajdonságával automatikusan rendelkezik, most tehát igaz az, hogy a Puli tud ugatni is:

  Puli p = new Puli();

  p.Ugat();  // kimenet: Vau-vau!

Ezt úgy fogalmazhatjuk meg, hogy minden Puli egyfajta Kutya. És ez a kijelentés nemcsak valami magyarázó szöveg, ez olyannyira tényszerűen igaz, hogy a fordítóprogram is tudja – ami abban nyilvánul meg, hogy Puli típusú objektumra rámutathatunk Kutya típusú referenciával is:

  Puli  p = new Puli();
  Kutya k;

  k = p;    // Kutya típusú referenciával mutatunk a Pulira
  k.Ugat(); // a Kutya típusú referencián keresztül 'szólítjuk meg' a Pulit

Képzeljük el, hogy van egy Puli és egy Kutya feliratú táblánk is. Mindkettővel joggal rámutathatunk egy pulira:


hiszen a Puli valóban egyfajta Kutya.

És most érkezünk el oda, hogy a különböző referenciákon keresztül ugyanazon objektum másnak és másnak látszódhat.

  Puli  p = new Puli();
  Kutya k;

  k = p;      // Kutya típusú referenciával mutatunk a Pulira

  p.Ugat();   // rendben, a Puli tud ugatni
  p.Terel();  // rendben, a Puli tud terelni

  k.Ugat();   // rendben, a Kutya tud ugatni
  k.Terel();  // hiba! nem minden Kutya tud terelni!

Itt egyetlen objektumról van szó, melynek tényleges típusa Puli – ám ha Kutya típusú referencián keresztül szólítjuk meg, akkor csak azon tulajdonságait fogjuk látni, melyekkel a Kutya rendelkezik:


Tehát valóban igaz, hogy egyazon objektum többfélének tud látszani.

1b.) Java interfészek

A Java nyelvben az öröklésen kívül egy másik, hasonló lehetőségünk is van arra, hogy ugyanazon objektumot többféleképpen láthassuk: az interface. Alakítsuk át az előző példát úgy, hogy definiálunk egy UgatóValami interfészt, melyet a Puli osztály implementál:

  interface UgatóValami{
    public void Ugat();
  }

  class Puli implements UgatóValami{
    public void Ugat(){
      System.out.println("Vau-vau!");
    }

    public void Terel(){
      System.out.println("Gyerünk, indulás, lusta banda!");
    }
  }

Egy Puli típusú objektumra most is rámutathatunk UgatóValami típusú referenciával, mert igaz az, hogy a Puli egyfajta UgatóValami. Ezen a 'szemüvegen' át nézve persze csak az interfészhez tartozó metódusok látszanak:

  Puli        p = new Puli();
  UgatóValami u;

  u = p;     // UgatóValami típusú referenciával mutatunk a Pulira
  p.Ugat();
  p.Terel();

  u.Ugat();
  u.Terel(); // ez nem megy! nem minden UgatóValami tud terelni!!

Az UgatóValami referencián keresztül nem minden olyan metódust tudunk elérni, mellyel a konkrét objektum rendelkezik.


A hatás most is ugyanaz: egy objektum – többféle nézet. Úgy tűnik, mintha az objektumnak több alakja lenne.

Ráadásul egy osztály több interfészt is implementálhat. Amikor egy fizikai program adott helyiség hőmérsékletét számítja ki, akkor a kutya lehet ebből a szempontból egyszerűen egy hőforrás:

  interface UgatóValami{
    public void Ugat();
  }

  interface HőtermelőValami{
    public void HőtTermel();
  }

  class Puli implements UgatóValami, HőtermelőValami{
    public void Ugat(){
      System.out.println("Vau-vau!");
    }

    public void HőtTermel(){
      System.out.println("Kályhaként viselkedem!");
    }

    public void Terel(){
      System.out.println("Gyerünk, indulás, lusta banda!");
    }
  }

Szegény jobb sorsra érdemes ebet most háromféleképpen is láthatjuk:


és mindegyik minőségében más parancskészletre hallgat (nehéz a pulimorf kutyák élete!):

És most lássuk a fordított esetet: Amikor több különféle objektum bizonyos szempontból nézve nem különbözik egymástól.

2.) Több különböző fajta objektum ugyanolyannak látszódhat

Ennek a jelenségnek az lesz a következménye, amint látni fogjuk, hogy sokféle objektum lehet egyik oldalon és ezeket mind egyformának láthatjuk a másik oldalról – és ezáltal ezt az oldalt nem fogja érinteni az objektumok típusának változása.

A virtuális függvények és a sablonok teszik lehetővé ezt a hatást. A virtuális függvények lépnek működésbe a háttérben Kezdjük az ismerkedést most is az öröklésnél.

2a.) Öröklés

Ismét egy Java nyelvű példán kezdjük a bemutatást. Definiálunk egy általános Kutya osztályt, melynek egyetlen képessége van, mégpedig az, hogy tud ugatni:

  class Kutya{
    public void Ugat(){
      System.out.println("Csak úgy általánosan ugatok!");
    }
  }

Ebből az osztályból örököltetünk két kutyafajtát, melyek mindegyike másféleképp ugat:

  class Bernáthegyi extends Kutya{
    public void Ugat(){
      System.out.println("Mély hangon ugatok!");
    }
  }

  class Pincsi extends Kutya{
    public void Ugat(){
      System.out.println("Élesen vakkantgatok!");
    }
  }

Mivel mindkettő egyfajta Kutya, ezekre az objektumokra rámutathatunk Kutya típusú referenciával is:

  Bernáthegyi  b = new Bernáthegyi();
  Pincsi       p = new Pincsi();
  Kutya        k;

  k = b;    // Kutya típusú referenciával mutathatunk a Bernáthegyire
  k = p;    // vagy egy Pincsire is

Ezt eddig is tudtuk, idáig semmi új nincs a dologban.

Készítsünk most egy függvényt, amely Kutya típusú paramétert kap és azt megszólaltatja:

  static void Megszólaltat(Kutya Q){
    Q.Ugat();
  }

És most jön a lényeg: mivel a Bernáthegyi és Pincsi is egyfajta Kutya, ezért az ilyen típusú objektumokat is átadhatjuk paraméterként a függvénynek:

  Bernáthegyi b = new Bernáthegyi();
  Pincsi      p = new Pincsi();

  Megszólaltat(b);  // kimenet: Mély hangon ugatok!
  Megszólaltat(p);  // kimenet: Élesen vakkantgatok!

A Megszólaltat függvény belsejéből mindegyik paraméter egyszerűen csak Kutyának látszik, aminek óriási előnye van: A függvényben nem kell azzal törődnünk, hogy hogyan hajtja végre az Ugat() parancsot a konkrét objektum. Mindegyiknek adhatjuk ugyanazt az utasítást, melynek hatására más és más fog történni az objektum konkrét, aktuális típusától függően.

Vagyis: a függvény belsejéből nézve a valójában különböző fajta objektumok a működtetés szempontjából egyformának látszanak:


Ami ténylegesen a külvilágban egy bernáthegyi vagy egy pincsi, az a függvény belsejéből nézve egy általános kutya. De olyan különleges kutya ám ez, ami igazából pincsiként vagy bernáthegyiként viselkedik!

Egy álruhás kutya.

2b.) Java interfészek

Az
előző példát csak minimális mértékben fogjuk átalakítani. A Kutya ős-osztály helyett most egy UgatóValami nevű interfészt veszünk alapul:

  interface UgatóValami{
    public void Ugat();
  }

Létrehozunk két konkrét kutya-típust, melyek mindegyike másféleképp ugat:

  class Bernáthegyi implements UgatóValami{
    public void Ugat(){
      System.out.println("Mély hangon ugatok!");
    }
  }

  class Pincsi implements UgatóValami{
    public void Ugat(){
      System.out.println("Élesen vakkantgatok!");
    }
  }

Most is igaz az, hogy mivel mindkettő egyfajta UgatóValami, az ilyen típusú objektumokra rámutathatunk UgatóValami típusú referenciával:

  Bernáthegyi  b = new Bernáthegyi();
  Pincsi       p = new Pincsi();
  UgatóValami  u;

  u = b;    // UgatóValami típusú referenciával mutathatunk a Bernáthegyire
  u = p;    // vagy egy Pincsire is

Most pedig elkészítjük a Megszólaltat függvényt, melynek paramétere ezúttal – nyilván – UgatóValami típusú:

  static void Megszólaltat(UgatóValami U){
    U.Ugat();
  }

Természetesen most is átadhatunk a függvénynek paraméterként Bernáthegyi és Pincsi típusú objektumot is:

  Bernáthegyi b = new Bernáthegyi();
  Pincsi      p = new Pincsi();

  Megszólaltat(b);  // kimenet: Mély hangon ugatok!
  Megszólaltat(p);  // kimenet: Élesen vakkantgatok!

A Megszólaltat függvény belsejéből mindegyik paraméter egyszerűen csak ugatásra képes valaminek látszik. Most sem kell ezen a helyen azzal törődnünk, hogy valójában milyen objektum hajtja majd végre az Ugat() parancsot.

Megjegyzés: A Java nyelv óriási előnye, hogy az örököltetésen kívül használhatjuk az interface konstrukciót is. Így precízen ki tudjuk fejezni, hogy két osztály nem áll rokonsági kapcsolatban egymással, csak éppen van közös tulajdonságuk. Ha például fókákat is szerepeltetni akarunk a fenti példában – mivel ezek az állatok is ugató hangot adnak – akkor ezt is megtehetjük és nem vagyunk rákényszerítve, hogy valamilyen kutyafajtának tekintsük a fókát. Egyszerűen kijelentjük, hogy ő is rendelkezik az ugatás képességével, de egyéb köze nincs a kutyákhoz:

  class Fóka implements UgatóValami{
    public void Ugat(){
      System.out.println("Fóka-hangon ugatok!");
    }
  }

Ezáltal tömören és elegánsan le tudjuk írni azt a 'távoli rokonsági fokot', közös tulajdonsághalmazt, ami a legjobban tükrözi a valóságot.

2c.) Sablonok (generikus programozás)

A generikus programozással való ismerkedéshez a Java-nál sokkal alkalmasabb a C++ nyelv, azon belül is a függvénysablonok.

Tegyük fel, hogy úgy van megalkotva a bernáthegyit és a pincsit reprezentáló két osztály, hogy semmilyen öröklési rokonságban nincsenek egymással és ezen pillanatnyilag nem tudunk változtatni. Van viszont mindkettőnek HangotAd() metódusa:

  class Bernathegyi{
  public:
    void HangotAd(){
      std::cout << "Mély hangon ugatok!" << '\n';
    }
  
    void RumosHordotVisz();
    // és más metódusok...
  };
  
  class Pincsi{
  public:
    void HangotAd(){
      std::cout << "Élesen vakkantgatok!" << '\n';
    }
  
    void Hizeleg();
    // és más metódusok...
  };

Szeretnénk egy – és csak egyetlenegy – Megszolaltat() függvényt írni, melynek paraméterként átadhatjuk mindkét fajta objektumot és a függvény belsejében fel akarjuk hívni az Ugat() metódusukat (és csak azt).

Az eddig megismert módszerek nem jönnek szóba, mert két teljesen különálló típusról van szó. Készíthetünk viszont egy úgynevezett függvénysablont (function template):

  template <typename T>
  void Megszolaltat(T Param){
    Param.HangotAd();
  }

Ez a függvény-sablon egy típussal (T-vel) van paraméterezve. A paraméter deklarációja ezt mondja:

"Ide tetszőleges típusú paraméter kerülhet, feltéve, hogy rendelkezik minden olyan metódussal, melyet a függvény törzsében felhívok."

Ami most ebben a konkrét esetben azt jelenti, hogy van az illető osztálynak nyilvános HangotAd() metódusa.

Mivel mindkét osztályunknak van ilyen metódusa, átadhatjuk paraméterként a megfelelő objektumokat a Megszolaltat() függvénynek, azaz valóban megoldódott a problémánk:

  Bernathegyi b;
  Pincsi      p;

  Megszolaltat(b);  // kimenet: Mély hangon ugatok!
  Megszolaltat(p);  // kimenet: Élesen vakkantgatok!

A sablonoknak hasonló az előnye, mint a Java interface-nek: egymással semmilyen rokonsági kapcsolatban nem álló objektumokat egységesen tudunk kezelni. Ennek kihangsúlyozására bővítsük ki a fenti példát, hozzunk létre egy autót leíró osztályt, amely szintén tud hangot adni a maga módján:

  class Auto{
  public:
    void HangotAd(){
      std::cout << "Dudálok!" << '\n';
    }
    
    void Gurul();
    // és más metódusok...
  };

Ennek aztán végképp semmi más köze nincs a bernáthargyihez vagy a pincsihez, ennek ellenére ilyen objektumot is átadhatunk a Megszolaltat() függvénynek paraméterként:

  Bernathegyi b;
  Pincsi      p;
  Auto        a;

  Megszolaltat(b);  // kimenet: Mély hangon ugatok!
  Megszolaltat(p);  // kimenet: Élesen vakkantgatok!
  Megszolaltat(a);  // kimenet: Dudálok!



Itt csak az volt a cél, hogy megemlítsük a generikus programozást, részletesebben máshol fogunk foglalkozni vele. Ehelyütt mindössze egyetlen dologra szeretnénk hangsúlyosan felhívni a figyelmet:

A generikus programozás is a polimorfizmus megvalósításának egyik eszköze!

Mégpedig fontos és fontosságához mérten egyelőre nem eléggé általánosan alkalmazott eszköze. Ha megkérdezünk egy programozót, mi a polimorfizmus lényege, tíz esetből kilencszer csak az öröklés és a Java (esetleg VB és COM) interfészek fognak szerepelni a válaszában, azaz a dinamikus polimorfizmus különböző megvalósítási módjai.

Jelen sorok írója szeretne közreműködni abban, hogy ez az arány jó irányban változzon – és reméli, hogy az Olvasó lesz a következő egyed, akit meg tud nyerni céljának.


A polimorfizmusról általában
  Általános szoftver ismeretek