• email
  • facebook
  • linkedin
  • google+
  • pinteres

A Singleton pattern

Kevés olyan tervezési mintákról szóló ismertető van, amely nem a Singletonnal kezdi a design patternek bemutatását. Ettől a hagyománytól én sem szeretnék elszakadni. Állásinterjúkon sem ritka, hogy elhangzik a kérdés: Mik azok a design patternek? Be tudna egyet mutatni? Ilyenkor a leggyakoribb példa szintén a Singleton. Rengeteg lerás található erről a patternről, köztük némelyik egészen kiváló, sőt magyarul is olvasható több, áttekintőbb jellegű vagy épp részletesebb ismertető. ... Mert mint látni fogjuk: a legyegyszerűbb pattern esete mégsem olyan egyszerűss.

UML: DataBase

Java (vagy más, objektum orientált nyelvű) alkalmazások fejlesztése során előfordul, hogy szükségünk van bizonyos (JVM/Classloader szinten) globálisan elérhető, mindig állandó változókra vagy interfészekre (kapcsolódási pontra külső erőforrásokhoz). Amikor a kis demót készítettem, éppen ilyen probléma volt az adatbázis interfész biztosítása. Ahogy a GoF fogalmaz (127. o.):

Intent: Ensure a class has only one instance, and provide a global point of access to it.

Az első reflex lehet, hogy ezt statikus osztály segítségével biztosítsuk. A statikus versus singleton vita kapcsán én igazából egyetlen, igazából esztétikai, mégis döntő érvet találtam eddig: objektum orientáltan programozunk, így ha következetesek akarunk lenni, legyen minden objektum, még ha egyetlen példányunk lesz is belőle mindig. Ha tehát szépen akarjuk a feladatot megoldani, akkor kell egy objektum, aminek az esetében biztosítanunk kell, hogy egyetlen példány lehessen csak belőle, és az bárhonnan elérhető legyen.

Az egyetlenség garantálásához az első lépés, hogy letiltsuk a konstruktor általi példányosítás lehetőségét, amit úgy tehetünk meg, ha private-ra vesszük a láthatóságát:

/** Interfész az adatbázishoz. Singleton osztály */
public class DataBase {

private DataBase() {
    ...
}

A második, amit gyakran elfelejtenek megemlíteni/megtenni: illik a clone metódust letiltani, nehogy ott megmaradjon a másolat létrehozásának lehetősége:

/* Klónozást nem engedjük! */
public Object clone() throws CloneNotSupportedException {
    throw new CloneNotSupportedException();
}

Már itt egy problémába futunk: ha private lesz a konstruktor az osztályt nem fogjuk tudni leszármaztatni. Ez sok esetben elfogadható, néha viszont nem jó megoldás. Ilyenkor protected-re módosíthatjuk a konstruktor láthatóságát, csak ez esetben gondoskodnunk kell róla, hogy külön csomagba is rakjuk az osztályt, különben az azonos csomagban lévő osztályokból példányosítható marad.

Ha mindebben megnyugtató megoldást találtunk, a következő kérdés az lesz: hogyan jön majd mégis létre példány az osztályunkból, illetve hogyan juthatunk ahhoz hozzá.

A private kulcsszó után sejthetjük: magát kell példányosítsa az osztály -- hozzunk létre tehát egy ilyen factory metódust (hagyományosan getInstance() néven:

/* Statikus változó a példány tárolására */
private static DataBase instance;

/* Factory metódus a példány elérésére */
public static DataBase getInstance() {

    // Létre kell hoznunk, ha még nem volt:
    if (instance == null)
        instance = new DataBase();

    return instance;
}

A fenti metódus statikusan lesz hívható, s amennyiben nincs még példányosítva az osztályunk, megteszi, majd visszaadja a példányt. Egyetlen probléma van ezzel a kóddal: nem szálbiztos (népszerűbb kifejezéssel: nem thread safe). Mit tehetünk ez ellen? Hozzáadhatjuk a metódus definíciójához a synchronized kulcsszót.

private static DataBase instance;

public synchronized static DataBase getInstance() {

    if (instance == null)
        instance = new DataBase();

    return instance;
}

Ezzel rendben is lehetnénk, csak hogy eszünkbe jut: a metódus ezzel sokkal lassabbá válik, így teljesítményben kell megfizessük a szálbiztosságot. Ez néha nem lesz elfogadható, s ilyenkor más megoldást kell keresnünk. Erre van is lehetőség:

/* Statikus konstans a példány tárolására */
private static final DataBase INSTANCE = new DataBase();

public static DataBase getInstance() {
    return INSTANCE;
}

Ha a példányt osztály szintű konstansként definiáljuk, akkor a szálbiztosság megmarad. Vgyáznunk kell rá, hogy a konstans definíció az utolsó legyen a statikus definíciók között, különben előfordulhat, hogy a konstruktorban még nem inicializált statikus konstansra/változóra hivatkozunk, és ez hibákhoz vezethez. (Hogy hogyan, erről itt olvasható egy remek kis írás.) Az utolsó megoldás emellett két kompromisszumot hozott: egyrész el kell fogadnunk, hogy a példány feleslegesen is létrejöhet, nem csak amikor elkérjük, azaz szükség van rá, illetve, ami tényleg lehet probléma: a példányt ezek után nem hozhatjuk újra létre. Ha újrapéldányosításokra lenne szükség, akkor sajnos újra belefutunk szálak közötti szinkronizálás már említett problémáiba.

A singletonokkal kapcsolatos problémák között meg kell még említeni, hogy ha szerializálni szeretnénk a singletonunkat, előfordulhat, hogy többször is deszerializáljuk. Ezt elkerülendő a readResolve() metódust is implementálnunk kell:

private Object readResolve() {
    return INSTANCE;
}

További lehetőség, hogy egyelemű Enum-ot használunk a singleton implementálására, amely már szerializálható és garantáltan egyetlen lesz belőle. Működik, bár sokaknak valószínűleg inkább tűnik hacknek mint szép megoldásnak.

public enum DataBase {
    INSTANCE

    //metódusok
    ...

}

Összetettebb, több singletont használó alkalmazás esetén érdemes lehet egy singleton factory/registry készítése -- mondjuk a singletonjainkkal egy csomagba -- , amely a példányosításokért felel. Itt azonban további hibalehetőségek nyílnak, fokozottan óvatosnak kell lennünk.

Mint láthatjuk, megtévesztő a Singleton pattern egyszerűsége: ha mindenképpen szeretnénk garantálni, hogy csak egyetlen példány jöhessen létre, és a szálbiztosság is szempont, akkor több dologra is oda kell figyelnünk, s előfordulhat, hogy például teljesítménybeli kompromisszumokra kényszerülünk.

Hogy mennyire is hasznosak, sőt, szükségesek a singleton objektumok, azt az is mutatja, hogy a Java EE 6 keretében készülő EJB 3.1 specifikáció része lett a singleton beanekre vonatkozó javaslat. A @Singleton annotációval megjelölt osztály a teljes alkalmazás-rétegre nézve globális adatok kezelését teszi lehetővé, úgy, hogy itt a pédlányosítás, szálbiztosság és az egyediség garantálásának felelősségét is átveszi az EJB konténer. Singleton beanjeink emellett ki tudják majd használni a konténer egyéb szolgáltatásait, mint például a tranzakciókezelés, életciklus menedzselés vagy a perzisztenciakezelés.