25. Tervezési minták

Ebben a fejezetben alapos elméleti bevezetést nem tudunk adni, csupán néhány egyszerű példát áttekinteni van lehetőségünk. A téma sokkal alaposabb megértéséhez Erich Gamma, Ralph Johnson, Richard Helm, John Vlissides: Programtervezési minták című könyvét érdemes elővenni. (Letenni az OOP iránt érdeklődők úgysem tudják :)

Mi a tervezési minta?

Ha egy feladat újra előkerül a fejlesztés folyamán, akkor valószínűleg a megoldás hasonló lesz a korábbihoz. A tervezési minták olyan objektumközpontú megoldásokat jelentenek, amelyek már bizonyítottak a gyakorlatban. Ezek felhasználása rugalmasabban módosítható és könnyebben, jobban újrahasznosítható alkalmazásokat készíthetünk.

A minták leírására egységes módszereket szokás alkalmazni. Ez lényegét tekintve a következőket tartalmazza:

  • Motiváció: mi volt az az eredeti probléma, ami miatt a téma előkerült.
  • A résztvevők és a struktúra leírása.
  • A használat feltételei: meddig terjed a minta alkalmazhatósága.
  • További alkalmazási példák.

E jegyzetben – terjedelmi okokból – csupán egy kevésbé formális, az érdeklődés felkeltésére szolgáló bevezetőt tudunk nyújtani.

Bevezetésként még következzen egy áttekintő táblázat:

Cél
Létrehozási Szerkezeti Viselkedési
Hatókör Osztály Gyártófüggvény (Osztály)illesztő Értelmező
Sablonfüggvény
Objektum Elvont gyár
Építő
Prototípus
Egyke
(Objektum)illesztő
Híd
Összetétel
Díszítő
Homlokzat
Pehelysúlyú
Helyettes
Felelősséglánc
Parancs
Bejáró
Közvetítő
Emlékeztető
Megfigyelő
Állapot
Stratégia
Látogató

25.1. Létrehozási minták

A létrehozási mintáknak az a céljuk, hogy az egyes objektumpéldányok létrehozásakor speciális igényeknek is eleget tudjunk tenni.

Az Egyke (Singleton) minta például lehetővé teszi, hogy egy osztályból csak egyetlen példányt lehessen létrehozni.

Az Elvont gyár (Abstract Factory), Építő (Builder) és Gyártófüggvény (Factory Method) minták szintén a példányosítást támogatják, amikor nem akarjuk, vagy nem tudjuk, milyen típusú is legyen az adott példány.

25.1.1 Egyke (Singleton)

Az Egyke minta akkor hasznos, ha egy osztályból csak egyetlen példány létrehozását szeretnénk engedélyezni. A módszer lényege, hogy kontrolláljuk a példányosítást egy privát konstruktor létrehozásával. Az osztályból csak egyetlen példányt hozunk létre, azt is csak szükség (az első kérés) esetén. A példány elérése egy statikus gyártó metóduson keresztül történik.

Nézzünk először egy általános példát:

class Singleton {
  private static Singleton instance = null;
  private Singleton() { }
  static public Singleton instance() {
    if (instance == null) {
      instance = new Singleton();
    }
    return instance;
  }
  public void finalize() {
    instance = null;
  }
}

Példányt létrehozni a következő módon tudunk:

Singleton one = Singleton.instance();

Ha egy újabb példányt kérünk, akkor is ugyanazt az objektumot kapjuk. Hibás lenne viszont, ha közvetlenül próbálnánk példányosítani:

Singleton one = new Singleton(); // hibás

Egy kicsit más megoldást láthatunk egy távoli kapcsolat kiépítését megvalósító osztálynál. Itt a kapcsolat mindig szükséges, a duplikálás elkerülése a fő célunk.

Megjegyzés: Érdemes belegondolni, milyen zavarokkal járna, ha a programozó figyelmetlensége miatt egyszerre két vagy több távoli kapcsolatot próbálnánk kiépíteni és használni ugyanazon erőforrás felé.

final class RemoteConnection {
  private Connect con;
  private static RemoteConnection rc =
      new RemoteConnection(connection);
  private RemoteConnection(Connect c) {
    con = c;
    ....
  }
  public static RemoteConnection getRemoteConnection() {
    return rc;
  }
  public void setConnection(Connect c) {
    this(c);
  }
}

Szerzői megjegyzés: További minták bemutatása tervben van.

25.1.2 Gyártófüggvény (Factory Method) minta

A gyártófüggvény minta az egyik legtöbbet alkalmazott tervezési minta. A minta célja egy objektum létrehozása különböző információk alapján.

A következő ábrán a bemeneti információt az abc jelképezi. Ez alapján a gyártófüggvény az x ősosztály valamelyik leszármazottját (xy vagy xz) fogja példányosítani. A getClass hívására létrejövő objektum tényleges típusáról többnyire nem is kell tudnia a felhasználónak.

Gyártófüggvény

Nézzünk egy konkrét példát. A feladatunk az, hogy a nevek kezelése körüli következő problémát megoldjuk. Az angol nyelvben kétféle módon is megadhatjuk ugyanazt a nevet:

  • Jack London
  • London, Jack

Ha egy beviteli mezőben a felhasználó megadja a nevét, akkor bármelyiket használja. Nekünk az a feladatunk, hogy a név alapján egy olyan objektumot hozzuk létre, amelyikből bármikor elérhetők a szükséges szolgáltatások.

Először nézzük meg azt az ősosztályt, amelynek a szolgáltatásaira végső soron szükségünk lesz:

abstract class Namer {
    protected String last;
    protected String first;
    public String getFirst() {
        return first;
    }
    public String getLast() {
        return last;
    }
}

Nézzük meg a két igen egyszerű leszármazott osztályt is.

Az első leszármazottunk a név megadásánál az első ( szóközös) megadást feltételezi.

class FirstFirst extends Namer {
    public FirstFirst(String s) {
        int i = s.lastIndexOf(" ");
        if (i > 0) {
            first = s.substring(0, i).trim();
            last =s.substring(i+1).trim();
        } else {
            first = "";
            last = s;
        }
    }
}

A másik leszármazott a vessző karaktert keresi elválasztóként:

class LastFirst extends Namer {
    public LastFirst(String s) {
        int i = s.indexOf(",");
        if (i > 0) {
            last = s.substring(0, i).trim();
            first = s.substring(i + 1).trim();
        } else {
            last = s;
            first = "";
        }
    }
}

A tényleges példányosítást (gyártást) a következő osztály végzi:

class NameFactory {
    public Namer getNamer(String entry) {
        int i = entry.indexOf(",");
        if (i>0)
            return new LastFirst(entry);
        else
            return new FirstFirst(entry);
    }
}

Elegendő a getNamer metódust a nevet tartalmazó String paraméterrel meghívni, eredményül pedig egy Namer (leszármazott) objektumot kapunk.

Megjegyezés: A példa láttán felmerülhet az a kifogás, hogy elegendő lett volna a Namer osztály konstruktorában ezt a kétféle inputot megkülönbözteti. Ennél a példánál tényleg járható lenne ez az út is.

A példa egyszerűsége abban rejlik, hogy a leszármazottak csak a konstruktorunkban térnek el egymástól. Más összetettebb szituáció esetén a leszármazottak érdemi működése is jelentősen eltérhet egymástól.

Legerősebb érvként pedig azt érdemes meggondolni, hogy ha a bemutatott struktúrát kell bővítenünk egy újfajta viselkedéssel, akkor elegendő egy új leszármazott osztály létrehozása és a getNamer metódus bővítése, a többi osztályhoz egyáltalán nem kell hozzányúlni. Ez egy nagyobb alkalmazás esetén nagyon erőteljes érv lehet.

25.2. Szerkezeti minták

A szerkezeti minták segítségével előírhatjuk, hogy az egyes osztályokból vagy objektumokból hogyan álljon elő egy komplexebb struktúra.

Az osztály minták célja, hogy olyan öröklési hierarchiát alakítsunk ki, amelyik jól használható programfelületet nyújt. Ezzel szemben az objektum minták az objektumok összeillesztésének célszerű módjait alkalmazzák.

Az Illesztő (Adapter) mintára akkor van szükség, ha különböző felületű osztályoknak kell kapcsolatba hozni. Így tulajdonképpen a két félnek nem is kell egymásról konkrétan tudni, elég, ha a köztük lévő illeszti mindkét felet.

A Híd (Bridge) minta ezzel szemben nem kényszerhelyzet, hanem tudatos tervezés miatt ad valami más felületet. A szolgáltatás felületét (interfészét) és megvalósítását (implementációját) tudatosan választja szét.

A Homlokzat (Facade) mintával egy nagyobb komponensnek egységes felületet (interfészt) tudunk nyújtani.

A Pehelysúlyú (Flyweight) minta lehetőséget ad arra, hogy elrejtsünk egy objektumot, és azt csak akkor hozzuk elő, ha arra tényleg szükség lesz.

25.3. Viselkedési minták

A viselkedési minták (a szerkezeti mintákkal szemben) nem az állandó kapcsolatra, hanem az objektumok közötti kommunikációra adnak hatékony megoldást.

A Megfigyelő (Observer) minta célja, hogy egy objektum állapotváltozásainak figyelését lehetővé tegye tetszőleges más objektumok számára. A figyelő pozícióba feliratkozással juthatunk, de a leiratkozás is bármikor megejthető.

A Közvetítő (Mediator) minta célja, hogy két – egymással kommunikálni képtelen osztály között közvetítő szerepet töltsön be. Érdekesség, hogy a két közvetve kommunikáló osztálynak semmit sem kell egymásról tudnia.

A Felelősséglánc (Chain of Responsibility) minta az objektumok közül megkeresi a felelőst. Egyszerű példaként el lehet képzelni egy böngésző alkalmazás ablakát, ahol a felület tulajdonképpen többszörösen egymásba ágyazott komponensek segítségével épül fel. Egy egérkattintás esetén a főablak-objektumból kiindulva (a vizuális tartalmazás mentén) egyre pontosabban meg tudjuk határozni, hogy a kattintás melyik doboz, melyik bekezdés, melyik űrlap, melyik űrlap-elem stb. területén történt.

A Stratégia (Strategy) minta egy algoritmust egy osztályba zár. Így az algoritmus későbbi leváltása csak az ős egy másik leszármazottját fogja igényelni.

Végül a Bejáró (Iterator) minta már ismerős lehet a Java Iterator interfésze miatt: feladata a bejárás biztosítása pl. tároló objektumokon.