Pe canalul meu de YouTube am făcut de la începutul anului mai multe clipuri legate de design pattern-urile mai cunoscute din Software Development. Azi o să vorbim despre decorator design pattern și în scris (sau decorator, în limba română, dar cred că e destul de clar), sau Wrapper, cum îmi mai place mie să-i spun, mi se pare că reprezintă mai bine ceea ce este și ce face el de fapt.

Deși decorator design pattern este unul din cele mai simple, de altfel toate din zona celor structurale sunt relativ simple față de cele behavioral sau creaționale, dar pe cât este de simplu pe atât este de folosit și de practic.

Context

Pentru a putea înțelege mai bine utilitatea pattern-ului, am exemplificat în diagrama de mai sus, sau am simulat mai bine zis, un sistem simplu care ar fi avut nevoie să scrie date pe disc. Pentru a face asta, să zicem că am folosit o librărie externă, care ne-a pus la dispoziție clasa FileStorage cât și interfața IStorage.

    public interface IStorage {
        void write(String data);
        String read();
    }

Am terminat implementarea și am cuplat codul nostru client ori de câte ori a fost nevoie, la interfața IStorage.

    public class LocalStorage implements IStorage {
        private String store;
        @Override
        public void write(String data) {
            store += data;
            System.out.printf("%s stored locally.", data);
        }

        @Override
        public String read(){
            return store;
        }
    }

Application reprezintă în acest caz codul client:

public class Application {
    private final IStorage storage;

    public Application(IStorage storage) {
        this.storage = storage;
    }

    public void storeData(String data){
        storage.write("Data to be stored");
    }

    public void readData(){
        System.out.printf("Stored data: %s", 
            storage.read());
    }
}

Problema

Câteva luni sau ani mai târziu, Product Owner-ul sau Manager-ul, vine cu solicitarea ca aplicația să împingă ceea ce salvează pe disc și într-un storage de cloud. Ce putem să facem în cazul ăsta pentru că din moment ce am folosit o librărie terță să putem scrie pe disc, nu putem adăuga alte funcționalități clasei FileStorage. Mai mult de atât, am cuplat tot codul client cu interfața IStorage, ceea ce va propaga modificări majore în codul aplicației dacă vom înlocui această interfață cu alta, vor crăpa o grămadă de teste pe care va trebui să le refacem și să ne asigurăm că totul funcționează corect. Deși task-ul părea unul simplu, aduce după sine un technical debt destul de mare.

Rezolvare

Aici intervine Decorator design pattern și ne ajută să rezolvăm această problemă. Adăugăm clasa CloudStorage, care va juca rolul de decorator sau wrapper pentru vechea clasă FileStorage, căreia îi pasăm un obiect de tipul FileStorage pentru a delega funcționalitatea deja existentă către acesta, dar îi adăugăm de asemenea și o metodă nouă care îndeplinește noua cerință, denumită syncInCloud().

public class CloudStorage implements IStorage{
    private final LocalStorage localStorage;

    public CloudStorage(LocalStorage localStorage) {
        this.localStorage = localStorage;
    }

    @Override
    public void write(String data) {
        localStorage.write(data);
        syncInCloud();
    }

    @Override
    public String read() {
        return localStorage.read();
    }

    private void syncInCloud(){
        System.out.printf("%s is sent to cloud storage.", 
            localStorage.read());
    }
}

Cam asta este tot, acum putem să schimbăm implementarea de IStorage pe care o pasăm lui Application la runtime și codul nostru nu va avea nevoie de nicio altă schimbare sau adaptare, iar datele vor fi acum împinse și către cloud.