paint-brush
Clean Code: Einzelverantwortung, offen/geschlossen, Liskov-Substitution SOLID-Prinzipien in TS [Teil 4]von@alenaananich
4,731 Lesungen
4,731 Lesungen

Clean Code: Einzelverantwortung, offen/geschlossen, Liskov-Substitution SOLID-Prinzipien in TS [Teil 4]

von Alena Ananich7m2023/11/09
Read on Terminal Reader

Zu lang; Lesen

In diesem Artikel werfen wir einen Blick auf die drei ersten SOLID-Prinzipien: Das Single-Responsibility-Prinzip, das Open/Closed-Prinzip und das Liskov-Substitutionsprinzip anhand praktischer Beispiele.
featured image - Clean Code: Einzelverantwortung, offen/geschlossen, Liskov-Substitution SOLID-Prinzipien in TS [Teil 4]
Alena Ananich HackerNoon profile picture


Vorherige Teile:


Wir denken weiterhin über Ansätze nach, wie wir unseren Code sauber, flexibel und wartbar machen können. Beginnen wir nun mit der Untersuchung sauberer architektonischer Lösungen.


Die SOLID-Prinzipien wurden von Robert C. Martin eingeführt und gelten weithin als Best Practices in der objektorientierten Programmierung und im Softwaredesign.


In diesem Artikel werfen wir einen Blick auf die drei ersten SOLID-Prinzipien: Das Single-Responsibility-Prinzip, das Open/Closed-Prinzip und das Liskov-Substitutionsprinzip anhand praktischer Beispiele.

Inhaltsverzeichnis:

  1. Prinzip der Einzelverantwortung
  2. Offen/Geschlossen-Prinzip
  3. Liskov-Substitutionsprinzip

1. Single-Responsibility-Prinzip (SRP)

Eine Klasse sollte nur einen Grund haben, sich zu ändern


Mit anderen Worten: Eine Klasse sollte eine einzige Verantwortung oder Aufgabe haben. Dies ist wichtig, da eine Klasse mit einer einzigen Verantwortung leichter zu verstehen, zu ändern und zu warten ist. Änderungen in einem Bereich des Codes wirken sich nicht auf das gesamte System aus, wodurch das Risiko der Einführung von Fehlern verringert wird.


Werfen wir einen Blick auf ein praktisches Beispiel und Möglichkeiten, dem Prinzip der Einzelverantwortung zu folgen:

 // Bad class UserSettingsService { constructor(user: IUser) { this.user = user; } changeSettings(settings: IUserSettings): void { if (this.isUserValidated()) { // ... } } getUserInfo(): Promise<IUserSettings> { // ... } async isUserValidated(): Promise<boolean> { const userInfo = await this.getUserInfo(); // ... } }

In diesem Beispiel führt unsere Klasse Aktionen in verschiedene Richtungen aus: Sie richtet den Kontext ein, ändert ihn und validiert ihn.


Um SRP zu befolgen, müssen wir diese unterschiedlichen Verantwortlichkeiten aufteilen.

 // Better class UserAuth { constructor(user: IUser) { this.user = user; } getUserInfo(): Promise<IUserSettings> { // ... } async isUserValidated(): boolean { const userInfo = await this.getUserInfo(); // ... } } class UserSettings { constructor(user: IUser) { this.user = user; this.auth = new UserAuth(user); } changeSettings(settings: IUserSettings): void { if (this.auth.isUserValidated()) { // ... } } }


Warum ist es wichtig?

SRP zielt darauf ab, die Codemodularität zu verbessern und die Herausforderungen, die sich aus gegenseitigen Abhängigkeiten ergeben, zu minimieren. Durch die Organisation des Codes in Funktionen, getrennte Klassen und die Förderung der Modularität wird er wiederverwendbar und spart Zeit, die andernfalls für die Neucodierung vorhandener Funktionen aufgewendet werden müsste.

2. Offenes/Geschlossenes Prinzip (OCP)

Softwareeinheiten (Klassen, Module, Funktionen) sollten für Erweiterungen offen, für Änderungen jedoch geschlossen sein


Sie sollten in der Lage sein, neue Funktionen hinzuzufügen, ohne den vorhandenen Code zu ändern. Dies ist wichtig, da Sie durch die Einhaltung dieses Prinzips neue Funktionen oder Komponenten in Ihr System einführen können, ohne die Stabilität des vorhandenen Codes zu gefährden.


Es fördert die Wiederverwendbarkeit von Code und reduziert die Notwendigkeit umfangreicher Änderungen bei der Erweiterung der Funktionalität.

 // Bad class Product { id: number; name: string[]; price: number; protected constructor(id: number, name: string[], price: number) { this.id = id; this.name = name; this.price = price; } } class Ananas extends Product { constructor(id: number, name: string[], price: number) { super(id, name, price); } } class Banana extends Product { constructor(id: number, name: string[], price: string) { super(id, name, price); } } class HttpRequestCost { constructor(product: Product) { this.product = product; } getDeliveryCost(): number { if (product instanceOf Ananas) { return requestAnanas(url).then(...); } if (product instanceOf Banana) { return requestBanana(url).then(...); } } } function requestAnanas(url: string): Promise<ICost> { // logic for ananas } function requestBanana(url: string): Promise<ICost> { // logic for bananas }

In diesem Beispiel liegt das Problem in der Klasse HttpRequestCost , die in der Methode getDeliveryCost Bedingungen für die Berechnung verschiedener Produkttypen enthält, und wir verwenden für jeden Produkttyp separate Methoden. Wenn wir also einen neuen Produkttyp hinzufügen müssen, sollten wir die HttpRequestCost Klasse ändern. Dies ist nicht sicher. wir könnten unerwartete Ergebnisse erzielen.


Um dies zu vermeiden, sollten wir eine abstrakte request in der Product ohne Realisierungen erstellen. Die besondere Erkenntnis wird die Klassen geerbt haben: Ananas und Banane. Sie werden die Bitte für sich selbst realisieren.


HttpRequestCost übernimmt den product , der der Product folgt, und wenn wir bestimmte Abhängigkeiten in HttpRequestCost übergeben, wird die request bereits für sich selbst realisiert.

 // Better abstract class Product { id: number; name: string[]; price: string; constructor(id: number, name: string[], price: string) { this.id = id; this.name = name; this.price = price; } abstract request(url: string): void; } class Ananas extends Product { constructor(id: number, name: string[], price: string) { super(id, name, price); } request(url: string): void { // logic for ananas } } class Banana extends Product { constructor(id: number, name: string[], price: string) { super(id, name, price); } request(url: string): void { // logic for bananas } } class HttpRequestCost { constructor(product: Product) { this.product = product; } request(): Promise<void> { return this.product.request(url).then(...); } }


Warum ist es wichtig?

Wenn Sie diesem Prinzip folgen, reduzieren Sie die Codekopplung und bewahren das System vor unvorhersehbarem Verhalten.

3. Liskov-Substitutionsprinzip (LSP)

Objekte einer Oberklasse sollten durch Objekte ihrer Unterklassen ersetzbar sein, ohne die Anwendung zu beeinträchtigen.


Um dieses Prinzip zu verstehen, schauen wir uns dieses Beispiel an:

 // Bad class Worker { work(): void {/../} access(): void { console.log('Have an access to closed perimeter'); } } class Programmer extends Worker { createDatabase(): void {/../} } class Seller extends Worker { sale(): void {/../} } class Designer extends Worker { access(): void { throwError('No access'); } }

In diesem Beispiel haben wir ein Problem mit der Contractor Klasse. Designer , Programmer und Seller sind alle Worker und haben von der übergeordneten Klasse Worker geerbt. Gleichzeitig haben Designer jedoch keinen Zugriff auf den geschlossenen Bereich, da sie Auftragnehmer und keine Mitarbeiter sind. Und wir haben die access außer Kraft gesetzt und das Liskov-Substitutionsprinzip gebrochen.


Dieses Prinzip besagt, dass die Funktionalität nicht beeinträchtigt werden sollte, wenn wir die Oberklasse Worker durch ihre Unterklasse, beispielsweise Designer Klasse, ersetzen. Wenn wir dies jedoch tun, wird die Funktionalität der Programmer beeinträchtigt – die access erhält unerwartete Erkenntnisse aus der Designer Klasse.


Gemäß dem Liskov-Substitutionsprinzip sollten wir die Realisierungen nicht in Unterklassen umschreiben, sondern müssen neue Abstraktionsebenen erstellen, in denen wir für jeden Abstraktionstyp bestimmte Realisierungen definieren.


Korrigieren wir es:

 // Better class Worker { work(): void {/../} } class Employee extends Worker { access(): void { console.log('Have an access to closed perimeter'); } } class Contractor extends Worker { addNewContract(): void {/../} } class Programmer extends Employee { createDatabase(): void {/../} } class Saler extends Employee { sale(): void {/../} } class Designer extends Contractor { makeDesign(): void {/../} }

Wir haben neue Ebenen der Abstraktionen Employee und Contractor erstellt, die access in die Klasse „ Employee verschoben und eine spezifische Realisierung definiert. Wenn wir Worker Klasse durch die Unterklasse Contractor ersetzen, wird die Worker Funktionalität nicht beeinträchtigt.

Warum ist es wichtig?

Wenn Sie sich an LSP halten, können Sie jede Unterklasseninstanz überall dort ersetzen, wo eine Basisklasseninstanz erwartet wird, und das Programm sollte weiterhin wie vorgesehen funktionieren. Dies fördert die Wiederverwendung von Code und die Modularität und macht Ihren Code widerstandsfähiger gegenüber Änderungen.


Im nächsten Artikel werfen wir einen Blick auf die SOLID-Prinzipien der Schnittstellentrennung und der Abhängigkeitsinversion.