Design patterns reprezintă soluții generale, reutilizabile, pentru probleme des întâlnite în cadrul software design. Un design pattern este o schemă a unei soluții pentru o problemă de design (nu reprezintă un algoritm sau o bucată de cod care poate fi aplicată direct), ce poate ușura munca programatorului și poate duce la simplificarea și eficientizarea arhitecturii unei aplicații.
Există 3 tipuri de design patterns, în funcție de aspectul și de funcționalitatea obiectelor:
Uneori ne dorim să avem un obiect care să apară doar o singură dată într-o aplicație (de exemplu conducătorul unei țări). De aceea folosim Singleton, un mod prin care restricționăm numărul de instanțieri ale unei clase: clasa are o singură instanță, care va fi folosită în întreg proiectul.
Pentru a asigura restricționarea:
Există două abordări frecvente:
public class SingletonClass { /* la inceput, inainte de prima si singura instantiere a clasei SingletonClass va avea valoarea null */ private static SingletonClass obj = null; public int value = 10; // lasam constructorul clasei privat pentru a nu fi accesat din exterior private SingletonClass() { // do stuff System.out.println("Instantiam!"); } // metoda prin care se creaza unica instanta a clasei // lazy instantiation public static SingletonClass getInstance() { // daca clasa nu a fost instantiata inainte, o facem acum if (obj == null) obj = new SingletonClass(); return obj; } public void show() { System.out.println("Singleton is magic"); } }
Un avantaj este accesul ușor la instanța globală, fără a avea nevoie să o transmitem ca parametru sau să o instanțiem manual.
public void modifyValue (int x) { SingletonClass.getInstance().value = x; // se modifica valoarea lui value din clasa } SingletonClass.getInstance().show();
public class SingletonClass { private static SingletonClass obj = new SingletonClass(); private SingletonClass() {} // eager instantiation - merge la threaduri public static SingletonClass getInstance() { return obj; } }
Utilizări frecvente:
Uneori suntem nevoiți să creăm obiecte în funcție de preferința utilizatorului sau de alte necesități. De aceea folosim Factory, prin care alcătuim o familie de clase înrudite (prin moștenirea aceleiași clase abstracte sau implementarea aceleiași interfețe), iar crearea obiectului concret este delegată către o metodă de tip factory.
În exemplul de mai jos, utilizatorul cere un tip de pizza prin nume; dacă tipul există, primește informații despre pizza.
interface IPizza { void showStuff(); } /* nu este neaparat sa avem o clasa abstracta ce implementeaza o interfata putem avea pur si simplu o clasa abstracta (fara sa implementeze o interfata) care e extinsa de clasele normale sau o interfata ce e implementata direct de clasele normale din Factory */ abstract class Pizza implements IPizza { public abstract void showStuff(); } class PizzaMargherita extends Pizza { public void showStuff() { System.out.println("Sos tomat si branza Mozzarella."); } } class PizzaQuattroStagioni extends Pizza { public void showStuff() { System.out.println("Sos tomat, branza Mozzarella, sunca, pepperoni, " + "ciuperci, ardei. "); } } class PizzaPepperoni extends Pizza { public void showStuff() { System.out.println("Sos tomat, branza Mozzarella, dublu pepperoni."); } } class PizzaHawaii extends Pizza { public void showStuff() { System.out.println("Sos tomat, branza Mozzarella, sunca, dublu ananas."); } } class PizzaFactory { public static Pizza factory (String pizzaName) { if (pizzaName.equals("Margherita")) return new PizzaMargherita(); if (pizzaName.equals("Hawaii")) return new PizzaHawaii(); if (pizzaName.equals("Quattro Stagioni")) return new PizzaQuattroStagioni(); if (pizzaName.equals("Pepperoni")) return new PizzaPepperoni(); return null; } }
Acest pattern este folosit în restaurantele de tip fast food care furnizează meniul pentru copii. Un meniu pentru copii constă de obicei într-un fel principal, unul secundar, o băutură și o jucărie. Pot exista variații în ceea ce privește conținutul meniului, dar procesul de creare este același. Fie că la felul principal se alege un hamburger sau un cheeseburger, procesul va fi același. Vânzătorul le va indica celor din spate ce să pună pentru fiecare fel de mâncare, pentru băutură și jucărie. Toate acestea vor fi puse într-o pungă și servite clienților.
Acest șablon realizează separarea construcției de obiecte complexe de reprezentarea lor astfel încât același proces să poată crea diferite reprezentări. Builder-ul creează părți ale obiectului complex de fiecare dată când este apelat și reține toate stările intermediare. Când produsul este terminat, clientul primește rezultatul de la builder.
În acest mod, se obține un control mai mare asupra procesului de construcție de noi obiecte. Spre deosebire de alte pattern-uri din categoria creational, care creau produsele într-un singur pas, pattern-ul Builder construiește un produs pas cu pas la comanda coordonatorului.
public class User { private final String firstName; // required private final String lastName; // required private final int age; // optional private final String phone; // optional private final String address; // optional private User(UserBuilder builder) { this.firstName = builder.firstName; this.lastName = builder.lastName; this.age = builder.age; this.phone = builder.phone; this.address = builder.address; } public String getFirstName() { return firstName; } public String getLastName() { return lastName; } public int getAge() { return age; } public String getPhone() { return phone; } public String getAddress() { return address; } public String toString() { return "User:" + this.firstName + " " + this.lastName + " " + this.age + " " + this.phone + " " + this.address; } public static class UserBuilder { private final String firstName; private final String lastName; private int age; private String phone; private String address; public UserBuilder(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } public UserBuilder age(int age) { this.age = age; return this; } public UserBuilder phone(String phone) { this.phone = phone; return this; } public UserBuilder address(String address) { this.address = address; return this; } public User build() { return new User(this); } } public static void main(String[] args) { User user1 = new User.UserBuilder("Lokesh", "Gupta") .age(30) .phone("1234567") .address("Fake address 1234") .build(); User user2 = new User.UserBuilder("Jack", "Reacher") .age(40) .phone("5655") //no address .build(); } }
O clasă de tip Factory poate fi utilizată în mai multe locuri în cadrul unui proiect. Pentru a economisi resurse, putem folosi pattern-ul Singleton pentru Factory, astfel încât să existe o singură instanță a clasei Factory.
Acest design pattern stabilește o relație one-to-many între obiecte. Avem un obiect numit subiect, căruia îi este asociată o colecție (listă) de observatori. Observatorii sunt obiecte dependente de subiect și sunt notificate automat de către subiect atunci când în subiect are loc o acțiune sau o modificare a stării.
Strategy este un design pattern behavioral ce oferă o familie de algoritmi (strategii), încapsulate în clase care oferă o interfață comună de folosire. Clientul (utilizatorul) poate alege dinamic strategia care va fi folosită.
Exemplu de motivare: la căutarea unui element într-o colecție, putem alege algoritmul în funcție de proprietăți ale colecției (de exemplu, dacă este sortată: căutare binară; altfel: iterare liniară).
Acest design pattern oferă posibilitatea de a separa un algoritm de structura de date pe care acesta operează, astfel încât să putem adăuga ușor funcții noi care operează peste o familie de clase fără să modificăm structura acestora.
Pe scurt, folosim Visitor dacă avem tipuri diferite și dorim să adăugăm/schimbăm operații fără să modificăm clasele.
În cadrul pattern-ului:
interface Visitor { void visit (Director f); void visit (Fisier f); } class Ls implements Visitor { public void visit (Director f) { System.out.println(f.getName()); for (Repository repo: f.getChildren()) { System.out.println("\t" + repo.getName()); // afisam numele unui repo (fisier / folder) } } public void visit (Fisier f) { System.out.println("Not a folder"); /* comanda Ls (in acest exemplu) este specifica doar folderelor, in acest caz este evidentiat un dezavantaj al Visitor-ului, faptul ca noi trebuie sa implementam metode de care nu avem nevoie in acest caz - se incalca Interface Segregation Principle */ } } class Cat implements Visitor { public void visit (Director f) { // avertisment ca avem folder, nu fisier } public void visit (Fisier f) { // citire fisier, folosind numele fisierului } } abstract class Repository { private String name; // numele unui fisier sau folder (de fapt, calea acestuia) public String getName() { return name; } public abstract void accept (Visitor f); } class Fisier extends Repository { public void accept (Visitor f) { f.visit(this); // Visitor-ul "viziteaza" fisierul, adica acesta // efectueaza o operatie asupra fisierului } } class Director extends Repository { private List<Repository> children = new ArrayList<>(); public List<Repository> getChildren() { return children; } public void accept (Visitor f) { f.visit(this); } }