De 5 mest almindelige designmønstre i PHP-applikationer

Foto af Neil Thomas på Unsplash

Hvis du tror, ​​at det første mønster er Singleton, bliver du fyret! Singleton-mønsteret er allerede forældet og ikke ønsket og endda hadet.

Lad os se på de 5 mest almindeligt anvendte designede mønstre i PHP-verdenen i disse dage.

Fabrik

Du skal bruge fabrikker, når du vil bygge et objekt. Det er rigtigt - opbyg og ikke skab. Du ønsker ikke at have en fabrik bare for at oprette et nyt objekt. Når du bygger objektet opretter du det først og initialiserer det derefter. Normalt kræver det at udføre flere trin og anvende visse logikker. Med det giver det mening at have alt det på et sted og genbruge det, når du har brug for at få et nyt objekt bygget på samme måde. Dybest set er det punktet med fabriksmønsteret.

Det er en god ide at have en grænseflade til din fabrik og have din kode afhængig af den og ikke af en betonfabrik. Med det kan du nemt udskifte en fabrik med en anden, når du har brug for den.

interface FriendFactoryInterface {
    public function create (): Ven
}

Dernæst implementerer vi vores fabriksgrænseflade i følgende klasse:

klasse FriendFactory implementerer FriendFactoryInterface {
    oprettelse af offentlig funktion (): Ven {
        
        $ ven = ny ven ();
        // initialiser din ven
        vende $ ven tilbage;
    }
}

Det er temmelig enkelt og alligevel kraftigt designmønster!

Strategi

Det bruges til at skjule implementeringsdetaljer for algoritmer, der er nødvendige for at udføre en operation. Når han har strategier, kan klienten vælge den nødvendige algoritme uden at kende den faktiske implementering og anvende den til at udføre handlingen.

Lad os sige, at vi er nødt til at oprette et bibliotek, der overfører dataene fra en datakilde til en anden. For eksempel er vi nødt til at overføre dataene fra databasen til csv-filen eller fra regnearket til json-filen. Hvordan ville du gøre det?

Først skal vi oprette respektive strategier for at læse dataene fra lagringerne. Lad os kalde dem læsere. Dernæst skal vi oprette respektive strategier for at skrive dataene til lagringerne. Lad os kalde dem forfattere.

Derfor har vi 2 læsere til at læse dataene enten fra databasen eller fra regnearket. Derfor vil vi have 2 forfattere til at skrive dataene enten i csv-filen eller i json-filen.

Vigtigt: klienten, der vil arbejde med vores strategier, skal ikke bry sig om deres implementeringer. Derfor bør vi også definere grænseflader til vores strategier. På den måde vil klienten kun vide om de metoder, der er defineret af strategigrænsefladerne og kun arbejde med dem, og hvad der sker bag scenen er ikke dets problem.

Endelig er vi nødt til at oprette den klient, der vælger de nødvendige strategier, baseret på hvor og hvor den skal overføres dataene.

Lad os se alt dette i handling:

interface ReaderInterface {
    start af offentlig funktion (): ugyldig;
    offentlig funktion læst (): array;
    public function stop (): ugyldig;
}
interface WriterInterface {
   start af offentlig funktion (): ugyldig;
   offentlig funktionsskrivning (array $ data): ugyldig;
   public function stop (): ugyldig;
}
klasse DatabaseReader implementerer ReaderInterface {
    ...
}
klasse SpreadsheetReader implementerer ReaderInterface {
    ...
}
klasse CsvWriter implementerer WriterInterface {
    ...
}
klasse JsonWriter implementerer WriterInterface {
    ...
}
klasse Transformer {
    
    ...
    offentlig funktionstransformation (streng $ fra, streng $ til): ugyldig {
        $ reader = $ this-> findReader ($ fra);
        $ Writer = $ this-> findWriter ($ til);
        
        $ Læser-> start ();
        $ Forfatter og> start ();
        prøve {
            foreach ($ reader-> read () som $ række) {
                $ Forfatter og> skrive ($ row);
            }
         } langt om længe {
             $ Forfatter og> stop ();
             $ Læser-> stop ();
         }
     }
     ...
}

Som du kan se, er den transformator, der er klienten for vores strategier, ikke ligeglad med de implementeringer, som den fungerer med. Alt det bekymrer sig om er de metoder, der er defineret af vores strategigrænseflader.

adapter

Det bruges til at omdanne et fremmed interface til et fælles interface. Lad os antage, at du i projektet henter dataene fra noget lager ved hjælp af følgende klasse.

klasse Opbevaring {
    privat $ kilde;
    
    offentlig funktion __constructor (AdapterInterface $ kilde) {
        $ this-> source = $ source;
    }
    offentlig funktion getOne (int $ id):? objekt {
        returner $ dette-> kilde-> find ($ id);
    }
    
    offentlig funktion getAll (array $ kriterier = []): Samling {
        returner $ dette-> kilde-> findAll ($ kriterier);
    }
}

Bemærk, at lageret ikke fungerer direkte med kilden, men i stedet fungerer det med adapterens kilde.

Desuden ved lagringen ikke noget om konkrete adaptere. Det henviser kun til adaptergrænsefladen. Således er den konkrete implementering af den medfølgende adapter en komplet sort boks til det.

Her er et eksempel på adaptergrænsefladen

interface AdapterInterface {
    offentlig funktion find (int $ id):? objekt;
    offentlig funktion findAll (array $ kriterier = []): Samling;
}

Lad os nu antage, at vi bruger et bibliotek til at få adgang til MySQL-databasen. Biblioteket dikterer sin egen grænseflade, og det ser ud som følgende:

$ række = $ mysql-> fetchRow (...);
$ data = $ mysql-> fetchAll (...);

Som du kan se, kan vi ikke integrere dette bibliotek ligesom det i vores lager. Vi er nødt til at oprette en adapter til det som nedenfor:

klasse MySqlAdapter implementerer AdapterInterface {
    
     ...
     offentlig funktion find (int $ id):? objekt {
         
         $ data = $ this-> mysql-> fetchRow (['id' => $ id]);
         // noget datatransformation
     }
     public function findAll (array $ criteria = []): Samling {
              
         $ data = $ dette-> mysql-> fetchAll ($ kriterier);
         // noget datatransformation
     }
   
     ...
}

Derefter kan vi injicere det i lageret på samme måde:

$ storage = new Storage (ny MySqlAdapter ($ mysql));

Hvis vi senere beslutter at bruge et andet bibliotek i stedet for det, bliver vi kun nødt til at oprette en anden adapter til det bibliotek, ligesom vi gjorde herover, og derefter injicere den nye adapter i lageret. Som du kan se, for at bruge et andet bibliotek til at hente dataene fra databasen behøver vi ikke at røre ved en ting inden for Storage-klassen. Det er kraften i adapterens designmønster!

Observer

Det bruges til at underrette resten af ​​systemet om visse begivenheder på et bestemt sted. For at få bedre forståelse af fordelene ved dette mønster, lad os gennemgå to løsninger på det samme problem.

Lad os sige, at vi er nødt til at skabe teater for at vise film til kritikerne. Vi definerer klassen Teater med den nuværende metode. Før vi præsenterer filmen, ønsker vi at sende beskeder til kritikernes mobiltelefoner. Derefter vil vi midt i filmen stoppe filmen i 5 minutter for at lade kritikerne få en pause. Endelig, efter at filmen er slut, vil vi bede kritikerne om at give deres feedback.

Lad os se, hvordan dette ville se ud i koden:

klasse Teater {
   
    offentlig funktion til stede (Movie $ film): ugyldig {
       
        $ kritikere = $ film-> getCritics ();
        $ this-> messenger-> send ($ kritikere, '...');

        $ Film-> play ();

        $ Film-> pause (5);
        $ This-> progress-> pause ($ kritikere)
        $ Film-> finish ();

        $ This-> feedback-> anmodning ($ kritikere);
    }
}

Det ser rent og lovende ud.

Efter nogen tid fortalte chefen os, at vi, før vi starter filmen, også ønsker at slukke lyset. Derudover vil vi i midten af ​​filmen, når den stopper, vise reklamen. Endelig, når filmen slutter, vil vi starte automatisk rengøring af rummet.

Nå, et af spørgsmålene her er, at for at opnå det, er vi nødt til at ændre vores teaterklasse, og det bryder SOLID-principper. Især bryder det det åbne / lukkede princip. Desuden vil denne tilgang gøre, at teaterklassen er afhængig af flere ekstra tjenester, som heller ikke er god.

Hvad hvis vi vender tingene på hovedet. I stedet for at tilføje mere og mere kompleksitet og afhængigheder til teaterklassen vil vi sprede kompleksiteten over hele systemet, og med det reducere teaterklassens afhængigheder som en bonus.

Sådan ser det ud i handling:

klasse Teater {
    
    offentlig funktion til stede (Movie $ film): ugyldig {
        
        $ This-> getEventManager ()
            -> underrette (ny begivenhed (begivenhed :: START, $ film));
        $ Film-> play ();

        $ Film-> pause (5);
        $ This-> getEventManager ()
            -> underrette (ny begivenhed (Event :: PAUSE, $ film));
        $ Film-> finish ();

        $ This-> getEventManager ()
            -> underrette (ny begivenhed (begivenhed :: END, $ film));
    }
}
$ teater = nyt teater ();
$ teater
    -> getEventManager ()
    -> lyt (Event :: START, nye MessagesListener ())
    -> lyt (Begivenhed :: START, nyt LightsListener ())
    -> lyt (Event :: PAUSE, ny BreakListener ())
    -> lyt (Event :: PAUSE, ny AnnonceListener ())
    -> lyt (Begivenhed :: END, ny FeedbackListener ())
    -> lyt (Event :: END, ny CleaningListener ());
$ Teater-> stede ($ film);

Som du kan se, bliver den nuværende metode ekstremt ligetil. Det er ligeglad med hvad der sker uden for klassen. Det gør bare, hvad det skal, og giver resten af ​​systemet besked om fakta. Uanset hvad der er interesseret i disse kendsgerninger kan lytte til de respektive begivenheder og blive underrettet om dem og gøre hvad det skal gøre.

Med denne tilgang bliver det også temmelig let at tilføje yderligere kompleksitet. Alt hvad du skal gøre er at oprette en ny lytter og sætte den nødvendige logik der.

Håber du fandt Observer-mønsteret nyttigt.

Dekoratør

Det bruges, når du vil justere et objekts adfærd ved kørsel og dermed reducere overflødige arv og antallet af klasser. Du spørger måske, hvorfor har jeg det overhovedet brug for? Det kunne godt forklares med eksempler.

Lad os sige, at vi har klasser Vindue og Dør, og de implementerer begge OpenerInterface.

interface OpenerInterface {
    offentlig funktion åben (): ugyldig;
}
klasse Dørredskaber OpenerInterface {
    offentlig funktion åben (): ugyldig {
        // åbner døren
    }
}
klasse Vindue implementerer OpenerInterface {
    offentlig funktion åben (): ugyldig {
        // åbner vinduet
    }
}

Både vinduer og døre har samme opførsel at åbne. Nu har vi brug for andre døre og vinduer med ekstra funktionalitet, der fortæller brugerne temperaturen udenfor, når de åbner dørene eller vinduerne. Vi kan løse dette problem med arv ligesom dette:

klasse SmartDoor udvider døren {
    offentlig funktion åben (): ugyldig {
        forælder :: open ();
        $ Dette-> temperatur ();
    }
}
klasse SmartWindow udvider vinduet {
    offentlig funktion åben (): ugyldig {
        forælder :: open ();
        $ Dette-> temperatur ();
    }
}

Alt i alt har vi 4 klasser i alt nu. Imidlertid kunne vi med Decorator-mønsteret løse dette problem med kun 3 klasser. Sådan gør du:

klasse SmartOpener implementerer OpenerInterface {
    
    privat $ åbner;
    offentlig funktion __construct (OpenerInterface $ opener) {
        $ this-> opener = $ opener;
    }
    
    offentlig funktion åben (): ugyldig {
        $ This-> opener-> open ();
        $ Dette-> temperatur ();
    }
}
$ dør = ny dør ();
$ vindue = nyt vindue ();
$ smartDoor = ny SmartOpener ($ dør);
$ smartWindow = nyt SmartOpener ($ vindue);

Vi har introduceret en ny type af en åbner, der fungerer som en proxy, men med en ekstra funktionalitet oven på. Det er det, der gør susen.

Håber du fandt denne artikel nyttig og interessant. I så fald skal du ikke tøve med at klappe og dele det på sociale netværk.

God kodning! :)