Wie schreibe ich Code, der Veränderungen beinhaltet?

Das Schreiben von Code, der leicht zu ändern ist, ist der Heilige Gral der Programmierung. Willkommen beim Programmieren von Nirvana! In der Realität sind die Dinge jedoch viel schwieriger: Quellcode ist schwer zu verstehen, Abhängigkeiten zeigen in unzählige Richtungen, Kopplung ist ärgerlich, und Sie spüren bald die Hitze der Programmierung. In diesem Lernprogramm werden einige Prinzipien, Techniken und Ideen beschrieben, mit deren Hilfe Sie Code schreiben können, der leicht zu ändern ist.


Einige objektorientierte Konzepte

Die objektorientierte Programmierung (OOP) wurde aufgrund ihres Versprechens der Organisation und Wiederverwendung von Code populär; es ist bei diesem Unterfangen völlig gescheitert. Wir verwenden OOP-Konzepte bereits seit vielen Jahren. Dennoch setzen wir in unseren Projekten immer wieder dieselbe Logik ein. OOP führte eine Reihe guter Grundprinzipien ein, die bei richtiger Anwendung zu besserem und saubererem Code führen können.

Zusammenhalt

Die Dinge, die zusammengehören, sollten zusammengehalten werden. Andernfalls sollten sie an einen anderen Ort verschoben werden. Darauf bezieht sich der Begriff Kohäsion. Das beste Beispiel für Kohäsion kann mit einer Klasse gezeigt werden:

Klasse ANOTCohesiveClass private $ firstNumber; private $ secondNumber; private $ length; private $ width; Funktion __construct ($ firstNumber, $ secondNumber) $ this-> firstNumber = $ firstNumber; $ this-> secondNumber = $ secondNumber;  function setLength ($ length) $ this-> length = $ length;  function setHeight ($ height) $ this-> width = $ height;  function add () return $ this-> firstNumber + $ this-> secondNumber;  function subtract () return $ this-> firstNumber - $ this-> secondNumber;  Funktionsbereich () return $ this-> length * $ this-> width; 

In diesem Beispiel wird eine Klasse mit Feldern definiert, die Zahlen und Größen darstellen. Diese Eigenschaften, nur nach ihren Namen beurteilt, gehören nicht zusammen. Wir haben dann zwei Methoden, hinzufügen() und subtrahieren (), die nur mit den beiden Zahlenvariablen arbeiten. Wir haben weiter eine Bereich() Methode, die auf die wirkt Länge und Breite Felder.

Es ist offensichtlich, dass diese Klasse für separate Informationsgruppen zuständig ist. Es hat eine sehr geringe Kohäsion. Lass es uns überarbeiten.

Klasse ACohesiveClass private $ firstNumber; private $ secondNumber; Funktion __construct ($ firstNumber, $ secondNumber) $ this-> firstNumber = $ firstNumber; $ this-> secondNumber = $ secondNumber;  function add () return $ this-> firstNumber + $ this-> secondNumber;  function subtract () return $ this-> firstNumber - $ this-> secondNumber; 

Das ist ein stark zusammenhängende Klasse. Warum? Weil jeder Abschnitt dieser Klasse zusammen gehört. Sie sollten sich um Kohäsion bemühen, aber Vorsicht, es kann schwierig sein, dies zu erreichen.

Orthogonalität

In einfachen Worten bezieht sich Orthogonalität auf die Isolierung oder Beseitigung von Nebenwirkungen. Eine Methode, Klasse oder ein Modul, die den Status von anderen nicht verwandten Klassen oder Modulen ändert, ist nicht orthogonal. Beispielsweise ist die Blackbox eines Flugzeugs orthogonal. Es hat seine innere Funktionalität, innere Energiequelle, Mikrofone und Sensoren. Es hat keine Auswirkungen auf das Flugzeug, in dem es sich befindet, oder in der Außenwelt. Es bietet lediglich einen Mechanismus zum Aufzeichnen und Abrufen von Flugdaten.

Ein Beispiel für ein solches nicht orthogonales System ist die Elektronik Ihres Autos. Das Erhöhen der Fahrzeuggeschwindigkeit hat mehrere Nebenwirkungen, z. B. die Erhöhung der Radiolautstärke (unter anderem). Die Geschwindigkeit ist nicht orthogonal zum Auto.

Klassenrechner private $ firstNumber; private $ secondNumber; Funktion __construct ($ firstNumber, $ secondNumber) $ this-> firstNumber = $ firstNumber; $ this-> secondNumber = $ secondNumber;  function add () $ sum = $ this-> firstNumber + $ this-> secondNumber; if ($ sum> 100) (neuer AlertMechanism ()) -> tooBigNumber ($ sum);  return $ summe;  function subtract () return $ this-> firstNumber - $ this-> secondNumber;  class AlertMechanism function tooBigNumber ($ number) echo $ number. 'ist zu groß!'; 

In diesem Beispiel ist die Taschenrechner Klasse hinzufügen() Methode zeigt unerwartetes Verhalten: Es erstellt eine AlertMechanism Objekt und ruft eine seiner Methoden auf. Dies ist ein unerwartetes und unerwünschtes Verhalten. Bibliotheksbenutzer erwarten niemals eine auf dem Bildschirm gedruckte Nachricht. Stattdessen erwarten sie nur die Summe der angegebenen Zahlen.

Klassenrechner private $ firstNumber; private $ secondNumber; Funktion __construct ($ firstNumber, $ secondNumber) $ this-> firstNumber = $ firstNumber; $ this-> secondNumber = $ secondNumber;  function add () return $ this-> firstNumber + $ this-> secondNumber;  function subtract () return $ this-> firstNumber - $ this-> secondNumber;  class AlertMechanism Funktion checkLimits ($ firstNumber, $ secondNumber) $ sum = (neuer Rechner ($ firstNumber, $ secondNumber)) -> add (); if ($ summe> 100) $ this-> tooBigNumber ($ summe);  Funktion tooBigNumber ($ number) echo $ number. 'ist zu groß!'; 

Das ist besser. AlertMechanism hat keine Auswirkung auf Taschenrechner. Stattdessen, AlertMechanism verwendet, was immer es benötigt, um zu bestimmen, ob eine Warnung ausgegeben werden soll.

Abhängigkeit und Kopplung

In den meisten Fällen sind diese beiden Wörter austauschbar. In einigen Fällen wird jedoch ein Begriff einem anderen vorgezogen.

Was ist also ein? Abhängigkeit? Wenn Objekt EIN muss ein Objekt verwenden B, um das vorgeschriebene Verhalten auszuführen, sagen wir das EIN kommt drauf an B. In OOP sind Abhängigkeiten extrem häufig. Objekte arbeiten häufig miteinander und sind voneinander abhängig. Die Beseitigung der Abhängigkeit ist zwar ein edles Streben, aber es ist fast unmöglich, dies zu tun. Abhängigkeiten zu kontrollieren und zu reduzieren ist jedoch vorzuziehen.

Die Begriffe, schwere Kupplung und lose Kopplung, beziehen sich normalerweise darauf, wie stark ein Objekt von anderen Objekten abhängt.

In einem lose gekoppelten System wirken sich Änderungen in einem Objekt weniger auf die anderen Objekte aus, die davon abhängig sind. In solchen Systemen sind Klassen auf Schnittstellen anstatt auf konkrete Implementierungen angewiesen (darüber werden wir später mehr sprechen). Deshalb sind locker gekoppelte Systeme offener für Modifikationen.

Kopplung in einem Feld

Betrachten wir ein Beispiel:

Klasse Anzeige privater $ Rechner; Funktion __construct () $ this-> Rechner = neuer Rechner (1,2); 

Es ist üblich, diese Art von Code zu sehen. Eine Klasse, Anzeige In diesem Fall hängt von der Taschenrechner Klasse durch direkten Verweis auf diese Klasse. Im obigen Code, Anzeige's $ Rechner Feld ist vom Typ Taschenrechner. Das in diesem Feld enthaltene Objekt ist das Ergebnis eines direkten Aufrufs TaschenrechnerKonstruktor.

Kopplung durch Zugriff auf die anderen Klassenmethoden

Überprüfen Sie den folgenden Code, um eine Demonstration dieser Art von Kopplung zu zeigen:

Klasse Anzeige privater $ Rechner; Funktion __construct () $ this-> Rechner = neuer Rechner (1, 2);  function printSum () echo $ this-> rechner-> add (); 

Das Anzeige Klasse nennt die Taschenrechner Objekt ist hinzufügen() Methode. Dies ist eine andere Form der Kopplung, da eine Klasse auf die andere Methode zugreift.

Kopplung nach Methodenreferenz

Sie können auch Klassen mit Methodenverweisen koppeln. Zum Beispiel:

 Klasse Anzeige privater $ Rechner; function __construct () $ this-> calculator = $ this-> makeCalculator ();  function printSum () echo $ this-> rechner-> add ();  function makeCalculator () return new Calculator (1, 2); 

Es ist wichtig zu wissen, dass die makeCalculator () Methode gibt a zurück Taschenrechner Objekt. Dies ist eine Abhängigkeit.

Kopplung durch Polymorphismus

Vererbung ist wahrscheinlich die stärkste Form der Abhängigkeit:

Klasse AdvancedCalculator erweitert den Calculator Funktion Sinus ($ value) return Sin ($ Value); 

Kann nicht nur AdvancedCalculator mache seine Arbeit nicht ohne Taschenrechner, aber es könnte nicht ohne es existieren.

Reduzierung der Kopplung durch Abhängigkeitseinspritzung

Man kann die Kopplung reduzieren, indem man eine Abhängigkeit einbringt. Hier ist ein solches Beispiel:

Klasse Anzeige privater $ Rechner; Funktion __construct (Rechner $ Rechner = Null) $ this-> Rechner = $ Rechner? : $ this-> makeCalculator ();  //… //

Durch die Injektion der Taschenrechner Objekt durch AnzeigeKonstruktor haben wir reduziert AnzeigeAbhängigkeit von der Taschenrechner Klasse. Dies ist jedoch nur die Hälfte der Lösung.

Kopplung mit Schnittstellen reduzieren

Wir können die Kopplung durch die Verwendung von Schnittstellen weiter reduzieren. Zum Beispiel:

interface CanCompute function add (); Funktion subtrahieren ();  class Calculator implementiert CanCompute private $ firstNumber; private $ secondNumber; Funktion __construct ($ firstNumber, $ secondNumber) $ this-> firstNumber = $ firstNumber; $ this-> secondNumber = $ secondNumber;  function add () return $ this-> firstNumber + $ this-> secondNumber;  function subtract () return $ this-> firstNumber - $ this-> secondNumber;  class Anzeige privater $ Rechner; Funktion __construct (CanCompute $ Rechner = Null) $ this-> Rechner = $ Rechner? : $ this-> makeCalculator ();  function printSum () echo $ this-> rechner-> add ();  function makeCalculator () return new Calculator (1, 2); 

Sie können sich das ISP als ein übergeordnetes Kohäsionsprinzip vorstellen.

Dieser Code führt die CanCompute Schnittstelle. Eine Schnittstelle ist so abstrakt wie Sie in OOP bekommen können; Es definiert die Member, die eine Klasse implementieren muss. Im Fall des obigen Beispiels, Taschenrechner implementiert die CanCompute Schnittstelle.

AnzeigeDer Konstruktor erwartet ein Objekt, das implementiert CanCompute. An diesem Punkt, AnzeigeAbhängigkeit von Taschenrechner ist effektiv gebrochen. Zu jeder Zeit können wir eine andere Klasse erstellen, die implementiert CanCompute und ein Objekt dieser Klasse an übergeben AnzeigeKonstruktor. Anzeige hängt jetzt nur noch vom CanCompute Schnittstelle, aber auch diese Abhängigkeit ist optional. Wenn wir keine Argumente übergeben AnzeigeKonstruktor wird einfach einen Klassiker schaffen Taschenrechner Objekt durch Aufruf makeCalculator (). Diese Technik wird häufig verwendet und ist äußerst hilfreich für die testgetriebene Entwicklung (TDD)..


Die SOLID-Prinzipien

SOLID ist ein Satz von Prinzipien für das Schreiben von sauberem Code, der die Änderung, Pflege und Erweiterung in der Zukunft erleichtert. Dies sind Empfehlungen, die sich auf Quellcode beziehen und die Wartbarkeit positiv beeinflussen.

Eine kleine Geschichte

Die SOLID-Prinzipien, auch Agile-Prinzipien genannt, wurden ursprünglich von Robert C. Martin definiert. Obwohl er nicht alle diese Prinzipien erfunden hat, hat er sie zusammengefügt. Weitere Informationen dazu finden Sie in seinem Buch: Agile Softwareentwicklung, Prinzipien, Muster und Praktiken. Die Prinzipien von SOLID decken ein breites Spektrum von Themen ab, aber ich werde sie so einfach präsentieren, wie ich kann. Bei Bedarf können Sie in den Kommentaren nach weiteren Details fragen.

Einzelverantwortungsprinzip (SRP)

Eine Klasse hat eine einzige Verantwortung. Das hört sich vielleicht einfach an, kann jedoch manchmal schwer zu verstehen und in die Praxis umzusetzen sein.

Klasse Reporter function generateIncomeReports (); function generatePaymentsReports (); Funktion computeBalance (); Funktion printReport (); 

Wem nützt das Verhalten dieser Klasse Ihrer Meinung nach? Nun, eine Buchhaltungsabteilung ist eine Option (für das Guthaben), die Finanzabteilung kann eine andere sein (für Einkommens- / Zahlungsberichte), und sogar die Archivabteilung könnte die Berichte drucken und archivieren.

Es gibt vier Gründe, warum Sie diese Klasse möglicherweise ändern müssen. Jede Abteilung möchte, dass ihre jeweiligen Methoden auf ihre Bedürfnisse zugeschnitten sind.

Das SRP empfiehlt, solche Klassen in kleinere, für Beahvior spezifische Klassen aufzuteilen, von denen jede nur einen Grund hat, sich zu ändern. Solche Klassen neigen dazu, stark zusammenhaltend und lose gekoppelt zu sein. In gewissem Sinne ist SRP die Kohäsion, die aus Sicht der Benutzer definiert wird.

Open-Closed-Prinzip (OCP)

Klassen (und Module) sollten die Erweiterung ihrer Funktionalität begrüßen und sich auch Änderungen ihrer aktuellen Funktionalität widersetzen. Spielen wir mit dem klassischen Beispiel eines elektrischen Lüfters. Sie haben einen Schalter und möchten den Lüfter steuern. Sie könnten also etwas schreiben wie:

Klasse Switch_ private $ fan; function __construct () $ this-> fan = neuer Lüfter ();  function turnOn () $ this-> fan-> on ();  function turnOff () $ this-> fan-> off (); 

Vererbung ist wahrscheinlich die stärkste Form der Abhängigkeit.

Dieser Code definiert a Schalter_ Klasse, die a erstellt und steuert Ventilator Objekt. Bitte beachten Sie den Unterstrich nach "Switch_". PHP erlaubt keine Definition einer Klasse mit dem Namen "Switch".

Ihr Chef entscheidet, dass er das Licht mit demselben Schalter steuern möchte. Das ist ein Problem, weil Sie muss sich ändern Schalter_.

Jede Änderung des bestehenden Codes ist ein Risiko. Andere Teile des Systems können betroffen sein und müssen noch weiter modifiziert werden. Es ist immer vorzuziehen, vorhandene Funktionen beim Hinzufügen neuer Funktionen allein zu lassen.

In der OOP-Terminologie können Sie das sehen Schalter_ hat eine starke Abhängigkeit von Ventilator. Hier liegt unser Problem und wir sollten unsere Änderungen vornehmen.

Schnittstelle umschaltbar Funktion ein (); Funktion aus ();  class Lüfter implementiert schaltbare public function on () // Code zum Starten des Lüfters public function off () // Code zum Stoppen des Lüfters class Switch_ private $ switchable; Funktion __construct (umschaltbar $ umschaltbar) $ this-> umschaltbar = $ umschaltbar;  function turnOn () $ this-> umschaltbar-> on ();  function turnOff () $ this-> umschaltbar-> aus (); 

Diese Lösung führt die Umschaltbar Schnittstelle. Sie definiert die Methoden, die alle switchfähigen Objekte implementieren müssen. Das Ventilator implementiert Umschaltbar, und Schalter_ akzeptiert einen Verweis auf a Umschaltbar Objekt innerhalb seines Konstruktors.

Wie hilft uns das??

Erstens bricht diese Lösung die Abhängigkeit zwischen Schalter_ und Ventilator. Schalter_ hat keine Ahnung, dass es einen Fan startet, und es interessiert es auch nicht. Zweitens Einführung von a Licht Klasse wird sich nicht auswirken Schalter_ oder Umschaltbar. Möchten Sie eine Licht Objekt mit Ihrem Schalter_ Klasse? Erstellen Sie einfach eine Licht Objekt und übergeben es an Schalter_, so was:

Klasse Licht implementiert schaltbare öffentliche Funktion ein () // Code zum Einschalten öffentliche Funktion aus () // Code zum Ausschalten des Lichts class SomeWhereInYourCode function controlLight () $ light = new Light (); $ switch = new Schalter _ ($ light); $ switch-> turnOn (); $ switch-> turnOff (); 

Liskov-Substitutionsprinzip (LSP)

Laut LSP darf eine untergeordnete Klasse niemals die Funktionalität der übergeordneten Klasse beschädigen. Dies ist äußerst wichtig, da Konsumenten einer übergeordneten Klasse ein bestimmtes Verhalten der Klasse erwarten. Die Übergabe einer untergeordneten Klasse an einen Verbraucher sollte funktionieren und die ursprüngliche Funktionalität nicht beeinträchtigen.

Dies ist auf den ersten Blick verwirrend. Schauen wir uns ein weiteres klassisches Beispiel an:

Klasse Rechteck privat $ width; private $ height; Funktion setWidth ($ width) $ this-> width = $ width;  function setHeigth ($ heigth) $ this-> height = $ heigth;  Funktionsbereich () return $ this-> width * $ this-> height; 

Dieses Beispiel definiert eine einfache Rechteck Klasse. Wir können seine Höhe und Breite einstellen Bereich() Methode stellt den Bereich des Rechtecks ​​bereit. Verwendung der Rechteck Die Klasse könnte wie folgt aussehen:

Klasse Geometrie Funktion rectArea (Rechteck $ Rechteck) $ Rechteck-> SetWidth (10); $ Rechteck-> SetHeigth (5); return $ Rechteck-> Bereich (); 

Das rectArea () Methode akzeptiert a Rechteck object als Argument, legt seine Höhe und Breite fest und gibt den Bereich der Form zurück.

In der Schule lernen wir, dass Quadrate Rechtecke sind. Dies deutet darauf hin, dass, wenn wir unser Programm an unser geometrisches Objekt modellieren, a Quadrat Klasse sollte ein verlängern Rechteck Klasse. Wie würde eine solche Klasse aussehen??

Klasse Quadrat erweitert Rechteck // Welchen Code soll hier geschrieben werden? 

Es fällt mir schwer, herauszufinden, was ich schreiben soll Quadrat Klasse. Wir haben mehrere Möglichkeiten. Wir könnten das überschreiben Bereich() Methode und geben Sie das Quadrat von $ width:

Klasse Rechteck protected $ width; geschützte $ Höhe; //… // class Square erweitert Rectangle Funktionsbereich () return $ this-> width ^ 2; 

Beachten Sie, dass ich mich geändert habe Rechtecks Felder zu geschützt, geben Quadrat Zugriff auf diese Felder. Dies erscheint aus geometrischer Sicht sinnvoll. Ein Quadrat hat gleiche Seiten; das Quadrat der Breite zurückzugeben ist vernünftig.

Wir haben jedoch ein Problem aus Programmiersicht. Ob Quadrat ist ein Rechteck, Wir sollten kein Problem damit haben, es in die Geometrie Klasse. Aber dadurch können Sie das sehen GeometrieCode macht nicht viel Sinn; Es werden zwei verschiedene Werte für Höhe und Breite festgelegt. Deshalb ein Quadrat ist nicht ein Rechteck in der Programmierung. LSP verletzt.

Schnittstellentrennungsprinzip (ISP)

Unit-Tests sollten schnell ablaufen - sehr schnell.

Dieses Prinzip konzentriert sich darauf, große Schnittstellen in kleine spezialisierte Schnittstellen aufzuteilen. Die Grundidee ist, dass verschiedene Konsumenten derselben Klasse die verschiedenen Schnittstellen nicht kennen sollten - nur die Schnittstellen, die der Konsument verwenden muss. Selbst wenn ein Consumer nicht alle öffentlichen Methoden für ein Objekt direkt verwendet, hängt er dennoch von allen Methoden ab. Warum also nicht Schnittstellen bereitstellen, die nur die Methoden angeben, die jeder Benutzer benötigt?

Dies steht in enger Übereinstimmung, dass Schnittstellen zu den Clients gehören sollten und nicht zur Implementierung. Wenn Sie Ihre Schnittstellen an die konsumierenden Klassen anpassen, werden sie den ISP respektieren. Die Implementierung selbst kann eindeutig sein, da eine Klasse mehrere Schnittstellen implementieren kann.

Stellen wir uns vor, wir implementieren eine Börsenanwendung. Wir haben einen Broker, der Aktien kauft und verkauft, und er kann seine täglichen Gewinne und Verluste melden. Eine sehr einfache Implementierung würde so etwas wie ein Makler Schnittstelle, a NYSEBroker Klasse, die implementiert Makler und ein paar Benutzeroberflächenklassen: eine zum Erstellen von Transaktionen (TransactionsUI) und eine für die Berichterstattung (DailyReporter). Der Code für ein solches System könnte dem folgenden ähneln:

Interface Broker Funktion buy ($ Symbol, $ Volume); Funktion verkaufen ($ Symbol, $ Volume); Funktion dailyLoss ($ date); Funktion dailyEarnings ($ date);  class NYSEBroker implementiert Broker öffentliche Funktion buy ($ -Symbol, $ volume) // Implementierungen geht hier hin öffentliche Funktionen currentBalance () // Implementierungen geht hier hinein öffentliche Funktionen dailyEarnings ($ date) // Implementierungen gehen hier public function dailyLoss ($ date) // Implementierungen gehen hier hin public function sell ($ symbol, $ volume) // Implementierungen gehen hier class TransactionsUI private $ broker; Funktion __construct (Broker $ broker) $ this-> broker = $ broker;  function buyStocks () // UI-Logik hier, um Informationen aus einem Formular in $ data zu erhalten $ this-> broker-> buy ($ data ['sybmol'], $ data ['volume']);  function sellStocks () // UI-Logik hier, um Informationen aus einem Formular in $ data zu erhalten $ this-> broker-> sell ($ data ['sybmol'], $ data ['volume']);  class DailyReporter privater $ broker; Funktion __construct (Broker $ broker) $ this-> broker = $ broker;  function currentBalance () echo 'Aktueller Kontostand für heute'. Terminzeit()) . "\ n"; Echo 'Einnahmen:'. $ this-> broker-> dailyEarnings (time ()). "\ n"; Echo 'Verluste:'. $ this-> broker-> dailyLoss (time ()). "\ n"; 

Während dieser Code möglicherweise funktioniert, verletzt er den ISP. Beide DailyReporter und TransactionUI abhängig von der Makler Schnittstelle. Sie verwenden jedoch jeweils nur einen Bruchteil der Schnittstelle. TransactionUI verwendet die Kaufen() und verkaufen() Methoden, während DailyReporter verwendet die dailyEarnings () und dailyLoss () Methoden.

Sie können das argumentieren Makler ist nicht kohäsiv, weil es Methoden hat, die nicht miteinander zusammenhängen und daher nicht zusammengehören.

Dies kann richtig sein, aber die Antwort hängt von den Implementierungen von ab Makler; Kauf und Verkauf können stark von den laufenden Verlusten und Erträgen abhängen. Beispielsweise können Sie möglicherweise keine Aktien kaufen, wenn Sie Geld verlieren.

Sie können das auch argumentieren Makler verstößt auch gegen SRP. Da wir zwei Klassen haben, die es auf unterschiedliche Weise verwenden, gibt es möglicherweise zwei verschiedene Benutzer. Nun, ich sage nein. Der einzige Benutzer ist wahrscheinlich der eigentliche Broker. Er / sie möchte ihre aktuellen Fonds kaufen, verkaufen und anzeigen. Aber die tatsächliche Antwort hängt auch vom gesamten System und Geschäft ab.

ISP wird sicher verletzt. Beide UI-Klassen hängen vom Ganzen ab Makler. Dies ist ein häufiges Problem, wenn Sie der Meinung sind, dass Schnittstellen zu deren Implementierungen gehören. Das Verschieben Ihres Standpunkts kann jedoch folgendes Design vorschlagen:

Schnittstelle BrokerTransactions Funktion buy ($ Symbol, $ Volume); Funktion verkaufen ($ Symbol, $ Volume);  Schnittstelle BrokerStatistics Funktion dailyLoss ($ date); Funktion dailyEarnings ($ date);  class NYSEBroker implementiert BrokerTransactions, BrokerStatistics öffentliche Funktion buy ($ -Symbol, $ volume) // Implementierungen gehen hier hin öffentliche Funktion currentBalance () // Implementierungen gehen hier öffentliche Funktionen dailyEarnings ($ date) // Implementierungen gehen hier ein  public function dailyLoss ($ date) // Implementierungen gehen hier hin public function verkaufen ($ Symbol, $ volume) // Implementierungen gehen hier class TransactionsUI private $ broker; Funktion __construct (BrokerTransactions $ broker) $ this-> broker = $ broker;  function buyStocks () // UI-Logik hier, um Informationen aus einem Formular in $ data zu erhalten $ this-> broker-> buy ($ data ['sybmol'], $ data ['volume']);  function sellStocks () // UI-Logik hier, um Informationen aus einem Formular in $ data zu erhalten $ this-> broker-> sell ($ data ['sybmol'], $ data ['volume']);  class DailyReporter privater $ broker; Funktion __construct (BrokerStatistics $ broker) $ this-> broker = $ broker;  function currentBalance () echo 'Aktueller Kontostand für heute'. Terminzeit()) . "\ n"; Echo 'Einnahmen:'. $ this-> broker-> dailyEarnings (time ()). "\ n"; Echo 'Verluste:'. $ this-> broker-> dailyLoss (time ()). "\ n"; 

Dies macht tatsächlich Sinn und respektiert den ISP. DailyReporter hängt nur von ab BrokerStatistics; es ist ihm egal und muss nicht über Verkaufs- und Kaufvorgänge Bescheid wissen. TransactionsUI, Auf der anderen Seite weiß nur das Kaufen und Verkaufen. Das NYSEBroker ist identisch mit unserer vorherigen Klasse, außer dass jetzt das implementiert wird BrokerTransactions und BrokerStatistics Schnittstellen.

Sie können sich das ISP als ein übergeordnetes Kohäsionsprinzip vorstellen.

Wenn beide UI-Klassen von der Makler Sie waren zwei Klassen mit jeweils vier Feldern ähnlich, von denen zwei in einer Methode und die anderen zwei in einer anderen Methode verwendet wurden. Die Klasse wäre nicht sehr zusammenhängend gewesen.

Ein komplexeres Beispiel für dieses Prinzip findet sich in einer der ersten Veröffentlichungen von Robert C. Martin zu diesem Thema: Das Prinzip der Schnittstellentrennung.

Prinzip der Inversion der Abhängigkeit (DIP)

Dieses Prinzip besagt, dass High-Level-Module nicht von Low-Level-Modulen abhängig sein sollten. Beides sollte von Abstraktionen abhängen. Abstraktionen sollten nicht von Details abhängen; Details sollten von Abstraktionen abhängen. Vereinfacht gesagt, sollten Sie sich so weit wie möglich auf Abstraktionen verlassen und niemals auf konkrete Implementierungen.

Der Trick bei DIP besteht darin, dass Sie die Abhängigkeit umkehren möchten, aber immer den Steuerungsfluss beibehalten möchten. Lassen Sie uns unser Beispiel vom OCP (the Schalter und Licht Klassen). In der ursprünglichen Implementierung hatten wir einen Schalter, der direkt ein Licht steuert.

Wie Sie sehen, fließen sowohl Abhängigkeit als auch Kontrolle ab Schalter zu Licht. Das ist es, was wir wollen, aber wir wollen uns nicht direkt darauf verlassen Licht. Also haben wir eine Schnittstelle eingeführt.

Es ist erstaunlich, wie einfach durch die Einführung einer Schnittstelle unser Code sowohl DIP als auch OCP berücksichtigt. Wie Sie sehen können, hängt die Klasse von der konkreten Implementierung von ab Licht, und beide Licht und Schalter abhängig von der Umschaltbar Schnittstelle. Wir haben die Abhängigkeit umgekehrt und der Kontrollfluss blieb unverändert.


High Level Design

Ein weiterer wichtiger Aspekt Ihres Codes ist Ihr übergeordnetes Design und Ihre allgemeine Architektur. Eine verschränkte Architektur erzeugt Code, der schwer zu ändern ist. Die Aufrechterhaltung einer sauberen Architektur ist von wesentlicher Bedeutung. Zunächst müssen Sie die unterschiedlichen Anliegen Ihres Codes voneinander trennen.

In diesem Bild habe ich versucht, die wichtigsten Bedenken zusammenzufassen. Im Zentrum des Schemas steht unsere Geschäftslogik. Es sollte vom Rest der Welt gut isoliert sein und in der Lage sein, wie erwartet zu arbeiten und sich zu verhalten, ohne dass andere Teile existieren. Sehen Sie es als Orthogonalität auf einer höheren Ebene.

Von rechts aus haben Sie Ihren "Haupt" - den Einstiegspunkt in die Anwendung - und die Fabriken, die Objekte erstellen. Eine ideale Lösung würde ihre Objekte von spezialisierten Fabriken beziehen, aber das ist meistens unmöglich oder unpraktisch. Dennoch sollten Sie Fabriken verwenden, wenn Sie die Gelegenheit dazu haben, und diese außerhalb Ihrer Geschäftslogik lassen.

Dann haben wir unten (in Orange) eine Persistenz (Datenbanken, Dateizugriffe, Netzwerkkommunikation), um Informationen zu erhalten. Kein Objekt in unserer Geschäftslogik sollte wissen, wie Persistenz funktioniert.

Auf der linken Seite befindet sich der Zustellmechanismus.

Ein MVC wie Laravel oder CakePHP sollte nur der Zustellmechanismus sein, mehr nicht.

Dadurch können Sie einen Mechanismus mit einem anderen austauschen, ohne Ihre Geschäftslogik zu berühren. Dies mag für einige von Ihnen unverschämt klingen. Uns wurde gesagt, dass unsere Geschäftslogik in unsere Modelle aufgenommen werden sollte. Ich stimme dem nicht zu. Unsere Modelle sollten "Anforderungsmodelle" sein, d. H. Dumme Datenobjekte, die zum Weiterleiten von Informationen von MVC an die Geschäftslogik verwendet werden. Optional sehe ich kein Problem, einschließlich der Eingabeprüfung in den Modellen, aber nichts weiter. Geschäftslogik sollte nicht in den Modellen sein.

Wenn Sie sich die Architektur oder Verzeichnisstruktur Ihrer Anwendung ansehen, sollten Sie eine Struktur sehen, die angibt, was das Programm macht, im Gegensatz zu dem verwendeten Framework oder der verwendeten Datenbank.

Stellen Sie schließlich sicher, dass alle Abhängigkeiten auf unsere Geschäftslogik zeigen. Benutzeroberflächen, Fabriken, Datenbanken sind sehr konkrete Implementierungen, auf die Sie sich niemals verlassen sollten. Durch die Umkehrung der Abhängigkeiten in Richtung unserer Geschäftslogik kann unser System modularisiert werden, sodass Abhängigkeiten geändert werden können, ohne die Geschäftslogik zu ändern.


Einige Gedanken zu Design Patterns

Entwurfsmuster spielen eine wichtige Rolle bei der Erleichterung der Modifizierbarkeit von Code, indem sie eine gemeinsame Entwurfslösung anbieten, die jeder Programmierer verstehen kann. Aus struktureller Sicht sind Entwurfsmuster offensichtlich vorteilhaft. Sie sind gut erprobte und durchdachte Lösungen.

Wenn Sie mehr über Designmuster erfahren möchten, habe ich einen Tuts + Premium-Kurs erstellt!


Die Kraft der Prüfung

Test-Driven Development unterstützt das Schreiben von Code, der leicht zu testen ist. TDD zwingt Sie, die meisten der oben genannten Prinzipien einzuhalten, damit Ihr Code einfach getestet werden kann. Das Injizieren von Abhängigkeiten und das Schreiben von orthogonalen Klassen sind unerlässlich. Andernfalls erhalten Sie riesige Testmethoden. Unit-Tests sollten schnell ablaufen - eigentlich sehr schnell, und alles, was nicht getestet wird, sollte verspottet werden. Viele komplexe Klassen für einen einfachen Test zu verspotten, kann überwältigend sein. Wenn Sie also zehn Objekte verspotten, um eine einzelne Methode in einer Klasse zu testen, haben Sie möglicherweise ein Problem mit Ihrem Code… nicht mit Ihrem Test.


Abschließende Gedanken

Am Ende des Tages kommt es darauf an, wie viel Sie Ihren Quellcode interessieren. Technisches Wissen zu haben reicht nicht aus; Sie müssen dieses Wissen immer wieder a