CodeGym /Java-Blog /Random-DE /SOLID: Fünf Grundprinzipien des Klassendesigns in Java
Autor
Jesse Haniel
Lead Software Architect at Tribunal de Justiça da Paraíba

SOLID: Fünf Grundprinzipien des Klassendesigns in Java

Veröffentlicht in der Gruppe Random-DE
Klassen sind die Bausteine ​​von Anwendungen. Genau wie Ziegelsteine ​​in einem Gebäude. Schlecht geschriebene Kurse können irgendwann zu Problemen führen. SOLID: Fünf Grundprinzipien des Klassendesigns in Java - 1Um zu verstehen, ob eine Klasse richtig geschrieben ist, können Sie überprüfen, wie sie den „Qualitätsstandards“ entspricht. In Java sind dies die sogenannten SOLID-Prinzipien, über die wir sprechen werden.

SOLID-Prinzipien in Java

SOLID ist ein Akronym, das aus den Großbuchstaben der ersten fünf Prinzipien von OOP und Klassendesign gebildet wird. Die Prinzipien wurden Anfang der 2000er Jahre von Robert Martin formuliert und die Abkürzung wurde später von Michael Feathers eingeführt. Hier sind die SOLID-Prinzipien:
  1. Prinzip der Einzelverantwortung
  2. Offen-Geschlossen-Prinzip
  3. Liskov-Substitutionsprinzip
  4. Prinzip der Schnittstellentrennung
  5. Abhängigkeitsinversionsprinzip

Prinzip der Einzelverantwortung (SRP)

Dieses Prinzip besagt, dass es nie mehr als einen Grund für einen Klassenwechsel geben sollte. Jedes Objekt hat eine Verantwortung, die vollständig in der Klasse gekapselt ist. Alle Angebote einer Klasse zielen darauf ab, diese Verantwortung zu unterstützen. Solche Klassen lassen sich bei Bedarf immer leicht ändern, da klar ist, wofür die Klasse verantwortlich ist und was nicht. Mit anderen Worten: Wir können Änderungen vornehmen und haben keine Angst vor den Konsequenzen, also vor den Auswirkungen auf andere Objekte. Darüber hinaus lässt sich solcher Code viel einfacher testen, da Ihre Tests eine einzelne Funktionalität isoliert von allen anderen abdecken. Stellen Sie sich ein Modul vor, das Bestellungen verarbeitet. Wenn eine Bestellung korrekt erstellt wurde, speichert dieses Modul sie in einer Datenbank und sendet eine E-Mail zur Bestätigung der Bestellung:

public class OrderProcessor {

    public void process(Order order){
        if (order.isValid() && save(order)) {
            sendConfirmationEmail(order);
        }
    }

    private boolean save(Order order) {
        MySqlConnection connection = new MySqlConnection("database.url");
        // Save the order in the database

        return true;
    }

    private void sendConfirmationEmail(Order order) {
        String name = order.getCustomerName();
        String email = order.getCustomerEmail();

        // Send an email to the customer
    }
}
Dieses Modul könnte sich aus drei Gründen ändern. Erstens kann sich die Logik zur Auftragsabwicklung ändern. Zweitens kann sich die Art und Weise, wie Bestellungen gespeichert werden (Datenbanktyp), ändern. Drittens kann sich die Art und Weise, wie die Bestätigung gesendet wird, ändern (angenommen, wir müssen beispielsweise eine Textnachricht anstelle einer E-Mail senden). Das Prinzip der Einzelverantwortung impliziert, dass es sich bei den drei Aspekten dieses Problems tatsächlich um drei verschiedene Verantwortlichkeiten handelt. Das bedeutet, dass sie in verschiedenen Klassen oder Modulen sein sollten. Die Kombination mehrerer Elemente, die sich zu unterschiedlichen Zeiten und aus unterschiedlichen Gründen ändern können, wird als schlechte Entwurfsentscheidung angesehen. Es ist viel besser, ein Modul in drei separate Module aufzuteilen, die jeweils eine einzelne Funktion erfüllen:

public class MySQLOrderRepository {
    public boolean save(Order order) {
        MySqlConnection connection = new MySqlConnection("database.url");
        // Save the order in the database

        return true;
    }
}

public class ConfirmationEmailSender {
    public void sendConfirmationEmail(Order order) {
        String name = order.getCustomerName();
        String email = order.getCustomerEmail();

        // Send an email to the customer
    }
}

public class OrderProcessor {
    public void process(Order order){

        MySQLOrderRepository repository = new MySQLOrderRepository();
        ConfirmationEmailSender mailSender = new ConfirmationEmailSender();

        if (order.isValid() && repository.save(order)) {
            mailSender.sendConfirmationEmail(order);
        }
    }

}

Open-Closed-Prinzip (OCP)

Dieses Prinzip wird wie folgt beschrieben: Software-Entitäten (Klassen, Module, Funktionen usw.) sollten für Erweiterungen offen, für Änderungen jedoch geschlossen sein . Das bedeutet, dass es möglich sein sollte, das externe Verhalten einer Klasse zu ändern, ohne Änderungen am vorhandenen Code der Klasse vorzunehmen. Nach diesem Prinzip sind Klassen so konzipiert, dass die Anpassung einer Klasse an bestimmte Bedingungen lediglich eine Erweiterung und das Überschreiben einiger Funktionen erfordert. Das bedeutet, dass das System flexibel sein muss und in der Lage sein muss, unter sich ändernden Bedingungen zu arbeiten, ohne den Quellcode zu ändern. Um unser Beispiel mit der Auftragsabwicklung fortzusetzen, nehmen wir an, dass wir einige Aktionen durchführen müssen, bevor eine Bestellung bearbeitet wird und nachdem die Bestätigungs-E-Mail gesendet wurde. Anstatt das zu ändernOrderProcessorIn der Klasse selbst werden wir sie erweitern, um unser Ziel zu erreichen, ohne das Open-Closed-Prinzip zu verletzen:

public class OrderProcessorWithPreAndPostProcessing extends OrderProcessor {

    @Override
    public void process(Order order) {
        beforeProcessing();
        super.process(order);
        afterProcessing();
    }
    
    private void beforeProcessing() {
        // Take some action before processing the order
    }
    
    private void afterProcessing() {
        // Take some action after processing the order
    }
}

Liskov-Substitutionsprinzip (LSP)

Dies ist eine Variante des bereits erwähnten Offen-Geschlossen-Prinzips. Es kann wie folgt definiert werden: Objekte können durch Objekte von Unterklassen ersetzt werden, ohne die Eigenschaften eines Programms zu ändern. Das bedeutet, dass eine durch Erweiterung einer Basisklasse erstellte Klasse deren Methoden überschreiben muss, damit die Funktionalität aus Sicht des Clients nicht beeinträchtigt wird. Das heißt, wenn ein Entwickler Ihre Klasse erweitert und in einer Anwendung verwendet, sollte er oder sie das erwartete Verhalten aller überschriebenen Methoden nicht ändern. Unterklassen müssen die Methoden der Basisklasse überschreiben, damit die Funktionalität aus Sicht des Clients nicht beeinträchtigt wird. Wir können dies im folgenden Beispiel im Detail untersuchen. Angenommen, wir haben eine Klasse, die dafür verantwortlich ist, eine Bestellung zu validieren und zu prüfen, ob alle Waren in der Bestellung auf Lager sind.isValid()Methode, die true oder false zurückgibt :

public class OrderStockValidator {

    public boolean isValid(Order order) {
        for (Item item : order.getItems()) {
            if (!item.isInStock()) {
                return false;
            }
        }

        return true;
    }
}
Nehmen wir außerdem an, dass einige Bestellungen anders validiert werden müssen als andere, z. B. müssen wir bei einigen Bestellungen prüfen, ob alle Waren in der Bestellung auf Lager sind und ob alle Waren verpackt sind. Dazu erweitern wir die Klasse, indem wir die Klasse OrderStockValidatorerstellen :OrderStockAndPackValidator

public class OrderStockAndPackValidator extends OrderStockValidator {

    @Override
    public boolean isValid(Order order) {
        for (Item item : order.getItems()) {
            if ( !item.isInStock() || !item.isPacked() ){
                throw new IllegalStateException(
                     String.format("Order %d is not valid!", order.getId())
                );
            }
        }

        return true;
    }
}
Aber hier haben wir gegen das Liskov-Substitutionsprinzip verstoßen, denn anstatt false zurückzugeben , wenn die Validierung der Bestellung fehlschlägt, wirft unsere Methode ein IllegalStateException. Clients, die diesen Code verwenden, erwarten dies nicht: Sie erwarten einen Rückgabewert von entweder true oder false . Dies kann zu Laufzeitfehlern führen.

Prinzip der Schnittstellentrennung (ISP)

Dieses Prinzip wird durch die folgende Aussage charakterisiert: Der Client sollte nicht gezwungen werden, Methoden zu implementieren, die er nicht verwenden wird . Das Prinzip der Schnittstellentrennung bedeutet, dass zu „dicke“ Schnittstellen in kleinere, spezifischere Schnittstellen aufgeteilt werden müssen, sodass Clients, die kleine Schnittstellen verwenden, nur über die Methoden Bescheid wissen, die sie für ihre Arbeit benötigen. Wenn sich eine Schnittstellenmethode ändert, sollten sich daher alle Clients, die diese Methode nicht verwenden, nicht ändern. Betrachten Sie dieses Beispiel: Alex, ein Entwickler, hat eine „Bericht“-Schnittstelle erstellt und zwei Methoden hinzugefügt: generateExcel()undgeneratedPdf(). Nun möchte ein Kunde diese Schnittstelle nutzen, beabsichtigt jedoch nur, Berichte im PDF-Format zu verwenden, nicht in Excel. Wird diese Funktionalität diesen Kunden zufriedenstellen? Nein. Der Kunde muss zwei Methoden implementieren, von denen eine größtenteils nicht benötigt wird und nur dank Alex, dem Entwickler der Software, existiert. Der Kunde wird entweder eine andere Schnittstelle verwenden oder nichts mit der Methode für Excel-Berichte tun. Was ist also die Lösung? Dabei soll die bestehende Schnittstelle in zwei kleinere aufgeteilt werden. Einer für PDF-Berichte, der andere für Excel-Berichte. Dadurch können Kunden nur die Funktionalität nutzen, die sie benötigen.

Abhängigkeitsinversionsprinzip (DIP)

In Java wird dieses SOLID-Prinzip wie folgt beschrieben: Abhängigkeiten innerhalb des Systems werden auf Basis von Abstraktionen aufgebaut. Module höherer Ebene sind nicht von Modulen niedrigerer Ebene abhängig. Abstraktionen sollten nicht von Details abhängen. Details sollten von Abstraktionen abhängen. Software muss so konzipiert sein, dass die verschiedenen Module in sich abgeschlossen und durch Abstraktion miteinander verbunden sind. Eine klassische Anwendung dieses Prinzips ist das Spring Framework. Im Spring Framework sind alle Module als separate Komponenten implementiert, die zusammenarbeiten können. Sie sind so autonom, dass sie ebenso problemlos in anderen Programmmodulen als dem Spring Framework verwendet werden können. Dies wird durch die Abhängigkeit der geschlossenen und offenen Prinzipien erreicht. Alle Module bieten nur Zugriff auf die Abstraktion, die in einem anderen Modul verwendet werden kann. Versuchen wir dies anhand eines Beispiels zu veranschaulichen. Apropos Einzelverantwortungsprinzip: Wir haben darüber nachgedachtOrderProcessorKlasse. Schauen wir uns den Code dieser Klasse noch einmal an:

public class OrderProcessor {
    public void process(Order order){

        MySQLOrderRepository repository = new MySQLOrderRepository();
        ConfirmationEmailSender mailSender = new ConfirmationEmailSender();

        if (order.isValid() && repository.save(order)) {
            mailSender.sendConfirmationEmail(order);
        }
    }

}
In diesem Beispiel hängt unsere OrderProcessorKlasse von zwei spezifischen Klassen ab: MySQLOrderRepositoryund ConfirmationEmailSender. Wir werden auch den Code dieser Klassen vorstellen:

public class MySQLOrderRepository {
    public boolean save(Order order) {
        MySqlConnection connection = new MySqlConnection("database.url");
        // Save the order in the database

        return true;
    }
}

public class ConfirmationEmailSender {
    public void sendConfirmationEmail(Order order) {
        String name = order.getCustomerName();
        String email = order.getCustomerEmail();

        // Send an email to the customer
    }
}
Diese Klassen sind weit entfernt von dem, was wir Abstraktionen nennen würden. Und aus der Sicht des Abhängigkeitsinversionsprinzips wäre es besser, zunächst einige Abstraktionen zu erstellen, mit denen wir in Zukunft arbeiten können, als spezifische Implementierungen. Lassen Sie uns zwei Schnittstellen erstellen: MailSenderund OrderRepository). Dies werden unsere Abstraktionen sein:

public interface MailSender {
    void sendConfirmationEmail(Order order);
}

public interface OrderRepository {
    boolean save(Order order);
}
Nun implementieren wir diese Schnittstellen in bereits dafür vorbereitete Klassen:

public class ConfirmationEmailSender implements MailSender {

    @Override
    public void sendConfirmationEmail(Order order) {
        String name = order.getCustomerName();
        String email = order.getCustomerEmail();

        // Send an email to the customer
    }

}

public class MySQLOrderRepository implements OrderRepository {

    @Override
    public boolean save(Order order) {
        MySqlConnection connection = new MySqlConnection("database.url");
        // Save the order in the database

        return true;
    }
}
Wir haben die Vorarbeit geleistet, damit unsere OrderProcessorKlasse nicht auf konkrete Details, sondern auf Abstraktionen angewiesen ist. Wir werden es ändern, indem wir unsere Abhängigkeiten zum Klassenkonstruktor hinzufügen:

public class OrderProcessor {

    private MailSender mailSender;
    private OrderRepository repository;

    public OrderProcessor(MailSender mailSender, OrderRepository repository) {
        this.mailSender = mailSender;
        this.repository = repository;
    }

    public void process(Order order){
        if (order.isValid() && repository.save(order)) {
            mailSender.sendConfirmationEmail(order);
        }
    }
}
Jetzt hängt unsere Klasse von Abstraktionen ab, nicht von bestimmten Implementierungen. Wir können sein Verhalten leicht ändern, indem wir die gewünschte Abhängigkeit zum Zeitpunkt der OrderProcessorObjekterstellung hinzufügen. Wir haben die SOLID-Designprinzipien in Java untersucht. Im CodeGym-Kurs erfahren Sie mehr über OOP im Allgemeinen und die Grundlagen der Java-Programmierung – nichts Langweiliges und Hunderte Stunden Übung. Zeit, ein paar Aufgaben zu lösen :)
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION