Zurück zum Blog

SOLID: Die ersten 5 Prinzipien des objektorientierten Designs?

SOLID: Die ersten 5 Prinzipien des objektorientierten Designs?

Einführung

SOLID ist ein mnemonisches Akronym für fünf Prinzipien des objektorientierten Designs, die von Robert C. Martin eingeführt wurden, der im Volksmund als Uncle Bob bezeichnet wird. Diese Prinzipien sollen Softwaredesignern, Architekten, Ingenieuren und Entwicklern helfen, flexiblere, wartbarere und skalierbarere Software zu erstellen. Durch das Befolgen dieser Prinzipien können Sie Klassen entwerfen, die einfacher zu testen, zu refaktorieren, wiederzuverwenden und zu erweitern sind.

Das SOLID-Akronym steht für:

S – Single Responsibility Principle

O – Open-Closed Principle

L – Liskov Substitution Principle

I – Interface Segregation Principle

D – Dependency Inversion Principle

In diesem Artikel werden wir jedes Prinzip einzeln erklären, um zu verstehen, wie es Ihnen helfen kann, besseren Code zu schreiben. Darüber hinaus werden wir Code-Snippets für jedes Prinzip hinzufügen, um Ihnen zu zeigen, wie Sie diese in Ihrer Programmierpraxis anwenden können und was Sie in einer Clean-Code-Architektur vermeiden sollten. Um die Konzepte zu demonstrieren, verwenden wir die Kotlin-Programmiersprache, die von JetBrains und Open-Source-Mitwirkenden entwickelt wurde.

Single-Responsibility-Prinzip

Das Single-Responsibility-Prinzip (SRP) ist ein Softwaredesign-Prinzip, das besagt, dass jede Klasse oder jedes Modul in einem Programm eine einzige, klar definierte Verantwortung haben sollte. Das bedeutet, dass eine Klasse nur einen einzigen Grund für Änderungen haben sollte und nur für einen einzelnen Teil der Funktionalität des Programms verantwortlich sein sollte.

Betrachten Sie das folgende Code-Snippet als Beispiel dafür, wie dieses Prinzip in Kotlin:

Das data-Schlüsselwort in Kotlin zeigt an, dass es sich bei dieser Klasse um eine Datenklasse handelt, was bedeutet, dass sie dazu gedacht ist, Daten zu halten, und kein komplexes Verhalten aufweist. Mit dieser Datenklasse können wir eine UserService -Klasse zur Verwaltung von Benutzern erstellen, wie unten gezeigt:

In diesem Beispiel hat die UserService-Klasse eine einzige Verantwortung: die Verwaltung von Benutzern in einer Datenbank. Jeder Benutzer wird durch den zuvor geteilten Code der data class User repräsentiert. Alle Methoden in der UserService-Klasse hängen mit dieser Verantwortung zusammen und sind daher kohäsiv. Dies macht die Klasse einfacher zu verstehen und zu warten, da klar ist, dass alle Methoden in der Klasse mit einer einzigen, klar definierten Aufgabe zusammenhängen.

Eine Methode, die das Single-Responsibility-Prinzip (SRP) in der UserService-Klasse verletzt, wäre eine, die nicht mit der Hauptverantwortung der Klasse (der Verwaltung von Benutzern in einer Datenbank) zusammenhängt. Betrachten Sie beispielsweise die folgende Variante der UserService-Klasse:

In diesem Beispiel bezieht sich die sendEmail -Methode nicht auf die Hauptverantwortung der UserService-Klasse, welche in der Verwaltung von Benutzern in einer Datenbank besteht. Diese Methode ist für das Senden von E-Mails verantwortlich, was ein separates Anliegen von der Verwaltung von Benutzern ist. Als Folge verletzt diese Methode das SRP, da sie einen zweiten Grund für eine Änderung der UserService-Klasse einführt.

Um das Single-Responsibility-Prinzip einzuhalten, wäre es besser, die E-Mail-Versandfunktionalität in eine separate Klasse auszulagern, wie beispielsweise eine EmailService -Klasse. Dies würde es der UserService-Klasse ermöglichen, sich auf ihre Hauptverantwortung der Benutzerverwaltung zu konzentrieren, und der EmailService-Klasse, sich auf ihre Verantwortung für den E-Mail-Versand zu konzentrieren.

Sie sollten beachten, dass es beim Single-Responsibility-Prinzip nicht um die Anzahl der Methoden geht, die eine Klasse hat, sondern vielmehr um den Zusammenhalt der Methoden und die klare Trennung der Verantwortlichkeiten innerhalb einer Klasse.

Open-Closed-Prinzip

Das Open-Closed-Prinzip (OCP) ist ein Software-Design-Prinzip, das besagt, dass Software-Entitäten (wie Klassen, Module oder Funktionen) offen für Erweiterungen, aber geschlossen für Modifikationen sein sollten. Dies bedeutet, dass es möglich sein sollte, einer Klasse oder einem Modul neue Funktionalität hinzuzufügen, ohne deren bestehenden Code zu ändern.

Betrachten Sie die UserService-Klasse aus dem vorherigen Beispiel. Angenommen, wir möchten der UserService-Klasse eine neue Funktion hinzufügen, mit der wir nach Benutzern anhand ihrer E-Mail-Adresse suchen können. Eine Möglichkeit, dies zu tun, bestünde darin, der UserService-Klasse eine neue Methode hinzuzufügen, wie im folgenden Code hervorgehoben:

Dieser Ansatz funktioniert zwar, verletzt jedoch das Open-Closed-Prinzip, da wir die bestehende UserService-Klasse ändern mussten, um die neue Funktion hinzuzufügen. Ein besserer Ansatz wäre die Verwendung von Vererbung oder Komposition, um die Funktionalität der UserService-Klasse zu erweitern, ohne deren Code zu ändern.

Um dies zu erreichen, könnten wir eine neue Klasse namens UserSearchService  erstellen, welche die UserService-Klasse erweitert und die E-Mail-Suchfunktionalität hinzufügt:

In diesem Beispiel ist die UserSearchService-Klasse offen für Erweiterungen, da sie zusätzliche Funktionalität über das hinaus bietet, was von der UserService-Klasse. Gleichzeitig bleibt die UserService-Klasse für Änderungen geschlossen, da wir ihren Code nicht modifizieren mussten, um die E-Mail-Suchfunktion hinzuzufügen.

Liskovsches Substitutionsprinzip

Das Liskovsche Substitutionsprinzip (LSP) ist ein Software-Design-Prinzip, das besagt, dass Objekte einer Superklasse durch Objekte einer Subklasse ersetzt werden können sollten, ohne die Korrektheit des Programms zu beeinträchtigen. Dies bedeutet, dass eine Subklasse ein gültiger Ersatz für ihre Superklasse sein sollte und sich im selben Kontext genauso verhalten sollte wie die Superklasse.

Wir werden die Demonstration mit den Klassen User und UserService aus den vorherigen Beispielen fortsetzen. Um eine Erweiterung der User-Klasse zu ermöglichen, verwenden wir das Schlüsselwort open in Kotlin:

Hier ist die ursprüngliche UserService-Klasse, die die obige User-Datenklasse verwendet:

Angenommen, wir möchten eine Subklasse von User namens AdminUser erstellen, die Benutzer mit administrativen Rechten darstellt. Das könnten wir wie folgt tun:

In diesem Beispiel ist die AdminUser-Klasse ein gültiger Ersatz für die User-Klasse, da sie sich genauso verhält wie die User-Klasse und überall dort verwendet werden kann, wo ein User-Objekt erwartet wird. Beispielsweise können wir die AdminUser-Klasse mit der UserService-Klasse wie folgt verwenden:

Dieser Code ist korrekt, da die AdminUser-Klasse ein gültiger Ersatz für die User-Klasse ist und auf dieselbe Weise wie ein User-Objekt verwendet werden kann.

Es’s wichtig zu beachten, dass es beim Liskovschen Substitutionsprinzip um mehr als nur Vererbung geht. Es geht darum sicherzustellen, dass sich Objekte einer Subklasse genauso verhalten wie Objekte der Superklasse, unabhängig davon, wie die Subklasse implementiert ist. Wenn sich beispielsweise die AdminUser-Klasse in irgendeiner Weise anders verhalten würde als die User-Klasse, würde dies gegen das Liskovsche Substitutionsprinzip verstoßen, da sie kein gültiger Ersatz für die User-Klasse wäre.

Interface-Segregations-Prinzip

Das Interface-Segregations-Prinzip (ISP) ist ein Software-Design-Prinzip, das besagt, dass Clients nicht dazu gezwungen werden sollten, von Schnittstellenabhängig zu sein, die sie nicht verwenden. Dies bedeutet, dass es im Allgemeinen eine gute Idee ist, kleine, fokussierte Schnittstellen zu erstellen, die eine Sache gut machen, anstatt große, universelle Schnittstellen zu erstellen, die versuchen, viele Dinge zu tun.

Hier’s ein Beispiel dafür, wie dieses Prinzip angewendet werden kann, indem die User- und UserService-Klassen aus den vorherigen Beispielen umgeschrieben werden:

In diesem Beispiel definiert das UserService-Interface vier Methoden, die mit der Verwaltung von Benutzern in einer Datenbank zusammenhängen. Die Klasse DatabaseUserService  implementiert dieses Interface und stellt konkrete Implementierungen für diese Methoden bereit.

Angenommen, wir möchten dem UserService-Interface eine neue Funktion hinzufügen, mit der wir nach Benutzern anhand ihrer E-Mail-Adresse suchen können. Eine Möglichkeit hierfür wäre, dem UserService-Interface eine neue Methode hinzuzufügen:

Ihr Code wird nicht ausgeführt, es sei denn, Sie implementieren diese Methode auch in allen Klassen, die das UserService-Interface implementieren:

Dieser Ansatz funktioniert zwar, verletzt jedoch das Interface-Segregations-Prinzip, da er die Klasse DatabaseUserService  dazu zwingt, eine Methode ( searchUsersByEmail ) zu implementieren, die sie möglicherweise nicht benötigt oder verwendet.

Ein besserer Ansatz wäre es, ein separates Interface  für die E-Mail-Suchfunktionalität zu erstellen:

Jetzt haben wir separate, kleine und fokussierte Interfaces, d. h. UserService  und UserSearchServicedie eine einzige Verantwortung haben. Eine Klasse, die alle Funktionalitäten dieser Schnittstellen benötigt, kann diese wie im folgenden Code-Snippet gezeigt implementieren:

Dies entspricht dem Interface-Segregations-Prinzip, da es sicherstellt, dass Clients (wie die DatabaseUserService Klasse) nicht gezwungen sind, von Schnittstellen abzuhängen, die sie nicht verwenden.

Um dieses Konzept besser zu verstehen, nehmen wir an, wir haben eine weitere Klasse namens MemoryUserService , die die Schnittstelle UserService  implementiert, aber die E-Mail-Suchfunktionalität nicht benötigt. Wir können den Code wie folgt schreiben:

In diesem Beispiel muss die Klasse MemoryUserService nur die in der Schnittstelle UserService definierten Methoden implementieren und muss sich nicht um die E-Mail-Suchfunktionalität kümmern. Dies ermöglicht es der Klasse MemoryUserService, sich auf ihre Hauptverantwortung zu konzentrieren, nämlich die Verwaltung von Benutzern im Speicher, anstatt gezwungen zu sein, nicht damit zusammenhängende Funktionalitäten zu implementieren.

Dependency-Inversion-Prinzip

Das Dependency-Inversion-Prinzip (DIP) is ein Software-Entwurfsprinzip, das besagt, dass Module auf höherer Ebene nicht von Modulen auf niedrigerer Ebene abhängen sollten, sondern beide von Abstraktionen abhängen sollten. Das bedeutet, dass es im Allgemeinen eine gute Idee ist, Software so zu entwerfen, dass Komponenten auf höherer Ebene nicht an bestimmte Implementierungen von Komponenten auf niedrigerer Ebene gebunden sind, sondern von Abstraktionen (wie Schnittstellen oder abstrakten Klassen) abhängen, die auf unterschiedliche Weise implementiert werden können.

Sehen wir uns ein Beispiel an, wie dieses Prinzip auf die Klassen User und UserService angewendet werden kann, die in den vorherigen Code-Snippets verwendet wurden:

In diesem Beispiel hängt die UserService-Klasse von einer Abstraktion namens der UserRepository-Schnittstelle ab, anstatt von einer spezifischen Implementierung eines Benutzer-Repositorys abzuhängen. Dies ermöglicht es uns, die UserRepository-Schnittstelle auf verschiedene Weise zu implementieren, beispielsweise mit einer Datenbank oder einem In-Memory-Speicher, ohne die UserService-Klasse zu beeinflussen.

Hier ist beispielsweise eine Implementierung der UserRepository-Schnittstelle, die eine Datenbank verwendet:

Hier ist eine weitere Implementierung der UserRepository-Schnittstelle, die einen In-Memory-Speicher verwendet:

Dies macht das System flexibler und wartbarer, da es uns ermöglicht, die Repository-Implementierung zu ändern, ohne den Rest des Systems zu beeinflussen. Es erleichtert auch das Testen der UserService-Klasse, da wir die UserRepository-Abhängigkeit in unseren Tests mocken können.

Fazit

In diesem Artikel besprachen wir die fünf Prinzipien von SOLID  Code und freigegebene Code-Snippets, die jedes Prinzip erfüllen. Die Einhaltung dieser Prinzipien kann Ihnen helfen, Softwaresysteme zu entwerfen, die flexibler, wartbarer und skalierbarer sind. Es ist jedoch wichtig zu bedenken, dass diese Prinzipien Richtlinien und keine festen Regeln sind, und es liegt am Entwickler zu entscheiden, wann und wie er sie im Kontext seines spezifischen Projekts anwendet. Lernen Sie weiter, indem Sie unseren Blog besuchen, um ausführlichere und aktuellere Artikel und Tutorials über Cloud-Computing und DevOps, Softwaredesign und -entwicklung, Technologietrends, auf die Sie achten sollten, und mehr.

Viel Spaß beim Codieren!

author

Preslav Dobrev

Autor · CloudSigma

Preslav Dobrev ist ein kreativer Designer bei CloudSigma und konzentriert sich auf eine konsistente Unternehmensidentität durch traditionelle und innovative Marketingkanäle. Er versteht es meisterhaft, künstlerische Vision mit strategischem Marketing zu verbinden, um wirkungsvolle Markengeschichten zu schaffen.

Kommentare

Noch keine Kommentare. Schreiben Sie den ersten.