FESTE Prinzipien, über die jeder Entwickler Bescheid wissen sollte

Die objektorientierte Programmierung hat neue Ansätze für das Anwendungsdesign in die Softwareentwicklung gebracht. Insbesondere ermöglichte OOP Programmierern, Entitäten, die durch ein gemeinsames Ziel oder eine gemeinsame Funktionalität vereint sind, in separaten Klassen zu kombinieren, um unabhängige Probleme zu lösen und unabhängig von anderen Teilen der Anwendung. Die Verwendung von OOP allein bedeutet jedoch nicht, dass der Entwickler vor der Möglichkeit geschützt ist, obskuren, verwirrenden Code zu erstellen, der schwer zu warten ist. Um allen zu helfen, die hochwertige OOP-Anwendungen entwickeln möchten, hat Robert Martin fünf Prinzipien der objektorientierten Programmierung und des objektorientierten Designs entwickelt, über die mit Hilfe von Michael Fazers das Akronym SOLID verwendet wird.



Das Material, dessen Übersetzung wir heute veröffentlichen, ist den Grundlagen von SOLID gewidmet und richtet sich an Anfänger.

Was ist FEST?


So steht das Akronym SOLID für:

  • S: Prinzip der Einzelverantwortung.
  • O: Offen-Geschlossen-Prinzip.
  • L: Liskov-Substitutionsprinzip (Barbara-Liskov-Substitutionsprinzip).
  • I: Prinzip der Schnittstellentrennung.
  • D: Prinzip der Abhängigkeitsinversion.

Nun werden wir diese Prinzipien in schematischen Beispielen betrachten. Beachten Sie, dass der Hauptzweck der Beispiele darin besteht, dem Leser zu helfen, die Prinzipien von SOLID zu verstehen, zu lernen, wie man sie anwendet und wie man sie beim Entwerfen von Anwendungen befolgt. Der Autor des Materials war nicht bestrebt, einen Arbeitscode zu erreichen, der in realen Projekten verwendet werden kann.

Grundsatz der alleinigen Verantwortung


„Ein Auftrag. Nur eine Sache. " - Loki erzählt Skurge im Film Thor: Ragnarok.
Jede Klasse sollte nur ein Problem lösen.

Eine Klasse sollte nur für eine Sache verantwortlich sein. Wenn eine Klasse für die Lösung mehrerer Probleme verantwortlich ist, erweisen sich ihre Subsysteme, die die Lösung dieser Probleme implementieren, als miteinander verbunden. Änderungen in einem solchen Subsystem führen zu Änderungen in einem anderen.

Beachten Sie, dass dieses Prinzip nicht nur für Klassen gilt, sondern auch für Softwarekomponenten im weiteren Sinne.

Betrachten Sie beispielsweise diesen Code:

class Animal {    constructor(name: string){ }    getAnimalName() { }    saveAnimal(a: Animal) { } } 

Die hier vorgestellte Tierklasse beschreibt eine Art Tier. Diese Klasse verstößt gegen das Prinzip der alleinigen Verantwortung. Wie genau wird dieses Prinzip verletzt?

Nach dem Prinzip der alleinigen Verantwortung darf eine Klasse nur eine Aufgabe lösen. Er löst die beiden saveAnimal indem er mit dem Data Warehouse in der saveAnimal Methode arbeitet und die Eigenschaften des Objekts im Konstruktor und in der getAnimalName Methode getAnimalName .

Wie kann eine solche Klassenstruktur zu Problemen führen?

Wenn sich das Verfahren für die Arbeit mit dem von der Anwendung verwendeten Data Warehouse ändert, müssen Sie Änderungen an allen Klassen vornehmen, die mit dem Warehouse arbeiten. Diese Architektur ist nicht flexibel, Änderungen in einigen Subsystemen wirken sich auf andere aus, was dem Dominoeffekt ähnelt.

Um den obigen Code mit dem Prinzip der alleinigen Verantwortung in Einklang zu bringen, erstellen wir eine weitere Klasse, deren einzige Aufgabe darin besteht, mit dem Repository zu arbeiten, insbesondere Objekte der Animal Klasse darin zu speichern:

 class Animal {   constructor(name: string){ }   getAnimalName() { } } class AnimalDB {   getAnimal(a: Animal) { }   saveAnimal(a: Animal) { } } 

Dazu sagt Steve Fenton: „Beim Entwerfen von Klassen sollten wir uns bemühen, verwandte Komponenten zu integrieren, dh solche, bei denen Änderungen aus den gleichen Gründen auftreten. Wir sollten versuchen, die Komponenten zu trennen, Änderungen, die verschiedene Gründe haben. "

Die korrekte Anwendung des Grundsatzes der alleinigen Verantwortung führt zu einem hohen Maß an Konnektivität der Elemente innerhalb des Moduls, dh zu der Tatsache, dass die darin gelösten Aufgaben gut seinem Hauptziel entsprechen.

Open-Closed-Prinzip


Software-Entitäten (Klassen, Module, Funktionen) sollten zur Erweiterung geöffnet sein, jedoch nicht zur Änderung.

Wir arbeiten weiter an der Animal .

 class Animal {   constructor(name: string){ }   getAnimalName() { } } 

Wir wollen die Liste der Tiere sortieren, von denen jedes durch ein Objekt der Animal , und herausfinden, welche Geräusche sie machen. Stellen Sie sich vor, wir lösen dieses Problem mit der AnimalSounds Funktion:

 //... const animals: Array<Animal> = [   new Animal('lion'),   new Animal('mouse') ]; function AnimalSound(a: Array<Animal>) {   for(int i = 0; i <= a.length; i++) {       if(a[i].name == 'lion')           return 'roar';       if(a[i].name == 'mouse')           return 'squeak';   } } AnimalSound(animals); 

Das Hauptproblem bei dieser Architektur besteht darin, dass die Funktion bestimmt, welche Art von Geräusch ein Tier bei der Analyse bestimmter Objekte macht. Die AnimalSound Funktion AnimalSound nicht dem Prinzip der Offenheit, da wir beispielsweise neue Tierarten ändern müssen, um die von ihnen erzeugten Geräusche zu erkennen.

Fügen Sie dem Array ein neues Element hinzu:

 //... const animals: Array<Animal> = [   new Animal('lion'),   new Animal('mouse'),   new Animal('snake') ] //... 

Danach müssen wir den Code der AnimalSound Funktion AnimalSound :

 //... function AnimalSound(a: Array<Animal>) {   for(int i = 0; i <= a.length; i++) {       if(a[i].name == 'lion')           return 'roar';       if(a[i].name == 'mouse')           return 'squeak';       if(a[i].name == 'snake')           return 'hiss';   } } AnimalSound(animals); 

Wie Sie sehen, müssen Sie beim Hinzufügen eines neuen Tieres zum Array den Funktionscode ergänzen. Ein Beispiel ist sehr einfach, aber wenn eine ähnliche Architektur in einem realen Projekt verwendet wird, muss die Funktion ständig erweitert werden und neue if Ausdrücke hinzugefügt werden.

Wie kann die AnimalSound Funktion mit dem Prinzip von offen-geschlossen in Einklang gebracht werden? Zum Beispiel so:

 class Animal {       makeSound();       //... } class Lion extends Animal {   makeSound() {       return 'roar';   } } class Squirrel extends Animal {   makeSound() {       return 'squeak';   } } class Snake extends Animal {   makeSound() {       return 'hiss';   } } //... function AnimalSound(a: Array<Animal>) {   for(int i = 0; i <= a.length; i++) {       a[i].makeSound();   } } AnimalSound(animals); 

Möglicherweise stellen Sie fest, dass die Animal Klasse jetzt über eine virtuelle makeSound Methode verfügt. Bei diesem Ansatz ist es erforderlich, dass Klassen zur Beschreibung bestimmter Tiere die Animal erweitern und diese Methode implementieren.

Infolgedessen verfügt jede Klasse, die ein Tier beschreibt, über eine eigene makeSound Methode. Wenn Sie ein Array mit Tieren in der AnimalSound Funktion AnimalSound , reicht AnimalSound aus, diese Methode für jedes Element des Arrays aufzurufen.

Wenn Sie dem Array jetzt ein Objekt hinzufügen, das das neue Tier beschreibt, müssen Sie die AnimalSound Funktion nicht ändern. Wir haben es mit dem Prinzip der Offenheit und Nähe in Einklang gebracht.

Betrachten Sie ein anderes Beispiel.

Angenommen, wir haben ein Geschäft. Wir gewähren Kunden einen Rabatt von 20% in dieser Klasse:

 class Discount {   giveDiscount() {       return this.price * 0.2   } } 

Nun wurde beschlossen, die Kunden in zwei Gruppen aufzuteilen. Lieblingskunden erhalten einen Rabatt von 20% und VIP-Kunden (VIP) den doppelten Rabatt, fav 40%. Um diese Logik zu implementieren, wurde beschlossen, die Klasse wie folgt zu ändern:

 class Discount {   giveDiscount() {       if(this.customer == 'fav') {           return this.price * 0.2;       }       if(this.customer == 'vip') {           return this.price * 0.4;       }   } } 

Dieser Ansatz verstößt gegen das Prinzip der Offenheit-Nähe. Wie Sie sehen können, müssen wir der Klasse einen neuen Code hinzufügen, wenn wir einer bestimmten Kundengruppe einen Sonderrabatt gewähren müssen.

Um diesen Code gemäß dem Prinzip der Offenheit und Nähe zu verarbeiten, fügen wir dem Projekt eine neue Klasse hinzu, die die Discount Klasse erweitert. In dieser neuen Klasse implementieren wir einen neuen Mechanismus:

 class VIPDiscount: Discount {   getDiscount() {       return super.getDiscount() * 2;   } } 

Wenn Sie „Super-VIP“ -Kunden einen Rabatt von 80% gewähren, sollte dies folgendermaßen aussehen:

 class SuperVIPDiscount: VIPDiscount {   getDiscount() {       return super.getDiscount() * 2;   } } 

Wie Sie sehen können, wird hier die Ermächtigung von Klassen verwendet, nicht deren Änderung.

Das Substitutionsprinzip von Barbara Liskov


Es ist notwendig, dass Unterklassen als Ersatz für ihre Oberklassen dienen.

Der Zweck dieses Prinzips besteht darin, dass Vererbungsklassen anstelle der übergeordneten Klassen verwendet werden können, aus denen sie gebildet werden, ohne das Programm zu stören. Wenn sich herausstellt, dass der Klassentyp im Code überprüft wird, wird das Substitutionsprinzip verletzt.

Betrachten Sie die Anwendung dieses Prinzips und kehren Sie zum Beispiel mit der Animal . Wir werden eine Funktion schreiben, die Informationen über die Anzahl der Gliedmaßen eines Tieres zurückgibt.

 //... function AnimalLegCount(a: Array<Animal>) {   for(int i = 0; i <= a.length; i++) {       if(typeof a[i] == Lion)           return LionLegCount(a[i]);       if(typeof a[i] == Mouse)           return MouseLegCount(a[i]);       if(typeof a[i] == Snake)           return SnakeLegCount(a[i]);   } } AnimalLegCount(animals); 

Die Funktion verstößt gegen das Substitutionsprinzip (und das Prinzip der Offenheitsschließung). Dieser Code sollte die Typen aller von ihm verarbeiteten Objekte kennen und je nach Typ die entsprechende Funktion verwenden, um die Gliedmaßen eines bestimmten Tieres zu berechnen. Daher muss beim Erstellen eines neuen Tiertyps die Funktion neu geschrieben werden:

 //... class Pigeon extends Animal {      } const animals[]: Array<Animal> = [   //...,   new Pigeon(); ] function AnimalLegCount(a: Array<Animal>) {   for(int i = 0; i <= a.length; i++) {       if(typeof a[i] == Lion)           return LionLegCount(a[i]);       if(typeof a[i] == Mouse)           return MouseLegCount(a[i]);        if(typeof a[i] == Snake)           return SnakeLegCount(a[i]);       if(typeof a[i] == Pigeon)           return PigeonLegCount(a[i]);   } } AnimalLegCount(animals); 

Damit diese Funktion nicht gegen das Substitutionsprinzip verstößt, transformieren wir sie anhand der von Steve Fenton formulierten Anforderungen. Sie bestehen in der Tatsache, dass Methoden, die Werte mit dem Typ einer Oberklasse akzeptieren oder zurückgeben (in unserem Fall Animal ), auch Werte akzeptieren und zurückgeben sollten, deren Typen ihre Unterklassen sind ( Pigeon ).

Mit diesen Überlegungen können wir die AnimalLegCount Funktion wiederholen:

 function AnimalLegCount(a: Array<Animal>) {   for(let i = 0; i <= a.length; i++) {       a[i].LegCount();   } } AnimalLegCount(animals); 

Diese Funktion interessiert sich jetzt nicht mehr für die Arten von Objekten, die an sie übergeben werden. Sie ruft einfach ihre LegCount Methoden auf. Alles, was sie über Typen weiß, ist, dass die Objekte, die sie verarbeitet, zur Animal Klasse oder ihren Unterklassen gehören müssen.

Die LegCount Methode sollte jetzt in der Animal Klasse LegCount werden:

 class Animal {   //...   LegCount(); } 

Und seine Unterklassen müssen diese Methode implementieren:

 //... class Lion extends Animal{   //...   LegCount() {       //...   } } //... 

Wenn Sie beispielsweise auf die LegCount Methode für eine Instanz der Lion Klasse zugreifen, wird die in dieser Klasse implementierte Methode aufgerufen und genau das zurückgegeben, was vom Aufruf einer solchen Methode erwartet werden kann.

Jetzt muss die AnimalLegCount Funktion AnimalLegCount mehr wissen, welches Objekt einer bestimmten Unterklasse der Animal Klasse sie verarbeitet, um Informationen über die Anzahl der Gliedmaßen in dem von diesem Objekt dargestellten Tier zu erhalten. Die Funktion ruft einfach die LegCount Methode der Animal Klasse auf, da Unterklassen dieser Klasse diese Methode implementieren müssen, damit sie stattdessen verwendet werden können, ohne die korrekte Operation des Programms zu verletzen.

Prinzip der Schnittstellentrennung


Erstellen Sie hochspezialisierte Schnittstellen für einen bestimmten Client. Clients sollten nicht auf Schnittstellen angewiesen sein, die sie nicht verwenden.

Dieses Prinzip zielt darauf ab, die mit der Implementierung großer Schnittstellen verbundenen Mängel zu beheben.

Betrachten Sie die Shape Oberfläche:

 interface Shape {   drawCircle();   drawSquare();   drawRectangle(); } 

Es werden Methoden zum Zeichnen von Kreisen ( drawCircle ), Quadraten ( drawSquare ) und Rechtecken ( drawRectangle ) beschrieben. Daher müssen Klassen, die diese Schnittstelle implementieren und einzelne geometrische Formen darstellen, wie z. B. ein Kreis, ein Quadrat und ein Rechteck, eine Implementierung all dieser Methoden enthalten. Es sieht so aus:

 class Circle implements Shape {   drawCircle(){       //...   }   drawSquare(){       //...   }   drawRectangle(){       //...   } } class Square implements Shape {   drawCircle(){       //...   }   drawSquare(){       //...   }   drawRectangle(){       //...   } } class Rectangle implements Shape {   drawCircle(){       //...   }   drawSquare(){       //...   }   drawRectangle(){       //...   } } 

Es stellte sich heraus, dass seltsamer Code vorhanden war. Beispielsweise implementiert die Rectangle Klasse, die ein Rechteck darstellt, Methoden ( drawCircle und drawSquare ), die überhaupt nicht benötigt werden. Das gleiche gilt für die Analyse des Codes von zwei anderen Klassen.

Angenommen, wir beschließen, der drawTriangle eine andere Methode hinzuzufügen, drawTriangle , mit der Dreiecke drawTriangle werden sollen:

 interface Shape {   drawCircle();   drawSquare();   drawRectangle();   drawTriangle(); } 

Dies führt dazu, dass Klassen, die bestimmte geometrische Formen darstellen, auch die drawTriangle Methode implementieren drawTriangle . Andernfalls tritt ein Fehler auf.

Wie Sie sehen können, ist es mit diesem Ansatz unmöglich, eine Klasse zu erstellen, die eine Methode zur Ausgabe eines Kreises implementiert, jedoch keine Methoden zum Ableiten eines Quadrats, Rechtecks ​​und Dreiecks. Solche Verfahren können so implementiert werden, dass bei ihrer Ausgabe ein Fehler ausgegeben wird, der anzeigt, dass eine solche Operation nicht ausgeführt werden kann.

Das Prinzip der Schnittstellentrennung warnt uns davor, Schnittstellen wie Shape aus unserem Beispiel zu erstellen. Clients (wir haben die Klassen Circle , Square und Rectangle ) sollten keine Methoden implementieren, die sie nicht verwenden müssen. Darüber hinaus gibt dieses Prinzip an, dass die Schnittstelle nur eine Aufgabe lösen sollte (dies ähnelt dem Prinzip der alleinigen Verantwortung), daher sollte alles, was über den Umfang dieser Aufgabe hinausgeht, auf eine andere Schnittstelle oder Schnittstellen übertragen werden.

In unserem Fall löst die Shape Schnittstelle Probleme, für deren Lösung separate Schnittstellen erstellt werden müssen. Nach dieser Idee überarbeiten wir den Code, indem wir separate Schnittstellen zur Lösung verschiedener hochspezialisierter Aufgaben erstellen:

 interface Shape {   draw(); } interface ICircle {   drawCircle(); } interface ISquare {   drawSquare(); } interface IRectangle {   drawRectangle(); } interface ITriangle {   drawTriangle(); } class Circle implements ICircle {   drawCircle() {       //...   } } class Square implements ISquare {   drawSquare() {       //...   } } class Rectangle implements IRectangle {   drawRectangle() {       //...   } } class Triangle implements ITriangle {   drawTriangle() {       //...   } } class CustomShape implements Shape {  draw(){     //...  } } 

Jetzt wird die ICircle Oberfläche nur zum Zeichnen von Kreisen sowie von anderen speziellen Schnittstellen zum Zeichnen anderer Formen verwendet. Die Shape Schnittstelle kann als universelle Schnittstelle verwendet werden.

Prinzip der Abhängigkeitsinversion


Das Objekt der Abhängigkeit sollte eine Abstraktion sein, nicht etwas Spezifisches.

  1. Module der oberen Ebene sollten nicht von Modulen der unteren Ebene abhängen. Beide Modultypen sollten von Abstraktionen abhängen.
  2. Abstraktionen sollten nicht von den Details abhängen. Details sollten von Abstraktionen abhängen.

Während der Softwareentwicklung gibt es einen Moment, in dem die Funktionalität der Anwendung nicht mehr in dasselbe Modul passt. In diesem Fall müssen wir das Problem der Modulabhängigkeiten lösen. Infolgedessen kann sich beispielsweise herausstellen, dass die Komponenten auf hoher Ebene von den Komponenten auf niedriger Ebene abhängen.

 class XMLHttpService extends XMLHttpRequestService {} class Http {   constructor(private xmlhttpService: XMLHttpService) { }   get(url: string , options: any) {       this.xmlhttpService.request(url,'GET');   }   post() {       this.xmlhttpService.request(url,'POST');   }   //... } 

Hier ist die Http Klasse eine XMLHttpService Komponente, und der XMLHttpService ist eine XMLHttpService Komponente. Eine solche Architektur verstößt gegen Klausel A des Abhängigkeitsinversionsprinzips: „Module höherer Ebenen sollten nicht von Modulen niedrigerer Ebenen abhängen. Beide Modultypen sollten von Abstraktionen abhängen. “

Die XMLHttpService Klasse muss von der XMLHttpService Klasse abhängen. Wenn wir den von der Http Klasse für die Interaktion mit dem Netzwerk verwendeten Mechanismus ändern möchten, beispielsweise einen Node.js-Dienst oder beispielsweise einen zu Testzwecken verwendeten Stub-Dienst, müssen wir alle Instanzen der Http Klasse bearbeiten, indem wir den entsprechenden Code ändern. Dies verstößt gegen das Prinzip der Offenheit-Nähe.

Die Http Klasse sollte nicht wissen, was genau zum Herstellen einer Netzwerkverbindung verwendet wird. Daher erstellen wir die Connection :

 interface Connection {   request(url: string, opts:any); } 

Die Connection enthält eine Beschreibung der request , und wir übergeben das Argument für den Connection an die Http Klasse:

 class Http {   constructor(private httpConnection: Connection) { }   get(url: string , options: any) {       this.httpConnection.request(url,'GET');   }   post() {       this.httpConnection.request(url,'POST');   }   //... } 

Unabhängig davon, was zum Organisieren der Interaktion mit dem Netzwerk verwendet wird, kann die Http Klasse nun das verwenden, was an sie übergeben wurde, ohne sich Gedanken darüber machen zu müssen, was sich hinter der Connection verbirgt.

Wir schreiben die XMLHttpService Klasse neu, damit sie diese Schnittstelle implementiert:

 class XMLHttpService implements Connection {   const xhr = new XMLHttpRequest();   //...   request(url: string, opts:any) {       xhr.open();       xhr.send();   } } 

Infolgedessen können wir viele Klassen erstellen, die die Connection implementieren und für die Verwendung in der Http Klasse zum Organisieren des Datenaustauschs über das Netzwerk geeignet sind:

 class NodeHttpService implements Connection {   request(url: string, opts:any) {       //...   } } class MockHttpService implements Connection {   request(url: string, opts:any) {       //...   } } 

Wie Sie sehen können, hängen die Module auf hoher und niedriger Ebene von Abstraktionen ab. Die Http Klasse (High-Level-Modul) hängt von der Connection (Abstraktion) ab. Die MockHttpService XMLHttpService , NodeHttpService und MockHttpService (Module auf niedriger Ebene) hängen auch von der Connection .

Darüber hinaus ist anzumerken, dass wir nach dem Prinzip der Abhängigkeitsinversion das Prinzip der Substitution Barbara Liskov beobachten. Es stellt sich nämlich heraus, dass die Typen XMLHttpService , NodeHttpService und MockHttpService als Ersatz für den NodeHttpService MockHttpService dienen können.

Zusammenfassung


Hier haben wir uns fünf SOLID-Prinzipien angesehen, die jeder OOP-Entwickler einhalten sollte. Zunächst mag dies nicht einfach sein, aber wenn Sie danach streben und die Wünsche der Praxis bekräftigen, werden diese Prinzipien zu einem natürlichen Bestandteil des Workflows, was sich sehr positiv auf die Qualität der Anwendungen auswirkt und deren Unterstützung erheblich erleichtert.

Liebe Leser! Verwenden Sie in Ihren Projekten SOLID-Prinzipien?

Source: https://habr.com/ru/post/de426413/


All Articles