Loovmustrid
Singel (Singleton)
Singli korral on klassist võimalik luua ainult üks objekt. Klassi konstruktor on privaatne ja uut objekti läbi konstruktori pole võimalik luua. Singli objekti kasutamiseks luuakse selleks staatiline avalik meetod, mille kaudu kontrollitakse, et alati on klassist ainult üks objekt.
Singleton'i võib olla mõttekas kasutada, kui erinevates lähtekoodi osades on vaja ligi pääseda ühetedele ja samadele andmetele (ingl. k. shared global state) või funktsionaalsustele, mis objektiti kunagi ei erine. Niiviisi saab vältida ühe-sama objekti koodis ringiratast vedamist.
Tuleb tähele panna, et lihtsalt arusaadava ning hõlpsalt edasiarendatava koodi kirjutamise üheks põhimõteteks on just globaalselt kättesaadavate andmete minimeerimine - seepärast peab enne singleton'i mustri kasutamist hoolega mõtlema, kas mustri kasutamine on olukorras õigustatud.
Singleton'i mustri antiteesiks on sõltuvuste süstimine, mille rakendamisega kaasneb hulk eeliseid. Üldlevinud kaasaegne arvamus on see, et singleton'i mustrit peaks vältima - eriti arvestades seda, kui ergonoomiline on rakendada sõltuvuste süstimist raamistike abiga.
Singleton'i on mitu erinevat tüüpi, igal ühel omad plussid ja omad miinused:
Agar laadimine (Eager initialization)
Agar laadimisega luuakse Singleton'i objekt programmi käivitamise hetkel, väga kerge implementeerida. Mõistlik kasutada kui antud klass on koguaeg kasutuses ning ei kasuta liiga palju ressursse. Kui loomisel peaks mingi erind sisse tulema, siis antud viisiga pole võimalik seda käsitleda.
Luuakse järgnevalt:
public class EagerSingleton {
private static final EagerSingleton instance = new EagerSingleton();
private EagerSingleton() {} // private constructor to avoid re-initialization
public static EagerSingleton getInstance() {
return instance;
}
}
Ning väljaspool klassi pääsetakse ligi järgnevalt:
EagerSingleton.getInstance().someMethod(); // Singleton already exists somewhere
Staatilise ploki laadimine (Static block initialization)
Staatilise ploki laadimine agar laadimise alaliik. Jagab agar laadimisega samu omadusi, kuid static {} ploki sees on võimalik erinditega tegeleda, kui neid peaks ette tulema. Puudub vajadus ka luua getInstance() meetod, kuna instance on public juurdepääsuga ehk sellele pääseb otse ligi.
Luuakse järgnevalt:
public class StaticBlockSingleton {
public static StaticBlockSingleton instance;
private StaticBlockSingleton() {} // private constructor to avoid re-initialization
static {
instance = new StaticBlockSingleton();
}
}
Ning väljaspool klassi pääsetakse ligi järgnevalt:
StaticBlockSingleton.instance.someMethod(); // Singleton already exists somewhere
Laisklaadimine (Lazy loading)
Singleton'i muster võimaldab hõlpsalt erinevates lähtekoodi osades pääseda ligi niiöelda laisalt laetud ressurssidele. Idee seisneb selles, et me ei proovi valmis seada mingit kulukat ressurssi enne, kui seda reaalselt kasutama hakatakse.
- Need ressursid võivad olla näiteks:
ajaliselt nõudlike arvutuste tulemused
failist mällu laetud andmed
andmebaasiühendused
muude seadmetega võrguühenduse loomine
üle võrgu saadud päringute vastused (nt. API päringud)
palju muud...
- Eelised:
Kui on mitu kulukat ressurssi, väldime kõikide laadimist ühel ja samal ajal programmi alguses
Kui tuleb välja, et ressurssi polnud programmi eluaja jooksul vaja, väldime tühja tööd
Luuakse järgnevalt:
public class LazySingleton {
public static LazySingleton instance;
private LazySingleton() {} // private constructor to avoid re-initialization
public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
Ning väljaspool klassi pääsetakse ligi järgnevalt:
LazySingleton.getInstance().someMethod(); // Creates instance of singleton on first time this is called
Antud singleton võib ebakorrektselt töötada mitmelõimelises keskkonnas. Järgnev singletoni muster parandab antud vea.
Thread Safe Singleton
Thread Safe Singleton-i on võimalik kasutada edukamalt mitmelõimelises programmis. Lisades getInstance() meetodile synchronised juurde, teeme kindlaks, et erinevad lõimed ei saa samale meetodile korraga ligi, vaid ootavad oma korda. Miinuseks on see, et on aeglasem laisklaadimisest.
Luuakse järgnevalt:
public class ThreadSafeSingleton {
public static ThreadSafeSingleton instance;
private ThreadSafeSingleton() {} // private constructor to avoid re-initialization
synchronised public static ThreadSafeSingleton getInstance() {
if (instance == null) {
instance = new ThreadSafeSingleton();
}
return instance;
}
}
Ning väljaspool klassi pääsetakse ligi järgnevalt:
ThreadSafeSingleton.getInstance().someMethod(); // Creates instance of singleton on first time this is called
Ehitaja (Builder)
Note
Builder muster järgib fluent interface'ide põhimõtteid ning on näide meetodite aheldamisest
Builder mustri mõte on muuta see kood:
var property = new Property(
4200,
"Luxury Apartment",
"Stunning waterfront apartment with spectacular views",
500000,
3,
2,
185.5,
"Tallinn",
"Sadama 1"
);
Selliseks:
var property = new Property.Builder()
.withId(4200)
.withTitle("Luxury Apartment")
.withDescription("Stunning waterfront apartment with spectacular views")
.withPrice(500000)
.withSquareMeters()
.withBathrooms(2)
.withBedrooms(3)
.withCity("Tallinn")
.withAddress("Sadama 1")
.build();
Selline objekti loomine kindlustab, et koodi lugeja saab aru, mis konstruktorisse sisse antud argumentide tähtsus on - iga arvu ja sõne mõte on silmapilkselt mõistetav.
Kas oskaksid muidu arvata, mida tähendavad väärtused 4200
, 3
ja 2
esimeses näites?
Note
Olulisel määral saab konstrueerimisloogikat (ja ka palju muud) teha selgemaks domeeni modelleerimist õigesti rakendades. Näiteks saab konstruktorisse anda:
Warning
Mõõteühikute ja valuutadega ümberkäimine on omaette jäneseurg, ning selleks peaks kindlasti kasutama mõnda olemasolevat teeki
Valikulised väljad
Builder muster võimaldab jätta välju vajadusel null
väärtuse peale, ilma seda eraldi konstruktoris välja kirjutamata - võrdle:
var person = new Person(
"John",
"Doe",
null, // birthday?
null // address?
);
var person = new Person.Builder()
.withFirstName("John")
.withLastName("Doe")
.build();
Selline kood ei ole ilmtingimata loetavam, kuna meelega välja jäetud asju on raske märgata,
ent võib olla hädavajalik, kui klassil on palju potentsiaalselt null
välju (sellist olukorda võiks aga eeskätt vältida). null
väärtuse asemel on soovitatav kasutada Optional
wrapper'it, mis annab arendajale märku, et teatud väli ei pruugi väärtustatud olla.
Klassi asukoht
Builder võib olla kas eraldi klassina, nt. PersonBuilder
, mispuhul näeks kutsumine välja selline:
var person = PersonBuilder()
// invoke builder methods
.build();
Või staatilise seesmise klassina, mispuhul näeb kutsumine välja selline:
var person2 = new Person.Builder()
// invoke builder methods
.build();
Variandid on samaväärsed - emba-kumba kasutamine on maitse asi.
Implementatsioon
Niiviisi võib implementeerida builder'it seesmise klassina:
public final class Person { // <- the class for which we want to build an object
private final String firstName;
private final String lastName;
public Person(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public static class Builder { // <- the class doing the building
private String firstName;
private String lastName;
public Builder withFirstName(String firstName) {
this.firstName = firstName;
return this;
}
public Builder withLastName(String lastName) {
this.lastName = lastName;
return this;
}
public Person build() {
return new Person(firstName, lastName);
}
}
}
Põhimõte on lihtne:
Builder'il on samad väljad mis ka ehitataval objektil
Pärast iga
withX
meetodi väljakutsumist seame vastava väljaX
väärtuse builder'il, et väärtused meelde jättaPärast iga
withX
meetodi väljakutsumist tagastame selle sama builder objekti, et võimaldadawithY
meetodite aheldamistKui kasutaja lõpuks
build
meetodit kutsub, tagastamePerson
klassi konstruktori abil uuePerson
objekti, kasutades juba builder'i väljades salvestatud väärtusi
Builderi loomine automaatselt IntelliJs
IntelliJ IDE-ga on võimalik olemasolevas koodis asendada konstruktori väljakutsumine Builder klassiga.
Vali kursoriga koodis koht, kus kutsutakse välja konstruktor.
Vali menüüs Refactor -> Replace Constructor with Builder
Avatud aknast vali sobivad seaded Builder klassi loomiseks.
Tehase meetodid (Static factory methods)
Tehas on üldine muster, mis olemuselt tähendab seda, et objekti konstrueerimise (instantiation) loogika tõstetakse eraldi klassi. Siin peatükis vaatleme vaid static factory method mustrit.
Eelised:
Meetod võib tagastada lisaks konkreetsele tüübile ka selle alamtüüpi objekte
Meetodisse saab kapseldada kogu keerulise objekti loomise loogika. Kui näiteks objekti loomisel on vaja rakendada keerukad loogikat, on see mõistlik lisada eraldi loomise meetodisse (mitte konstruktorisse) - konstruktor jätta vaid objekti loomiseks
Meetodile on võimalik lisada sisukas nimi. Konstruktori nimi on fikseeritud (näiteks
Product
), aga tehase meetodile võib anda näiteks nimecreateProductWithPrice
võicreateSaleProductWithDiscount
. Need mõlemad võivad lõpuks välja kutsudaProduct
konstruktorit, aga objekti loomisel ette antavad parameetrid/loogika võib olla erinev.Programmi loomulik voog ning loetavus ei lämbu konstrueerimisloogikasse
`new
võtmesõna kasutamine viitab konkreetsetele implementatsioonidele. Factory mustrit kasutades järgime liidestele programmeerimise põhimõtet, mille tulemusel on kood paindlikum.
Näide
Tehase meetodite nimetused võimaldavad selgemalt väljendada seda, mida luuakse, võrreldes konstruktoriga, kus nimetamine on piiratud. Antud näites on nendeks meetoditeks createWithLoggedInstantiationTime ja createWithDefaultCountry. Need meetodid võtavad mõlemad sisendiks nime ning valikuliselt ka riigi. Nende põhjal luuakse uus User objekt.
public class User {
private static final Logger LOGGER = Logger.getLogger(User.class.getName());
private final String name;
private final String country;
public User(String name, String country) {
this.name = name;
this.country = country;
}
public static User createWithLoggedInstantiationTime(String name, String country) {
setLoggerProperties();
LOGGER.log(INFO, "User created at : {0}", LocalTime.now());
return new User(name, country);
}
public static User createWithDefaultCountry(String name) {
return new User(name, "Poland");
}
private static void setLoggerProperties() {
LOGGER.setLevel(ALL);
}
public static void main(String[] args) {
User tim = createWithDefaultCountry("Tim");
User tom = createWithLoggedInstantiationTime("Tom", "Germany");
}
}
Näiteid tehasemeetoditest Javas endas oleksid näiteks Optional
klassi of, ofNullable ja empty meetodid, List.of meetodid, String.valueOf ja paljud muud.
Järgnev näide on Javast endast võetud, vaatame läbi, kuidas Optional
klassi ofNullable meetod töötab
Warning
NB: Järgnevas näites on kasutatud geneerilisi tüüpe. Kui sa pole antud teemaga tutvunud, leiab selle siit: Geneerilised tüübid
public final class Optional<T> { // We don't know yet what type will be in this Optional, using generics to determine it
private static final Optional<?> EMPTY = new Optional<>(null); // A common instance for empty Optional
private T value; // Value gets stored here
// Private constructor so it can only be instantiated by static factory methods
private Optional(T value) {
this.value = value;
}
// Static Factory Method, determines what to return based on input (if null, then return empty optional)
public static <T> Optional<T> ofNullable(T value) {
return value == null ? (Optional<T>) EMPTY : new Optional<>(value);
}
}