Vorherige Teile: Sauberer Code: Funktionen und Methoden in TypeScript [Teil 1] Sauberer Code: Benennung und Codekomposition in TypeScript [Teil 2] Sauberer Code: Klassen und Objekte in TypeScript [Teil 3] 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: Prinzip der Einzelverantwortung Offen/Geschlossen-Prinzip 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 , die in der Methode 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 Klasse ändern. Dies ist nicht sicher. wir könnten unerwartete Ergebnisse erzielen. HttpRequestCost getDeliveryCost HttpRequestCost Um dies zu vermeiden, sollten wir eine abstrakte in der ohne Realisierungen erstellen. Die besondere Erkenntnis wird die Klassen geerbt haben: Ananas und Banane. Sie werden die Bitte für sich selbst realisieren. request Product übernimmt den , der der folgt, und wenn wir bestimmte Abhängigkeiten in übergeben, wird die bereits für sich selbst realisiert. HttpRequestCost product Product HttpRequestCost request // 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 Klasse. , und sind alle Worker und haben von der übergeordneten Klasse geerbt. Gleichzeitig haben Designer jedoch keinen Zugriff auf den geschlossenen Bereich, da sie Auftragnehmer und keine Mitarbeiter sind. Und wir haben die außer Kraft gesetzt und das Liskov-Substitutionsprinzip gebrochen. Contractor Designer Programmer Seller Worker access Dieses Prinzip besagt, dass die Funktionalität nicht beeinträchtigt werden sollte, wenn wir die Oberklasse durch ihre Unterklasse, beispielsweise Klasse, ersetzen. Wenn wir dies jedoch tun, wird die Funktionalität der beeinträchtigt – die erhält unerwartete Erkenntnisse aus der Klasse. Worker Designer Programmer access Designer 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 und erstellt, die in die Klasse „ verschoben und eine spezifische Realisierung definiert. Wenn wir Klasse durch die Unterklasse ersetzen, wird die Funktionalität nicht beeinträchtigt. Employee Contractor access Employee Worker Contractor Worker 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.