paint-brush
SOLID-Prinzipien in Java: Ein Leitfaden für Anfängervon@ps06756
14,454 Lesungen
14,454 Lesungen

SOLID-Prinzipien in Java: Ein Leitfaden für Anfänger

von Pratik Singhal14m2024/03/22
Read on Terminal Reader

Zu lang; Lesen

SOLID-Prinzipien sind die Prinzipien der objektorientierten Programmierung, die für die Entwicklung skalierbarer Software unerlässlich sind. Die Prinzipien sind: S: Single Responsibility Principle O: Open / Closed Principle L: Liskovsches Substitutionsprinzip I: Interface Segregation Principle D: Dependency Inversion Principle
featured image - SOLID-Prinzipien in Java: Ein Leitfaden für Anfänger
Pratik Singhal HackerNoon profile picture
0-item

SOLID-Designprinzipien sind die wichtigsten Designprinzipien, die Sie kennen müssen, um sauberen Code zu schreiben. Eine solide Beherrschung der SOLID-Prinzipien ist eine unverzichtbare Fähigkeit für jeden Programmierer. Sie bilden die Grundlage, auf der andere Designmuster entwickelt werden.


In diesem Artikel werden wir uns anhand einiger Beispiele aus der Praxis mit den SOLID-Designprinzipien befassen und ihre Bedeutung verstehen.


Zusammen mit Polymorphismus, Abstraktion und Vererbung sind die SOLID-Prinzipien wirklich wichtig, um gut in der objektiven Programmierung zu sein.

Warum sind SOLIDE Prinzipien wichtig?

SOLID-Prinzipien sind aus mehreren Gründen wichtig:


  • Die SOLID-Prinzipien ermöglichen es uns, sauberen und wartbaren Code zu schreiben: Wenn wir ein neues Projekt starten, ist die Codequalität zunächst gut, da wir nur über eine begrenzte Anzahl von Funktionen verfügen, die wir implementieren. Je mehr Funktionen wir integrieren, desto unübersichtlicher wird der Code.


  • SOLID-Prinzipien bauen auf den Grundlagen von Abstraktion, Polymorphismus und Vererbung auf und führen zu Entwurfsmustern für häufige Anwendungsfälle. Das Verständnis dieser Entwurfsmuster hilft bei der Implementierung gängiger Anwendungsfälle in der Programmierung.


  • Die SOLID-Prinzipien helfen uns, sauberen Code zu schreiben, der die Testbarkeit des Codes verbessert. Dies liegt daran, dass der Code modular und lose gekoppelt ist. Jedes Modul kann unabhängig entwickelt und unabhängig getestet werden.


Lassen Sie uns nun jedes der SOLID-Prinzipien im Detail anhand von Beispielen aus der Praxis untersuchen.

1. Prinzip der Einzelverantwortung

S in SOLID-Prinzipien steht für Single-Responsibility-Prinzip. Das Single-Responsibility-Prinzip besagt, dass eine Klasse nur einen einzigen Grund für eine Änderung haben sollte. Dies begrenzt die Anzahl der Stellen, an denen wir Änderungen vornehmen müssen, wenn wir zusätzliche Anforderungen in unser Projekt integrieren.

Jede Klasse sollte genau einen Grund zur Änderung haben.

Nehmen wir zum Beispiel an, wir entwerfen eine Bankanwendung in Java, in der wir über eine SavingsAccount Klasse verfügen, die grundlegende Vorgänge wie Debit, Credit und sendUpdates ermöglicht. Die sendUpdate Methode nimmt eine Enumeration namens NotificationMedium (wie E-Mail, SMS usw.) und sendet das Update mit dem entsprechenden Medium. Wir werden den Code dafür wie unten gezeigt schreiben.


 public class SavingsAccount { public int balance; public String name; public SavingsAccount(int initialBalance, String name) { this.balance = initialBalance; this.name = name; System.out.println("Created a savings account with balance = " + initialBalance); } public void debit(int amountToDebit) { // debit business logic } public void credit(int amountToCredit) { // credit business logic } public void sendNotification(NotificationMedium medium) { if (medium == NotificationMedium.SMS) { // Send SMS here } else if (medium == NotificationMedium.EMAIL) { // Send Email here } } }


 public enum NotificationMedium { SMS, EMAIL }


Wenn Sie sich nun die SavingsAccount Klasse oben ansehen, kann sie sich aus mehreren Gründen ändern:


  1. Wenn sich die Kernlogik der SavingsAccount Klasse ändert (wie debit , credit usw.).


  2. Wenn die Bank beschließt, ein neues Benachrichtigungsmedium einzuführen (sagen wir WhatsApp).


Dies stellt einen Verstoß gegen das Single-Responsibility-Prinzip der SOLID-Prinzipien dar. Um das Problem zu beheben, erstellen wir eine separate Klasse, die die Benachrichtigung sendet.


Lassen Sie uns den obigen Code gemäß den SOLID-Prinzipien umgestalten


 public class SavingsAccount { public int balance; public String name; public SavingsAccount(int initialBalance, String name) { this.balance = initialBalance; this.name = name; System.out.println("Created a savings account with balance = " + initialBalance); } public void debit(int amountToDebit) { // debit business logic } public void credit(int amountToCredit) { // credit business logic } public void printBalance() { System.out.println("Name: " + name+ " Account Balance: " + balance); } public void sendNotification(Medium medium) { Sender.sendNotification(medium, this); } }


 public enum NotificationMedium { SMS, EMAIL }


 public class Sender { public static void sendNotification(NotificationMedium medium, SavingsAccount account) { // extract account data from the account object if (medium == NotificationMedium.SMS) { //logic to send SMS here } else if (medium == NotificationMedium.EMAIL) { // logic to send Email here } } }


Da wir nun den Code umgestaltet haben, ändern wir bei jeder Änderung des NotificationMedium oder des Formats die Sender Klasse. Wenn sich jedoch die Kernlogik von SavingsAccount ändert, wird es auch Änderungen in der SavingsAccount Klasse geben.


Dadurch wird der Verstoß behoben, den wir im ersten Beispiel beobachtet haben.

2. Öffnen/Schließen-Prinzip

Das Open-Close-Prinzip besagt, dass wir die Klassen so gestalten sollten, dass sie für Erweiterungen offen sind (im Falle des Hinzufügens zusätzlicher Funktionen), aber für Änderungen geschlossen sind. Die Schließung wegen Änderung hilft uns in zweierlei Hinsicht:


  • Oftmals ist die ursprüngliche Klassenquelle möglicherweise nicht einmal verfügbar. Es könnte sich um eine von Ihrem Projekt verbrauchte Abhängigkeit handeln.


  • Wenn Sie die ursprüngliche Klasse unverändert lassen, verringert sich die Wahrscheinlichkeit von Fehlern. Da es möglicherweise andere Klassen gibt, die von der Klasse abhängig sind, die wir ändern möchten.


Um ein Beispiel für das Open-Close-Prinzip zu sehen, werfen wir einen Blick auf das Beispiel eines Warenkorbs (wie er beispielsweise auf E-Commerce-Websites implementiert ist).


Ich werde eine Klasse namens „ Cart erstellen, die eine Liste von Item enthält, die Sie hinzufügen können. Abhängig von der Art des Artikels und der darauf erhobenen Besteuerung möchten wir eine Methode erstellen, die den gesamten Warenkorbwert innerhalb der Cart Klasse berechnet.


Der Unterricht sollte für Erweiterungen geöffnet und für Änderungen geschlossen sein


 import java.util.ArrayList; import java.util.List; public class Cart { private List<Item> items; public Cart() { this.items = new ArrayList<>(); } public void addToCart(Item item) { items.add(item); } public double calculateCartValue() { double value = 0.0; for(Item item: items) { if (item.getItemType() == GIFT) { // 8% tax on gift + 2% gift wrap cost value += (item.getValue()*1.08) + item.getValue()*0.02 } else if (item.getItemType() == ItemType.ELECTRONIC_ITEM) { value += (item.getValue()*1.11); } else { value += item.getValue()*1.10; } } return value; } }


 @Getter @Setter public abstract class Item { protected double price; private ItemType itemType; public double getValue() { return price; } }
 public enum ItemType { ELECTRONIC, GIFT }


Im obigen Beispiel berechnet die Methode calculateCartValue den Warenkorbwert, indem sie alle Artikel im Warenkorb durchläuft und eine Logik basierend auf dem Artikeltyp aufruft.


Obwohl dieser Code korrekt aussieht, verstößt er gegen die SOLID-Prinzipien.


Nehmen wir an, wir müssen bei der Berechnung des Warenkorbwerts eine neue Regel für einen anderen Artikeltyp (z. B. Lebensmittel) hinzufügen. In diesem Fall müssten wir die ursprüngliche Klasse Cart ändern und eine weitere else if Bedingung darin schreiben, die nach Artikeln vom Typ Grocery sucht.


Mit wenig Refactoring können wir jedoch dafür sorgen, dass der Code dem Öffnen/Schließen-Prinzip entspricht. Mal sehen, wie.


Zuerst werden wir die Item Klasse abstrahieren und konkrete Klassen für verschiedene Arten von Item erstellen, wie unten gezeigt.


 public abstract class Item { protected double price; public double getValue() { return price; } }


 public class ElectronicItem extends Item { public ElectronicItem(double price) { super.price = price; } @Override public double getValue() { return super.getValue()*1.11; } }


 public class GiftItem extends Item { public GiftItem(double price) { super.price = price; } @Override public double getValue() { return super.getValue()*1.08 + super.getValue()*0.02; } }


 public class GroceryItem extends Item { public GroceryItem(double price) { super.price = price; } @Override public double getValue() { return super.getValue()*1.03; } }


In jedem konkreten Item implementieren Klassen wie GroceryItem , GiftItem und ElectronicItem die Methode getValue() , die die Geschäftslogik für die Besteuerung und Wertberechnung enthält.


Jetzt machen wir die Cart Klasse von der abstrakten Klasse Item abhängig und rufen für jedes Element die Methode getValue() auf, wie unten gezeigt.


 import java.util.ArrayList; import java.util.List; public class Cart { private List<Item> items; public Cart(Payment paymentOption) { this.items = new ArrayList<>(); } public void addToCart(Item item) { items.add(item); } public double calculateCartValue() { double value = 0.0; for(Item item: items) { value += item.getValue(); } return value; } }


In diesem überarbeiteten Code bleibt die Cart Klasse unverändert, auch wenn neue Item eingeführt werden. Aufgrund des Polymorphismus wird unabhängig vom tatsächlichen Typ des Item in der ArrayList items die Methode getValue() dieser Klasse aufgerufen.

3. Liskovs Substitutionsprinzip

Das Substitutionsprinzip von Liskov besagt, dass in einem bestimmten Code der Code nicht beschädigt werden sollte, selbst wenn wir das Objekt einer Oberklasse durch ein Objekt der untergeordneten Klasse ersetzen. Mit anderen Worten: Wenn eine Unterklasse eine Oberklasse erbt und deren Methoden überschreibt, sollte sie mit dem Verhalten der Methode in der Oberklasse konsistent bleiben.


Wenn wir zum Beispiel die folgenden Klassen Vehicle und zwei Klassen Car “ und Bicycle erstellen. Nehmen wir nun an, wir erstellen eine Methode namens startEngine() innerhalb der Vehicle-Klasse. Sie kann in der Car Klasse überschrieben werden, wird jedoch in der Bicycle Klasse nicht unterstützt, da Bicycle keine Engine hat (siehe Codebeispiel unten).


Die Unterklasse sollte beim Überschreiben von Methoden konsistent mit dem Verhalten der Oberklasse bleiben.

 class Vehicle { public void startEngine() { // start engine of the vehicle } } class Car extends Vehicle { @Override public void startEngine() { // Start Engine } } class Bicycle extends Vehicle { @Override public void startEngine(){ throw new UnsupportedOperationException("Bicycle doesn't have engine"); } }


Nehmen wir nun an, es gibt Code, der ein Objekt vom Typ Fahrzeug erwartet und auf die Methode startEngine() angewiesen ist. Wenn wir beim Aufruf dieses Codeabschnitts statt eines Objekts vom Typ „ Vehicle ein Bicycle Objekt übergeben, würde dies zu Problemen im Code führen. Da die Methode der Klasse Bicycle (s) eine Ausnahme auslöst, wenn die Methode startEngine() aufgerufen wird. Dies wäre ein Verstoß gegen die SOLID-Prinzipien (Liskovs Substitutionsprinzip)


Um dieses Problem zu lösen, können wir zwei Klassen MotorizedVehicle “ und NonMotorizedVehicle erstellen und Car von der Klasse „ MotorizedVehicle “ und Bicycle von NonMotorizedVehicle erben lassen


 class Vehicle { } class MotorizedVehicle extends Vehicle { public void startEngine() { // start engine here } } class Car extends MotorizedVehicle { @Override public void startEngine() { // Start Engine } } class NonMotorizedVehicle extends Vehicle { public void startRiding() { // Start without engine } } class Bicycle extends NonMotorizedVehicle { @Override public void startRiding(){ // Start riding without the engine. } }


4. Prinzip der Schnittstellentrennung

Das „I“ in SOLID Principles steht für das Interface Segregation Principle.


Das Prinzip der Schnittstellentrennung besagt, dass wir statt größerer Schnittstellen, die implementierende Klassen dazu zwingen, ungenutzte Methoden zu implementieren, kleinere Schnittstellen haben und Klassen implementieren sollten. Auf diese Weise implementieren Klassen nur die relevanten Methoden und bleiben sauber.

Teilen Sie Ihre Schnittstellen in mehrere kleinere Schnittstellen auf, anstatt in eine große Schnittstelle.

Schauen wir uns zum Beispiel das integrierte Collections-Framework in Java an. Neben anderen Datenstrukturen bietet Java auch die Datenstrukturen LinkedList und ArrayList .


ArrayList Klasse implementiert die folgenden Schnittstellen: Serializable , Cloneable , Iterable , Collection , List und RandomAccess .

LinkedList Klasse implementiert Serializable , Cloneable , Iterable , Collection , Deque , List und Queue .


Das sind ziemlich viele Schnittstellen!


Anstatt so viele Schnittstellen zu haben, hätten Java-Entwickler Serializable , Cloneable , Iterable , Collecton , List und RandomAccess in einer Schnittstelle kombinieren können, sagen wir IList -Schnittstelle. Nun hätten sowohl ArrayList als auch LinkedList Klasse diese neue IList Schnittstelle implementieren können.


Da LinkedList jedoch keinen Direktzugriff unterstützt, hätte es die Methoden in der RandomAccess Schnittstelle implementieren und UnsupportedOperationException auslösen können, wenn jemand versucht, es aufzurufen.


Dies wäre jedoch ein Verstoß gegen das Prinzip der Schnittstellentrennung in den SOLID-Prinzipien, da es die LinkedList-Klasse „zwingen“ würde, die Methoden innerhalb der RandomAccess Schnittstelle zu implementieren, auch wenn dies nicht erforderlich ist.


Daher ist es besser, die Schnittstelle basierend auf dem gemeinsamen Verhalten aufzuteilen und jede Klasse viele Schnittstellen implementieren zu lassen, anstatt eine große Schnittstelle.

Abhängigkeitsinversionsprinzip

Das Abhängigkeitsinversionsprinzip besagt, dass Klassen auf der oberen Ebene nicht direkt von den Klassen auf der unteren Ebene abhängen sollten. Dies führt zu einer engen Kopplung zwischen den beiden Ebenen.


Stattdessen sollten niedrigere Klassen eine Schnittstelle bereitstellen, von der die oberen Klassen abhängig sein sollten.


Verlassen Sie sich eher auf Schnittstellen als auf Klassen


Fahren wir beispielsweise mit dem Cart Beispiel fort, das wir oben gesehen haben, und erweitern es, um einige Zahlungsoptionen hinzuzufügen. Nehmen wir an, wir haben bei uns zwei Zahlungsmöglichkeiten DebitCard und Paypal . Nun möchten wir in der Klasse Cart eine Methode zu placeOrder hinzufügen, die den Warenkorbwert berechnet und die Zahlung basierend auf der bereitgestellten Zahlung initiiert. Methode.


Zu diesem Zweck hätten wir im obigen Cart Beispiel eine Abhängigkeit hinzufügen können, indem wir die beiden Zahlungsoptionen als Felder innerhalb der Cart Klasse hinzugefügt hätten. Dies würde jedoch die Cart Klasse eng mit der DebitCard und Paypal Klasse koppeln.


Stattdessen würden wir eine Payment erstellen und sowohl die DebitCard als auch die Paypal Klasse die Payment implementieren lassen. Nun hängt die Cart Klasse von der Payment ab und nicht von den einzelnen Zahlungsarten. Dadurch bleiben die Klassen lose gekoppelt.


Siehe den Code unten.


 public interface Payment { void doPayment(double amount); } public class PaypalPayment implements Payment { @Override public void doPayment(double amount) { // logic to initiate paypal payment } } public class DebitCardPayment implements Payment { @Override public void doPayment(double amount) { // logic to initiate payment via debit card } } import java.util.ArrayList; import java.util.List; public class Cart { private List<Item> items; private Payment paymentOption; public Cart(Payment paymentOption) { this.items = new ArrayList<>(); this.paymentOption = paymentOption; } public void addToCart(Item item) { items.add(item); } public double calculateCartValue() { double value = 0.0; for(Item item: items) { value += item.getValue(); } return value; } public void placeOrder() { this.paymentOption.doPayment(calculateCartValue()); } }


Wenn Sie daran interessiert sind, mehr über objektorientierte Programmierung, GoF-Entwurfsmuster und Low-Level-Design-Interviews zu erfahren, dann schauen Sie sich meinen hoch bewerteten Kurs „Objektorientierte Programmierung + Java-Entwurfsmuster“ an