Die 5 häufigsten Entwurfsmuster in PHP-Anwendungen

Foto von Neil Thomas auf Unsplash

Wenn Sie denken, dass das Muster Nummer eins Singleton ist, dann sind Sie gefeuert! Das Singleton-Muster ist bereits veraltet und wird nicht mehr gewollt und sogar gehasst.

Werfen wir einen Blick auf die 5 derzeit am häufigsten verwendeten Entwurfsmuster in der PHP-Welt.

Fabrik

Sie sollten Fabriken verwenden, wenn Sie ein Objekt erstellen möchten. Das ist richtig - bauen und nicht kreieren. Sie möchten keine Fabrik, nur um ein neues Objekt zu erstellen. Wenn Sie das Objekt erstellen, erstellen Sie es zuerst und initialisieren es dann. Normalerweise müssen mehrere Schritte ausgeführt und bestimmte Logik angewendet werden. In diesem Fall ist es sinnvoll, alles an einem Ort zu haben und es dann wiederzuverwenden, wenn Sie ein neues Objekt auf die gleiche Weise erstellen müssen. Im Grunde ist das der Punkt des Fabrikmusters.

Es ist eine gute Idee, eine Schnittstelle für Ihre Fabrik zu haben und Ihren Code davon abhängig zu machen und nicht von einer konkreten Fabrik. So können Sie eine Fabrik bei Bedarf problemlos durch eine andere ersetzen.

Schnittstelle FriendFactoryInterface {
    öffentliche Funktion create (): Freund
}

Als nächstes implementieren wir unsere Factory-Schnittstelle mit der folgenden Klasse:

Klasse FriendFactory implementiert FriendFactoryInterface {
    öffentliche Funktion create (): Friend {
        
        $ friend = neuer Freund ();
        // initialisiere deinen Freund
        $ friend zurückgeben;
    }
}

Das ist ein ziemlich einfaches und dennoch leistungsstarkes Designmuster!

Strategie

Es wird verwendet, um Implementierungsdetails von Algorithmen auszublenden, die zum Ausführen einer Operation benötigt werden. Mit Strategien kann der Client den benötigten Algorithmus auswählen, ohne die tatsächliche Implementierung zu kennen, und ihn anwenden, um die Operation auszuführen.

Nehmen wir an, wir müssen eine Bibliothek erstellen, die die Daten von einer Datenquelle zu einer anderen überträgt. Beispielsweise müssen wir die Daten von der Datenbank in die CSV-Datei oder von der Tabellenkalkulation in die JSON-Datei übertragen. Wie würdest du das machen?

Zuerst müssen wir entsprechende Strategien erstellen, um die Daten aus den Speichern zu lesen. Nennen wir sie Leser. Als nächstes müssen wir entsprechende Strategien erstellen, um die Daten in die Speicher zu schreiben. Nennen wir sie Autoren.

Daher haben wir 2 Leser, um die Daten entweder aus der Datenbank oder aus der Tabelle zu lesen. Dementsprechend haben wir 2 Writer, um die Daten entweder in die CSV-Datei oder in die JSON-Datei zu schreiben.

Wichtig: Der Kunde, der mit unseren Strategien arbeitet, sollte sich nicht um deren Implementierung kümmern. Deshalb sollten wir auch Schnittstellen für unsere Strategien definieren. Auf diese Weise kennt der Kunde nur die von den Strategieschnittstellen definierten Methoden und arbeitet nur mit ihnen. Was hinter den Kulissen passiert, ist nicht das Problem.

Schließlich müssen wir den Client erstellen, der die erforderlichen Strategien basierend darauf auswählt, von wo und wohin die Daten übertragen werden sollen.

Sehen wir uns das alles in Aktion an:

Schnittstelle ReaderInterface {
    public function start (): void;
    öffentliche Funktion read (): array;
    öffentliche Funktion stop (): void;
}
interface WriterInterface {
   public function start (): void;
   public function write (Array $ data): void;
   öffentliche Funktion stop (): void;
}
Klasse DatabaseReader implementiert ReaderInterface {
    ...
}
Klasse SpreadsheetReader implementiert ReaderInterface {
    ...
}
Klasse CsvWriter implementiert WriterInterface {
    ...
}
Klasse JsonWriter implementiert WriterInterface {
    ...
}
klasse Transformator {
    
    ...
    public function transform (string $ von, string $ bis): void {
        $ reader = $ this-> findReader ($ from);
        $ writer = $ this-> findWriter ($ to);
        
        $ reader-> start ();
        $ writer-> start ();
        Versuchen {
            foreach ($ reader-> read () als $ row) {
                $ writer-> write ($ row);
            }
         } endlich {
             $ writer-> stop ();
             $ reader-> stop ();
         }
     }
     ...
}

Wie Sie sehen, kümmert sich der Transformator, der der Kunde unserer Strategien ist, nicht wirklich um die Implementierungen, mit denen er arbeitet. Alles, worauf es ankommt, sind die von unseren Strategie-Schnittstellen definierten Methoden.

Adapter

Es wird verwendet, um eine fremde Schnittstelle in eine gemeinsame Schnittstelle umzuwandeln. Nehmen wir an, dass Sie im Projekt die Daten aus einem Speicher mit der folgenden Klasse abrufen.

Klasse Speicher {
    private $ source;
    
    public function __constructor (AdapterInterface $ source) {
        $ this-> source = $ source;
    }
    öffentliche Funktion getOne (int $ id):? object {
        return $ this-> source-> find ($ id);
    }
    
    public function getAll (array $ criterion = []): Collection {
        return $ this-> source-> findAll ($ criterions);
    }
}

Beachten Sie, dass der Speicher nicht direkt mit der Quelle funktioniert, sondern mit dem Adapter der Quelle.

Außerdem weiß der Speicher nichts über konkrete Adapter. Es bezieht sich nur auf die Adapter-Schnittstelle. Somit ist die konkrete Umsetzung des mitgelieferten Adapters eine komplette Black-Box dafür.

Hier ist ein Beispiel für die Adapter-Schnittstelle

interface AdapterInterface {
    public function find (int $ id):? object;
    öffentliche Funktion findAll (array $ criterion = []): Collection;
}

Angenommen, wir verwenden eine Bibliothek, um auf die MySQL-Datenbank zuzugreifen. Die Bibliothek diktiert eine eigene Schnittstelle und sieht folgendermaßen aus:

$ row = $ mysql-> fetchRow (...);
$ data = $ mysql-> fetchAll (...);

Wie Sie sehen, können wir diese Bibliothek nicht einfach so in unseren Speicher integrieren. Wir müssen einen Adapter dafür wie folgt erstellen:

Klasse MySqlAdapter implementiert AdapterInterface {
    
     ...
     public function find (int $ id):? object {
         
         $ data = $ this-> mysql-> fetchRow (['id' => $ id]);
         // Einige Datenumwandlungen
     }
     öffentliche Funktion findAll (array $ criterion = []): Collection {
              
         $ data = $ this-> mysql-> fetchAll ($ criterions);
         // Einige Datenumwandlungen
     }
   
     ...
}

Danach können wir es wie folgt in den Speicher injizieren:

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

Wenn wir später beschließen, eine andere Bibliothek anstelle dieser zu verwenden, müssen wir nur einen weiteren Adapter für diese Bibliothek erstellen, wie wir es oben getan haben, und dann den neuen Adapter in den Speicher einfügen. Wie Sie sehen, müssen wir nichts in der Storage-Klasse berühren, um mit einer anderen Bibliothek die Daten aus der Datenbank abzurufen. Das ist die Stärke des Adapter-Entwurfsmusters!

Beobachter

Es wird verwendet, um den Rest des Systems über bestimmte Ereignisse an bestimmten Orten zu informieren. Um die Vorteile dieses Musters besser zu verstehen, werden zwei Lösungen desselben Problems vorgestellt.

Nehmen wir an, wir müssen Theater schaffen, um den Kritikern Filme zu zeigen. Mit der vorliegenden Methode definieren wir die Klasse Theater. Bevor wir den Film präsentieren, möchten wir Nachrichten an die Handys der Kritiker senden. Dann wollen wir in der Mitte des Films den Film für 5 Minuten anhalten, damit die Kritiker eine Pause einlegen können. Schließlich möchten wir die Kritiker nach dem Ende des Films bitten, ihr Feedback zu hinterlassen.

Mal sehen, wie das im Code aussehen würde:

Klasse Theater {
   
    Öffentliche Funktion vorhanden (Movie $ movie): void {
       
        $ critics = $ movie-> getCritics ();
        $ this-> messenger-> send ($ critics, '...');

        $ movie-> play ();

        $ movie-> pause (5);
        $ this-> progress-> break ($ ​​critics)
        $ movie-> finish ();

        $ this-> feedback-> request ($ critics);
    }
}

Es sieht sauber und vielversprechend aus.

Jetzt, nach einiger Zeit, sagte uns der Chef, dass wir vor dem Start des Films auch das Licht ausschalten wollen. Außerdem möchten wir in der Mitte des Films, wenn er pausiert, die Werbung zeigen. Wenn der Film zu Ende ist, möchten wir endlich mit der automatischen Reinigung des Raums beginnen.

Nun, eines der Probleme hier ist, dass wir unsere Theaterklasse modifizieren müssen, um dies zu erreichen, und dies verstößt gegen die SOLID-Prinzipien. Insbesondere bricht es das Auf / Zu-Prinzip. Darüber hinaus wird dieser Ansatz dazu führen, dass die Theaterklasse von mehreren zusätzlichen Dienstleistungen abhängig ist, was ebenfalls nicht gut ist.

Was ist, wenn wir die Dinge auf den Kopf stellen? Anstatt der Theaterklasse immer mehr Komplexität und Abhängigkeiten hinzuzufügen, werden wir die Komplexität über das System verteilen und damit die Abhängigkeiten der Theaterklasse als Bonus reduzieren.

So wird das in Aktion aussehen:

Klasse Theater {
    
    Öffentliche Funktion vorhanden (Movie $ movie): void {
        
        $ this-> getEventManager ()
            -> benachrichtigen (neues Ereignis (Ereignis :: START, $ movie));
        $ movie-> play ();

        $ movie-> pause (5);
        $ this-> getEventManager ()
            -> benachrichtigen (neues Ereignis (Ereignis :: PAUSE, $ movie));
        $ movie-> finish ();

        $ this-> getEventManager ()
            -> benachrichtigen (neues Event (Event :: END, $ movie));
    }
}
$ theater = neues Theater ();
Theater
    -> getEventManager ()
    -> listen (Event :: START, neuer MessagesListener ())
    -> listen (Event :: START, neuer LightsListener ())
    -> listen (Event :: PAUSE, neuer BreakListener ())
    -> listen (Event :: PAUSE, neuer AdvertisementListener ())
    -> listen (Event :: END, neuer FeedbackListener ())
    -> listen (Event :: END, neuer CleaningListener ());
$ theater-> present ($ movie);

Wie Sie sehen können, ist das vorliegende Verfahren äußerst einfach. Es ist egal, was außerhalb der Klasse passiert. Es tut einfach das, was es tun soll und benachrichtigt den Rest des Systems über die Fakten. Was auch immer an diesen Fakten interessiert ist, kann sich die jeweiligen Ereignisse anhören und darüber informiert werden und tun, was es zu tun hat.

Mit diesem Ansatz wird es auch ziemlich einfach, zusätzliche Komplexität hinzuzufügen. Alles, was Sie tun müssen, ist, einen neuen Listener zu erstellen und die erforderliche Logik dort abzulegen.

Ich hoffe, Sie fanden das Observer-Muster nützlich.

Dekorateur

Es wird verwendet, wenn Sie das Verhalten eines Objekts zur Laufzeit anpassen und damit redundante Vererbungen und die Anzahl der Klassen reduzieren möchten. Sie könnten fragen, warum ich das überhaupt brauche? Nun, es könnte besser mit Beispielen erklärt werden.

Nehmen wir an, wir haben die Klassen Window und Door und beide implementieren OpenerInterface.

interface OpenerInterface {
    öffentliche Funktion offen (): nichtig;
}
Klasse Door implementiert OpenerInterface {
    öffentliche Funktion offen (): nichtig {
        // öffnet die Tür
    }
}
Klasse Window implementiert OpenerInterface {
    öffentliche Funktion offen (): nichtig {
        // öffnet das Fenster
    }
}

Sowohl die Fenster als auch die Türen haben das gleiche Öffnungsverhalten. Jetzt brauchen wir andere Türen und Fenster mit zusätzlichen Funktionen, die den Benutzern die Außentemperatur anzeigen, wenn sie die Türen oder Fenster öffnen. Wir können dieses Problem mit der folgenden Vererbung lösen:

Klasse SmartDoor verlängert Tür {
    öffentliche Funktion offen (): nichtig {
        parent :: open ();
        $ this-> temperature ();
    }
}
Klasse SmartWindow erweitert Window {
    öffentliche Funktion offen (): nichtig {
        parent :: open ();
        $ this-> temperature ();
    }
}

Insgesamt haben wir mittlerweile 4 Klassen. Mit dem Decorator-Muster konnten wir dieses Problem jedoch nur mit 3 Klassen lösen. Hier ist wie:

Klasse SmartOpener implementiert OpenerInterface {
    
    privater $ opener;
    public function __construct (OpenerInterface $ opener) {
        $ this-> opener = $ opener;
    }
    
    öffentliche Funktion offen (): nichtig {
        $ this-> opener-> open ();
        $ this-> temperature ();
    }
}
$ door = new Door ();
$ window = neues Fenster ();
$ smartDoor = neuer SmartOpener ($ door);
$ smartWindow = neuer SmartOpener ($ window);

Wir haben einen neuen Opener-Typ eingeführt, der sich wie ein Proxy verhält, aber eine zusätzliche Funktionalität enthält. Das ist es, was den Trick macht.

Ich hoffe, Sie fanden diesen Artikel nützlich und interessant. Wenn ja, zögern Sie nicht, zu klatschen und in sozialen Netzwerken zu teilen.

Viel Spaß beim Codieren! :)