JavaRush /Java-Blog /Random-DE /SOLIDE Prinzipien, die Ihren Code sauberer machen
Paul Soia
Level 26
Kiyv

SOLIDE Prinzipien, die Ihren Code sauberer machen

Veröffentlicht in der Gruppe Random-DE
Was ist SOLID? Hier ist, wofür das Akronym SOLID steht: - S: Single  Responsibility Principle. - O: Offen-Geschlossen-Prinzip  . - L: Liskov-Substitutionsprinzip  . - I: Prinzip der Schnittstellentrennung  . - D: Abhängigkeitsinversionsprinzip  . Die SOLID-Prinzipien geben Hinweise zum Entwurf von Modulen, d. h. Bausteine, aus denen die Anwendung aufgebaut ist. Der Zweck der Prinzipien besteht darin, Module zu entwerfen, die: - Veränderungen fördern - leicht verständlich sind - wiederverwendbar sind. Schauen wir uns also anhand von Beispielen an, was sie sind.
class MainRepository(
    private val auth: FirebaseAuth
) {
    suspend fun loginUser(email: String, password: String) {
        try {
            auth.signInWithEmailAndPassword(email, password)
        } catch (e: Exception) {
            val file = File("errors.txt")
            file.appendText(text = e.message.toString())
        }
    }
}
Was sehen wir in diesem Beispiel? FirebaseAuth wird im Konstruktor übergeben und wir verwenden es, um zu versuchen, uns anzumelden. Wenn wir als Antwort einen Fehler erhalten, schreiben wir die Nachricht in eine Datei. Sehen wir uns nun an, was mit diesem Code nicht stimmt. 1. S: Prinzip der Einzelverantwortung  : Eine Klasse (oder Funktion/Methode) sollte nur für eine Sache verantwortlich sein. Wenn eine Klasse für die Lösung mehrerer Probleme verantwortlich ist, sind ihre Subsysteme, die die Lösung dieser Probleme umsetzen, miteinander verbunden. Änderungen in einem solchen Subsystem führen zu Änderungen in einem anderen. In unserem Beispiel ist die Funktion loginUser für zwei Dinge verantwortlich: die Anmeldung und das Schreiben eines Fehlers in eine Datei. Damit es eine Verantwortlichkeit gibt, muss die Logik zum Aufzeichnen eines Fehlers in einer separaten Klasse untergebracht werden.
class FileLogger{
    fun logError(error: String) {
        val file = File("errors.txt")
        file.appendText(text = error)
    }
}
class MainRepository(
    private val auth: FirebaseAuth,
    private val fileLogger: FileLogger,
) {
    suspend fun loginUser(email: String, password: String) {
        try{
            auth.signInWithEmailAndPassword(email, password)
        } catch (e: Exception) {
            fileLogger.logError(e.message.toString())
        }
    }
}
Die Fehleraufzeichnungslogik wurde in eine separate Klasse verschoben und die loginUser-Methode hat jetzt nur noch eine Verantwortung – die Autorisierung. Wenn die Fehlerprotokollierungslogik geändert werden muss, tun wir dies in der FileLogger-Klasse und nicht in der Funktion loginUser 2. O: Open-Closed  -Prinzip: Software-Entitäten (Klassen, Module, Funktionen) müssen für Erweiterungen offen sein. aber nicht zur Änderung. In diesem Zusammenhang ist Offenheit gegenüber Erweiterungen die Fähigkeit, bei Bedarf neues Verhalten zu einer Klasse, einem Modul oder einer Funktion hinzuzufügen, und Offenheit gegenüber Änderungen bedeutet das Verbot, den Quellcode von Software-Entitäten zu ändern. Das klingt auf den ersten Blick kompliziert und widersprüchlich. Aber wenn man es sich anschaut, ist das Prinzip ganz logisch. Nach dem OCP-Prinzip wird Software nicht durch die Änderung vorhandener Codes, sondern durch das Hinzufügen von neuem Code geändert. Das heißt, der ursprünglich erstellte Code bleibt „intakt“ und stabil, und neue Funktionalität wird entweder durch Implementierungsvererbung oder durch die Verwendung abstrakter Schnittstellen und Polymorphismus eingeführt. Schauen wir uns also die FileLogger-Klasse an. Wenn wir Protokolle in eine andere Datei schreiben müssen, können wir den Namen ändern:
class FileLogger{
    fun logError(error: String) {
        val file = File("errors2.txt")
        file.appendText(text = error)
    }
}
Dann werden aber alle Protokolle in eine neue Datei geschrieben, die wir höchstwahrscheinlich nicht benötigen. Aber wie können wir unseren Logger ändern, ohne die Klasse selbst zu ändern?
open class FileLogger{

    open fun logError(error: String) {
        val file = File("error.txt")
        file.appendText(text = error)
    }

}

class CustomErrorFileLogger : FileLogger() {

    override fun logError(error: String) {
        val file = File("my_custom_error_file.txt")
        file.appendText(text = error)
    }

}
Wir markieren die FileLogger-Klasse und die logError-Funktion als geöffnet, erstellen eine neue CustomErrorFileLogger-Klasse und schreiben eine neue Protokollierungsimplementierung. Daher steht unsere Klasse zur Erweiterung der Funktionalität zur Verfügung, ist jedoch zur Änderung geschlossen. 3. L: Liskov-Substitutionsprinzip  . Es ist notwendig, dass Unterklassen als Ersatz für ihre Oberklassen dienen können. Der Zweck dieses Prinzips besteht darin, dass Nachkommenklassen anstelle der Elternklassen, von denen sie abgeleitet sind, verwendet werden können, ohne dass das Programm beschädigt wird. Stellt sich heraus, dass der Code den Typ einer Klasse überprüft, liegt ein Verstoß gegen das Substitutionsprinzip vor. Wenn wir die Nachfolgeklasse so schreiben würden:
class CustomErrorFileLogger : FileLogger() {

    fun customErrorLog(error: String) {
        val file = File("my_custom_error_file.txt")
        file.appendText(text = error)
    }

}
Und jetzt ersetzen wir die FileLogger-Klasse durch CustomErrorFileLogger im Repository
class MainRepository(
    private val auth: FirebaseAuth,
    private val fileLogger: CustomErrorFileLogger,
) {

    suspend fun loginUser(email: String, password: String) {
        try{
            auth.signInWithEmailAndPassword(email, password)
        }catch(e: Exception) {
            fileLogger.logError(e.message.toString())
        }
    }

}
In diesem Fall wird logError von der übergeordneten Klasse aufgerufen, oder Sie müssen den Aufruf von fileLogger.logError(e.message.toString()) in fileLogger.customErrorLog(e.message.toString()) ändern. 4. I : Prinzip der Schnittstellentrennung . Erstellen Sie hochspezialisierte Schnittstellen, die für einen bestimmten Kunden entwickelt wurden. Clients sollten nicht auf Schnittstellen angewiesen sein, die sie nicht nutzen. Es klingt kompliziert, ist aber eigentlich ganz einfach. Machen wir zum Beispiel die FileLogger-Klasse zu einer Schnittstelle, fügen ihr aber noch eine weitere Funktion hinzu:
interface FileLogger{

    fun printLogs()

    fun logError(error: String) {
        val file = File("errors.txt")
        file.appendText(text = error)
    }

}
Jetzt müssen alle Nachkommen die printLogs-Funktion implementieren, auch wenn wir sie nicht in allen Nachkommenklassen benötigen.
class CustomErrorFileLogger : FileLogger{

    override fun printLog() {

    }

    override fun logError(error: String) {
        val file = File("my_custom_error_file.txt")
        file.appendText(text = error)
    }

}
und jetzt haben wir leere Funktionen, was sich negativ auf die Code-Sauberkeit auswirkt. Stattdessen können wir einen Standardwert in der Schnittstelle festlegen und die Funktion dann nur in den Klassen überschreiben, in denen sie benötigt wird:
interface FileLogger{

    fun printLogs() {
        //Wieая-то дефолтная реализация
    }

    fun logError(error: String) {
        val file = File("errors.txt")
        file.appendText(text = error)
    }

}
class CustomErrorFileLogger : FileLogger{

    override fun logError(error: String) {
        val file = File("my_custom_error_file.txt")
        file.appendText(text = error)
    }

}
Jetzt werden die Klassen, die die FileLogger-Schnittstelle implementieren, sauberer. 5. D: Abhängigkeitsinversionsprinzip  . Das Objekt der Abhängigkeit sollte eine Abstraktion und nicht etwas Konkretes sein. Kehren wir zu unserer Hauptklasse zurück:
class MainRepository(
    private val auth: FirebaseAuth,
    private val fileLogger: FileLogger,
) {

    suspend fun loginUser(email: String, password: String) {
        try{
            auth.signInWithEmailAndPassword(email, password)
        } catch (e: Exception) {
            fileLogger.logError(e.message.toString())
        }
    }

}
Es scheint, dass wir hier bereits alles konfiguriert und behoben haben. Aber es gibt noch einen weiteren Punkt, der geändert werden muss. Dies verwendet die FirebaseAuth-Klasse. Was passiert, wenn wir irgendwann die Autorisierung ändern und uns nicht über Firebase, sondern beispielsweise über eine API-Anfrage anmelden müssen? Dann müssen wir vieles ändern, und das würden wir nicht wollen. Erstellen Sie dazu eine Schnittstelle mit der Funktion signInWithEmailAndPassword(email: String, password: String):
interface Authenticator{
    fun signInWithEmailAndPassword(email: String, password: String)
}
Diese Schnittstelle ist unsere Abstraktion. Und jetzt nehmen wir spezifische Implementierungen des Logins vor
class FirebaseAuthenticator : Authenticator{

    override fun signInWithEmailAndPassword(email: String, password: String) {
        FirebaseAuth.getInstance().signInWithEmailAndPassword(email, password)
    }

}
class CustomApiAuthenticator : Authenticator{

    override fun signInWithEmailAndPassword(email: String, password: String) {
        //другой способ логина
    }

}
Und in der Klasse „MainRepository“ gibt es nun keine Abhängigkeit von der konkreten Implementierung, sondern nur noch von der Abstraktion
class MainRepository (
    private val auth: Authenticator,
    private val fileLogger: FileLogger,
) {

    suspend fun loginUser(email: String, password: String) {
        try{
            auth.signInWithEmailAndPassword(email, password)
        }catch(e: Exception) {
            fileLogger.logError(e.message.toString())
        }
    }

}
Und jetzt müssen wir zum Ändern der Autorisierungsmethode nur eine Zeile in der Modulklasse ändern.
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION