Singleton
Enne selle peatükiga alustamist tuleks arvestada ühe asjaga, mis antud mustriga kaasas käib. Nimelt singleton on GoF raamatu üks kõige vastuolulisemaid mustreid. Paljud kogenud arendajad peavad seda anti-pattern'iks ning soovitavad seda võimalusel vältida. Singleton'i probleemid on lahti seletatud antud peatüki lõpus. Loe need hoolikalt läbi enne, kui otsustad seda oma koodis kasutada.
Sissejuhatus
Vahest tuleb ette olukordi, kus mõnest klassist peaks täpselt üks instanss eksisteerima üle terve programmi eluea jooksul. Nendeks võivad näiteks olla konfiguratsioonid, vahemälu, logijad. Need on ressursid, mille mitmekordne olemasolu ei ole mõistlik ning mis peavad olema kättesaadavad igalt poolt.
Singleton muster tagab, et klassil on ainult üks instants, ning pakub sellele globaalse ligipääsupunkti.
Probleemist lähemalt
Toome näiteks ConfigurationManager klassi, mis loeb rakenduse seadistused käivitamisel failist programmi mällu.
Failist lugemine on suhteliselt kulukas protsess ning saadud seadistused peaksid olema kogu programmi ulatuses ühesugused.
Kui oleks kaks eraldi konfiguratsioonihaldurit, loetaks faili kaks korda ning tekiks oht vastuoludeks, kui ühte uuendatakse ja teist mitte.
Naiivne lähenemine oleks umbes „loo üks instants ja anna see kõikjale edasi“:
ConfigurationManager config = new ConfigurationManager("app.properties");
ServiceA serviceA = new ServiceA(config);
ServiceB serviceB = new ServiceB(config);
// ... and so on
See on tegelikult vägagi aktsepteeritav lahendus (ja täpselt see, mida sõltuvuste süstimine teeb). Kuid selle peatüki mõttes oletame, et soovime tagada - klassi tasemel - et teist konfiguratsioonihaldurit ei saaks kunagi luua, ükskõik mida kutsuv kood ka ei prooviks teha.
Just seda võimekust pakub singleton.
Põhimõte
Iga singleton'i implementatsioon koosneb kolmest osast:
- Privaatne konstruktor - et antud klassi ei saaks väljastpoolt
newkaudu luua. - Privaatne staatiline väli, mis hoiab ainsat instantsi.
- Avalik staatiline meetod (
getInstance()), mis eelmist tagastab.
public class ConfigManager {
private static ConfigManager instance; // <-- 2. static instance
private final Map<String, String> properties = new HashMap<>();
// 1. private constructor
private ConfigManager() {
// load configuration from disk
}
// 3. public static method
public static ConfigManager getInstance() {
if (instance == null) {
instance = new ConfigManager();
}
return instance;
}
public String get(String key) {
return properties.get(key);
}
}
See ei ole ainus viis kuidas singleton'i luua, kuid see on kõige enim levinud viis.
Antud viisi kutsutakse lazy initialization'iks,
sest instants luuakse alles siis, kui getInstance() esimest korda välja kutsutakse.
// Can be called globally from anywhere
// Actual ConfigManager gets initialized on first call - lazy initialization
String dbUrl = ConfigManager.getInstance().get("database.url");
Sellel on aga varjatud viga: kui kaks lõime kutsuvad getInstance() välja täpselt samal ajal, kui instance on veel null, võivad mõlemad läbida kontrolli ja luua oma instantsi.
Seega murdub „ühe instantsi“ garantii mitmelõimelises keskkonnas.
Selle parandamiseks on mitmeid viise, näiteks sünkroniseeritud meetodid, double-checked locking, holder-klassid. Kuid Javas on olemas ka palju lihtsam lahendus, mis väldib kogu seda lisakeerukust.
Enum singleton'ina
Java enum-tüüpidel on omadus, mis sobib singleton'i jaoks ideaalselt: JVM tagab, et iga konstant luuakse täpselt üks kord, laisalt ning tagab ka thread safety't iseenesest.
Kui arvestada juurde ka asjaolu, et enum-id on Javas täisväärtuslikud klassid,
siis on võimalik singleton'e luua suhteliselt lihtsalt järgneval viisil:
public enum EnumConfigManager {
INSTANCE;
private final Map<String, String> properties = new HashMap<>();
EnumConfigManager() {
// load configuration from disk
}
public String get(String key) {
return properties.get(key);
}
}
See ongi kogu singleton. Seda saab kasutada järgnevalt:
String dbUrl = EnumConfigManager.INSTANCE.get("database.url");
Sellel lähenemisel on mitmeid eeliseid võrreldes laiska initsialiseerimist kasutava variandiga:
- Iseloomult thread-safe, JVM haldab initsialiseerimist.
- Turvaline serialiseerimise vastu.
Enum-singleton'i ei saa serialiseerimise kaudu dubleerida. - Reflektsiooni suhtes turvaline. Privaatset
enum-konstruktorit ei saa reflektsiooni abil välja kutsuda, et luua teine instants. - Vähem koodi. Puudub
getInstance()meetod,null-kontroll ja sünkroniseerimine.
Raamatus Effective Java soovitab Joshua Bloch enum-lahendust kui parimat viisi singletoni realiseerimiseks Javas.
Peamine põhjus, miks seda vähem kasutatakse, on see, et see tundub ebatavaline -
enum oma loomult on mõeldud kui fikseeritud väärtuste loendina.
Miks singletoni peetakse anti-pattern’iks
Tuleme nüüd peatüki alguses oleva hoiatuse juurde tagasi.
1. Varjatud globaalne olek
Singleton on sisuliselt globaalne muutuja.
Iga kood, mis kutsub SomeSingleton.getInstance()-i, sõltub sellest, kuid see sõltuvus ei ole väljastpoolt nähtav.
Meetodi signatuur ei ütle midagi selle kohta, milliseid singleton’e see kasutab.
See muudab koodi raskemini loetavaks ja mõistetavaks.
2. Tugev sidusus
Kui klass kutsub otse Logger.getInstance()-i, on see igaveseks seotud just selle konkreetse Logger klassiga.
Teist implementatsiooni ei saa asendada ilma klassi ennast muutmata.
See on vastupidine sellele, mida sõltuvuste süstimine saavutada püüab.
3. Raske testida
Testid peaksid olema isoleeritud - iga test loob oma keskkonna, käivitub ja lõpetab ilma teisi teste mõjutamata. Singleton rikub seda põhimõtet, sest see üle elab kogu JVM-i eluea. Ühe testi poolt jäetud olek kandub järgmisse. Puudub puhas viis singleton'i lähtestamiseks testide vahel ning samuti pole lihtsat viisi selle asemel test-versiooni kasutada.
4. Elutsükkel on nähtamatu
Tavaliselt objekt luuakse, kasutatakse ja seejärel korjatakse prügikoristuse käigus kokku. Singleton'il puudub selge omanik - see lihtsalt eksisteerib - mistõttu on raske aru saada, millal seda on ohutu sulgeda, asendada või ümber seadistada.
5. „Üks instants“ ei pea tihti paika
Singleton garanteerib ühe instantsi classLoader'i kohta. Keskkondades, kus kasutatakse mitut classLoader'it (nt rakendusserverid, pluginate süsteemid, OSGi), võib tekkida mitu „singleton'i“, mis nullib mustri algse eesmärgi.
Mida teha selle asemel
Peaaegu igas olukorras, kus võiks tekkida kiusatus kasutada singletonit, annab sõltuvuste süstimine sama praktilise tulemuse ilma selle puudusteta:
- Loo instants üks kord rakenduse käivitamisel.
- Anna see konstruktorite kaudu edasi kõigile, kes seda vajavad.
- Testides kasuta teist instantsi (või mock’i).
Instants on endiselt sisuliselt ainulaadne, lihtsalt puudub klassitasemel range piirang, et see peab nii olema. Vastutasuks muutub kood testitavaks, sõltuvused nähtavaks ning objekti elutsükkel jääb selle koodi kontrolli alla, mis seda omab.
Millal võiks singleton olla õigustatud?
Peamiselt olekuta utiliitide puhul (nt Java java.util.logging.Logger)
või tõeliste globaalsete ressursside korral, kus alternatiiv on päriselt halvem.
Kuid vaikimisi eelista sõltuvuste süstimist ning käsitle singleton'i erandina, mitte reeglina.
Spring raamistikus on bean'id vaikimisi singleton'id.
Springi bean on tavaline klass tavalise konstruktoriga.
Raamistik lihtsalt loob ühe instantsi ja süstib selle kõikjale, kus seda vaja on.
Klass ise ei sunni midagi peale - puudub privaatne konstruktor, puudub getInstance()
ning testides saab lihtsalt luua uue instantsi (või mock’i) ja seda kasutada nagu iga teist sõltuvust.
„Ühe instantsi“ omadus tuleneb konteineri konfiguratsioonist, mitte klassist endast.
Ehk teisisõnu, Springi bean’id realiseerivad täpselt varem kirjeldatud "kasuta sõltuvuste süstimist" lähenemist.