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:

  • 500000 asemel Money.of(500000, "EUR") (kasutades JSR 354 liidest järgivat teeki)

  • 180.5 asemel Quantities.getQuantity(180.5, SI.SQUARE_METRE) (kasutades JSR 385 liidest järgivat teeki)

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älja X väärtuse builder'il, et väärtused meelde jätta

  • Pärast iga withX meetodi väljakutsumist tagastame selle sama builder objekti, et võimaldada withY meetodite aheldamist

  • Kui kasutaja lõpuks build meetodit kutsub, tagastame Person klassi konstruktori abil uue Person 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.

  1. Vali kursoriga koodis koht, kus kutsutakse välja konstruktor.

  2. Vali menüüs Refactor -> Replace Constructor with Builder

  3. 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 nime createProductWithPrice või createSaleProductWithDiscount. Need mõlemad võivad lõpuks välja kutsuda Product 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);
    }
}