JavaRush /Java-Blog /Random-DE /Fünf Grundprinzipien des Klassendesigns (SOLID) in Java
Ve4niY
Level 14

Fünf Grundprinzipien des Klassendesigns (SOLID) in Java

Veröffentlicht in der Gruppe Random-DE
Klassen sind die Blöcke, aus denen eine Anwendung erstellt wird. Genau wie die Ziegel in einem Gebäude. Schlecht geschriebene Kurse können eines Tages zu Problemen führen. Fünf Grundprinzipien des Klassendesigns (SOLID) in Java - 1Um zu verstehen, ob eine Klasse richtig geschrieben ist, können Sie die „Qualitätsstandards“ überprüfen. In Java sind dies die sogenannten SOLID-Prinzipien. Lass uns über sie reden.

SOLID-Prinzipien in Java

SOLID ist ein Akronym, das sich aus den Großbuchstaben der ersten fünf Prinzipien von OOP und Design zusammensetzt. Die Prinzipien wurden Anfang der 2000er Jahre von Robert Martin erfunden und das Akronym später von Michael Feathers geprägt. Folgendes beinhalten die SOLID-Prinzipien:
  1. Prinzip der Einzelverantwortung.
  2. Offen-Geschlossen-Prinzip.
  3. Liskovs 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 einer Klasse gekapselt ist. Alle Unterrichtsleistungen zielen darauf ab, dieser Verantwortung gerecht zu werden. Solche Klassen lassen sich bei Bedarf immer leicht ändern, da klar ist, wofür die Klasse verantwortlich ist und wofür nicht. Das heißt, es wird möglich sein, Änderungen vorzunehmen, ohne Angst vor den Konsequenzen zu haben – den Auswirkungen auf andere Objekte. Und solcher Code lässt sich viel einfacher testen, da Sie eine Funktionalität isoliert von allen anderen mit Tests abdecken. Stellen Sie sich ein Modul vor, das Bestellungen verarbeitet. Wenn die Bestellung korrekt ist, wird sie in der Datenbank gespeichert und eine E-Mail zur Bestätigung der Bestellung gesendet:
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");
        // Bestellung in der Datenbank speichern

        return true;
    }

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

        // Einen Brief an den Kunden senden
    }
}
Ein solches Modul kann sich aus drei Gründen ändern. Erstens kann die Logik der Auftragsabwicklung unterschiedlich sein, zweitens die Art der Speicherung (Datenbanktyp) und drittens die Art des Versendens eines Bestätigungsschreibens (z. B. müssen Sie anstelle einer E-Mail eine SMS senden). Das Prinzip der Einzelverantwortung impliziert, dass es sich bei den drei Aspekten dieses Problems tatsächlich um drei verschiedene Verantwortlichkeiten handelt. Dies bedeutet, dass sie in verschiedenen Klassen oder Modulen sein müssen. 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, das Modul in drei separate Module zu unterteilen, die jeweils eine einzige Funktion erfüllen:
public class MySQLOrderRepository {
    public boolean save(Order order) {
        MySqlConnection connection = new MySqlConnection("database.url");
        // Bestellung in der Datenbank speichern

        return true;
    }
}

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

        // Einen Brief an den Kunden senden
    }
}

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);
        }
    }

}

Offen/Geschlossen-Prinzip (OCP)

Dieses Prinzip lässt sich kurz und bündig wie folgt beschreiben: Software-Entitäten (Klassen, Module, Funktionen usw.) müssen für Erweiterungen offen, für Änderungen jedoch geschlossen sein . Das bedeutet, dass es möglich sein sollte, das äußere Verhalten einer Klasse zu ändern, ohne physische Änderungen an der Klasse selbst vorzunehmen. Nach diesem Prinzip werden Klassen so entwickelt, dass es zur Anpassung der Klasse an bestimmte Anwendungsbedingungen ausreicht, sie zu erweitern und einige Funktionen neu zu definieren. Daher muss das System flexibel sein und in der Lage sein, unter variablen Bedingungen zu arbeiten, ohne den Quellcode zu ändern. Um mit unserem Bestellbeispiel fortzufahren, nehmen wir an, dass wir einige Aktionen ausführen müssen, bevor die Bestellung bearbeitet wird und nachdem die Bestätigungs-E-Mail gesendet wurde. Anstatt die Klasse selbst zu ändern OrderProcessor, werden wir sie erweitern und eine Lösung für das vorliegende Problem erreichen, ohne das OCP-Prinzip zu verletzen:
public class OrderProcessorWithPreAndPostProcessing extends OrderProcessor {

    @Override
    public void process(Order order) {
        beforeProcessing();
        super.process(order);
        afterProcessing();
    }

    private void beforeProcessing() {
        // Führen Sie einige Aktionen aus, bevor Sie die Bestellung bearbeiten
    }

    private void afterProcessing() {
        // Nach der Auftragsbearbeitung einige Aktionen ausführen
    }
}

Barbara-Liskov-Substitutionsprinzip (LSP)

Dies ist eine Variation des zuvor diskutierten Offen/Geschlossen-Prinzips. Man kann es wie folgt beschreiben: Objekte in einem Programm können durch ihre Erben ersetzt werden, ohne dass sich die Eigenschaften des Programms ändern. Das bedeutet, dass eine Klasse, die durch Erweiterung einer Basisklasse entwickelt wird, ihre Methoden so überschreiben muss, dass 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 das erwartete Verhalten der überschriebenen Methoden nicht ändern. Unterklassen müssen Basisklassenmethoden so überschreiben, dass die Funktionalität aus Sicht des Clients nicht beeinträchtigt wird. Dies kann anhand des folgenden Beispiels im Detail untersucht werden. Nehmen wir an, wir haben eine Klasse, die für die Bestellvalidierung zuständig ist und prüft, ob alle Bestellartikel auf Lager sind. Diese Klasse verfügt über eine Methode , die true oder falseisValid 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: Überprüfen Sie, ob alle Waren der Bestellung auf Lager sind und ob alle Waren verpackt sind. Dazu haben wir die Klasse OrderStockValidatorum die Klasse erweitert 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;
    }
}
Allerdings haben wir in dieser Klasse gegen das LSP-Prinzip verstoßen, denn statt false zurückzugeben , wenn die Bestellung die Validierung nicht bestanden hat, löst unsere Methode eine Ausnahme aus IllegalStateException. Clients dieses Codes erwarten dies nicht: Sie erwarten die Rückgabe von true oder false . Dies kann zu Fehlern im Programm führen.

Interface-Split-Prinzip (ISP)

Gekennzeichnet durch die folgende Aussage: Clients sollten nicht gezwungen werden, Methoden zu implementieren, die sie nicht verwenden . Das Prinzip der Schnittstellentrennung legt nahe, dass zu „dicke“ Schnittstellen in kleinere und spezifischere unterteilt werden müssen, damit Clients kleiner Schnittstellen nur die für ihre Arbeit notwendigen Methoden kennen. Daher sollten sich beim Ändern einer Schnittstellenmethode Clients, die diese Methode nicht verwenden, nicht ändern. Schauen wir uns ein Beispiel an. Entwickler Alex hat die Schnittstelle „Bericht“ erstellt und zwei Methoden hinzugefügt: generateExcel()und generatedPdf(). Jetzt möchte Kunde A diese Schnittstelle nutzen, er möchte jedoch nur PDF-Berichte und nicht Excel verwenden. Wird er mit dieser Funktionalität zufrieden sein? Nein. Er muss zwei Methoden implementieren, von denen eine weitgehend unnötig ist und nur dank Alex, dem Software-Designer, existiert. Der Client verwendet entweder eine andere Schnittstelle oder lässt das Excel-Feld leer. Was ist also die Lösung? Es besteht darin, die bestehende Schnittstelle in zwei kleinere zu teilen. Einer ist ein Bericht im PDF-Format, der zweite ist ein Bericht im Excel-Format. Dadurch erhält der Nutzer die Möglichkeit, nur die für ihn notwendige Funktionalität zu nutzen.

Abhängigkeitsinversionsprinzip (DIP)

Dieses SOLID-Prinzip in Java wird wie folgt beschrieben: Abhängigkeiten innerhalb des Systems werden auf Basis von Abstraktionen aufgebaut . Module der obersten Ebene sind unabhängig von Modulen der unteren Ebene. Abstraktionen sollten nicht von Details abhängen. Details müssen von Abstraktionen abhängen. Software muss so konzipiert sein, dass die verschiedenen Module autonom sind und durch Abstraktion miteinander verbunden werden. Eine klassische Anwendung dieses Prinzips ist das Spring-Framework. Innerhalb des Spring-Frameworks werden alle Module als separate Komponenten implementiert, die zusammenarbeiten können. Sie sind so eigenständig, dass sie neben dem Spring-Framework auch in anderen Softwaremodulen problemlos verwendet werden können. Dies wird durch die Abhängigkeit von geschlossenen und offenen Prinzipien erreicht. Alle Module bieten nur Zugriff auf eine Abstraktion, die in einem anderen Modul verwendet werden kann. Versuchen wir dies anhand eines Beispiels zu demonstrieren. Als wir über den Grundsatz der Alleinverantwortung sprachen, haben wir einige Überlegungen angestellt OrderProcessor. 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 OrderProcessorvon zwei spezifischen Klassen ab MySQLOrderRepositoryund ConfirmationEmailSender. Wir präsentieren auch den Code für diese Klassen:
public class MySQLOrderRepository {
    public boolean save(Order order) {
        MySqlConnection connection = new MySqlConnection("database.url");
        // Bestellung in der Datenbank speichern

        return true;
    }
}

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

        // Einen Brief an den Kunden senden
    }
}
Diese Klassen werden bei weitem nicht als Abstraktionen bezeichnet. Und aus der Sicht des DIP-Prinzips wäre es richtiger, zunächst einige Abstraktionen zu erstellen, die es uns ermöglichen, in Zukunft mit ihnen zu arbeiten, statt mit spezifischen Implementierungen. Erstellen wir zwei Schnittstellen MailSenderund OrderRepository, die zu unseren Abstraktionen werden:
public interface MailSender {
    void sendConfirmationEmail(Order order);
}

public interface OrderRepository {
    boolean save(Order order);
}
Nun implementieren wir diese Schnittstellen in Klassen, die dafür bereits bereit sind:
public class ConfirmationEmailSender implements MailSender {

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

        // Einen Brief an den Kunden senden
    }

}

public class MySQLOrderRepository implements OrderRepository {

    @Override
    public boolean save(Order order) {
        MySqlConnection connection = new MySqlConnection("database.url");
        // Bestellung in der Datenbank speichern

        return true;
    }
}
Wir haben die Vorarbeit geleistet, sodass es in unserem Unterricht OrderProcessornicht auf konkrete Details, sondern auf Abstraktionen ankommt. Nehmen wir Änderungen daran vor, indem wir unsere Abhängigkeiten im Klassenkonstruktor einführen:
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);
        }
    }
}
Unsere Klasse ist jetzt eher auf Abstraktionen als auf konkrete Implementierungen angewiesen. Sie können das Verhalten leicht ändern, indem Sie die gewünschte Abhängigkeit zum Zeitpunkt der Instanzerstellung einfügen OrderProcessor. Wir haben uns SOLID – Designprinzipien in Java – angesehen. Mehr über OOP im Allgemeinen, die Grundlagen dieser Programmiersprache – nicht langweilig und mit Hunderten Stunden Übung – im JavaRush-Kurs. Zeit, einige Probleme zu lösen :) Fünf Grundprinzipien des Klassendesigns (SOLID) in Java - 2
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION