Przyszedł czas na pierwszy opisywany przeze mnie wzorzec projektowy, a mianowicie Singleton. Jest to kreacyjny wzorzec projektowy, którego założenia są bardzo proste. Nie mniej jednak używając tego wzorca dostajemy bardzo wygodne w użyciu narzędzie. Dlaczego? Zachęcam do przeczytania artykułu.
Spis Treści
Zbudujmy Wzorzec od początku
Zacznijmy może od przedstawienia samej idei i celu używania tego wzorca. Wzorzec ten ma na celu kontrolowanie aby w trakcie działania aplikacji powstał tylko jeden obiekt danej klasy. Programista nie musi się skupiać na tym, aby samemu tworzyć mechanizmy odwołujące się do tego samego obiektu, ponieważ klasa sama pilnuje aby nie zostało utworzone więcej obiektów.
Standardowo tworząc obiekt korzystamy z konstruktora danej klasy, który nawet jeśli go sami nie utworzymy to tworzony jest domyślnie. Zazwyczaj tworzenie obiektu wygląda następująco (wzorce będę opisywał na przykładzie języka Java):
MyClass object = new Myclass();
Uniemożliwienie tworzenia wielu obiektów rozpocznijmy od zabrania możliwości tworzenia ich w ogóle. Jak to zrobić? Wystarczy, że nie będziemy w stanie użyć konstruktora czyli zmienimy modyfikator dostępu z public na private. Wówczas nasza klasa MyClass będzie wyglądała w następujący sposób:
public class MyClass {
private static MyClass exampleObject;
private MyClass() {}
}
No dobra, ale po co mi klasa, z której nie można utworzyć w ogóle żadnego obiektu…. No cóż, teraz czas aby zaimplementować w naszej klasie bardzo ważny, podstawowy mechanizm tworzenia obiektu w oparciu o statyczną metodę.
public class MyClass {
private static MyClass exampleObject;
private MyClass() {}
public static MyClass getExampleObject(){
if (exampleObject == null) {
exampleObject = new MyClass();
}
return exampleObject;
}
}
Instrukcja warunkowa zapewnia nam, że jeśli obiekt został utworzony to metoda zwróci nam gotowy wcześniej utworzony obiekt, a jeśli nie został on utworzony to go stworzy. Aby skorzystać z tak utworzonej klasy należy ją wywołać poprzez metodę statyczną w następujący sposób:
MyClass object = Myclass.getExampleObject();
W tym momencie można już stwierdzić że wzorzec został zaimplementowany. Aby go przetestować zaimplementujemy jeszcze dodatkowe metody w naszej klasie inkrementujące liczbę, a następnie spróbujemy utworzyć kilka obiektów naszej klasy i wywołać na nich metodę inkrementowania liczby. Nasza klasa będzie prezentować się następująco:
public class MyClass {
private static MyClass exampleObject;
private int number;
private MyClass() {
number = 0;
}
public static MyClass getExampleObject() {
if (exampleObject == null) {
exampleObject = new MyClass();
}
return exampleObject;
}
public void incrementNumber() {
number++;
}
public int getNumber() {
return number;
}
}
A teraz skorzystajmy z utworzonej klasy w następujący sposób:
public class Main {
public static void main(String[] args) {
MyClass objectOne = MyClass.getExampleObject();
System.out.println("Wartosc z obiektu 1 wynosi " + objectOne.getNumber());
objectOne.incrementNumber();
System.out.println("Wartosc z obiektu 1 wynosi " + objectOne.getNumber());
MyClass objectTwo = MyClass.getExampleObject();
System.out.println("Wartosc z obiektu 2 wynosi " + objectTwo.getNumber());
objectTwo.incrementNumber();
System.out.println("Wartosc z obiektu 2 wynosi " + objectTwo.getNumber());
}
}
Program zwraca następujące wartości:
Wartosc z obiektu 1 wynosi 0
Wartosc z obiektu 1 wynosi 1
Wartosc z obiektu 2 wynosi 1
Wartosc z obiektu 2 wynosi 2
Pierwsze wywołanie obiektu 1 tworzy nam obiekt, a następnie metoda incerementNumber() wykonuje operację na liczbie. Próba utworzenia drugiego obiektu sprawia że nie dostajemy nowego obiektu z wyzerowaną wartością lecz przypisujemy do obiektu objectTwo tak naprawdę ten sam obiekt co w przypadku objectOne. Takie rozwiązanie daje możliwość łatwego używania tego samego obiektu w różnych miejscach w aplikacji co znacząco ułatwia tworzenie bardziej elastycznego oprogramowania.
Przykład Użycia
Jako przykład napisałem prostą konsolową grę, w której aplikacja generuje losową liczbę z ustalonego zakresu. Zadaniem użytkownika jest odgadnięcie tej liczby i wpisanie jej w terminal. Gra trwa póki użytkownik nie zgadnie wylosowanej liczby. Wówczas aplikacja wyświetla stosowny komunikat i podaje ile błędów zostało popełnionych zanim użytkownik poprawnie odgadł liczbę.
Pierwsza została utworzona klasa Zgadula. W niej zapisana jest cała logika aplikacji. Klasa wygląda w następujący sposób:
package com.company;
import java.util.Random;
public class Zgadula {
private final int randomNumber;
private boolean win;
//podaj minimalną i maksymalną liczbę z zakresu z którego ma być wylosowana liczba.
public Zgadula(int min, int max) {
win = false;
Random random = new Random();
randomNumber = random.nextInt(max - min + 1) + min;
}
//zwraca wylosowaną liczbę
public int getRandomNumber() {
return randomNumber;
}
public boolean isWin() {
return win;
}
//sprawdza czy podana liczba jest tą wylosowaną
public void checkNumber(int number) {
if (this.randomNumber == number) win = true;
else {
LicznikBledow licznik = LicznikBledow.getLicznik();
licznik.addMistake();
}
}
}
Konstruktor tej klasy przyjmuje dwie wartości: maksymalną i minimalną z zakresu z którego ma zostać wylosowana liczba, a następnie przypisuje te wartość do zmiennej randomNumber. Dwie kolejne metody to metody pomocnicze, natomiast zwróćmy uwage na ostatnią metodę czyli checkNumber.
Metoda ta sprawdza czy podana przez użytkownika liczba jest taka sama jak liczba randomNumber. Jeśli liczba zgadza się wówczas flaga win zostaje ustalona na wartośc true. W przeciwnym razie obiekt licznik wywołuje metodę addMistake(). Zwróćmy uwagę jak zadeklarowany jest obiekt licznik. Poprzez metodę statyczną klasy LicznikBledow. Zauważmy że obiekt tworzony jest dopiero wtedy gdy, użytkownik popełni błąd. Przejdźmy zatem do właśnie tej klasy LicznikBledow, a wygląda ona nastepująco:
package com.company;
public class LicznikBledow {
private static LicznikBledow licznik;
private int numberOfMistakes;
//prywatny konstruktor uniemożliwiający utworzenie nowego obiektu wprost
private LicznikBledow() {
numberOfMistakes = 0;
}
//metoda statyczna, która tworzy i kontroluje ilość obiektów i ogranicza je do 1
public static LicznikBledow getLicznik() {
if (licznik == null) {
licznik = new LicznikBledow();
}
return licznik;
}
//inkrementuje ilosc bledow
public void addMistake() {
numberOfMistakes++;
}
//zwraca ilosc bledow
public int getNumberOfMistakes() {
return numberOfMistakes;
}
}
Najważniejszy jest w tej klasie konstruktor, który jest prywatny oraz metoda statyczna getLicznik(), która kontroluje tworzenie i zwracanie obiektu licznik. Właśnie tutaj mamy zaimplementowany wzorzec singleton. Nie ma możliwości utworzenia więcej niż jednego licznika błędów (choć to nie do końca prawda ale o tym później). No dobrze, ale może nasunąć się pytanie po co używać tego wzorca projektowego skoro można by utworzyć zwykłą klasę LicznikBledow z publicznym konstruktorem skoro tylko raz odwołujemy się w klasie Zgadula do obiektu licznik i zadeklarować ją następująco:
package com.company;
public class LicznikZwykly {
private int numberOfMistakes;
//klasyczny publiczny konstruktor
public LicznikZwykly() {
numberOfMistakes = 0;
}
//inkrementuje ilosc bledow
public void addMistake() {
numberOfMistakes++;
}
//zwraca ilosc bledow
public int getNumberOfMistakes() {
return numberOfMistakes;
}
}
Taki kod jest krótszy, prostszy i wydaje się bardziej przejrzysty. Odpowiedź można dostrzec gdy zaimplementujemy naszego maina do utworzenia obiektu klasy Zgadula. Zatem do dzieła:
package com.company;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Zgadula zgadula = new Zgadula(2, 6);
Scanner scanner = new Scanner(System.in);
while (!zgadula.isWin()) {
System.out.println("Podaj liczbe: ");
zgadula.checkNumber(scanner.nextInt());
}
LicznikBledow licznik = LicznikBledow.getLicznik();
System.out.println("BRAWO! Szukana liczba to " + zgadula.getRandomNumber());
System.out.println("Popelniles " + licznik.getNumberOfMistakes() + " bledow.");
}
}
Po stworzeniu obiektu zgadula i scanner na potrzeby wczytywania danych do konsoli zapisano pętlę, która zostaje przerwana dopiero po poprawnym odgadnięciu wylosowanej liczby. I teraz zwrócmy uwagę na linię:
LicznikBledow licznik = LicznikBledow.getLicznik();
Wygląda to jak tworzenie nowego obiektu na podstawie klasy LicznikBledow poprzez metodę statyczną, ale tak naprawdę przypisujemy do wartości licznik już wcześniej utworzony w klasie Zgadula obiekt z zapisanymi w nim informacjami odnośnie popełnionej liczby błędów podczas gry. Przykładowa tura gry wygląda wówczas w konsoli nastepująco:
Podaj liczbe:
2
Podaj liczbe:
3
Podaj liczbe:
4
Podaj liczbe:
5
BRAWO! Szukana liczba to 5
Popelniles 3 bledow.
Zauważ w jak bardzo prosty sposób odnieśliśmy się do utworzonego gdzieś wcześniej obiektu na podstawie klasy LicznikBledow. W ten sposób można implementować kolejne klasy jak np. Menu tej aplikacji, gdzie można by wyświetlać np. ilość błędów z poprzednio rozgrywanej tury gry. W nowej klasie utworzylibyśmy nowy obiekt do którego tak naprawdę znów przypisalibyśmy wcześniej utworzony obiekt poprzez metodę statyczną. Ależ to proste i wygodne, prawda?
Podsumowanie
Wzorzec singleton mądrze używany potrafi znacząco umilić pracę programiście i uprościć kod aplikacji. W pewnym momencie wspomniałem o tym, że pomimo zastosowania mechanizmu, który uniemożliwia utworzenie większej ilości obiektów może do tego dojść. Może się tak stać gdy aplikacja jest wielowątkowa, wówczas jest ryzyko, że utworzymy 2 lub więcej singletonów jednocześnie. Istnieją sposoby żeby temu zapobiec, nie mniej jednak jest to temat na osobny artykuł, który być może kiedyś powstanie.
Mam nadzieję, że przybliżyłem możliwie dokładnie idee tego prostego lecz bardzo przydatnego wzorca i być może ktoś dozna olśnienia i przekona się do jego użycia w swojej aplikacji.