JavaRush /Java Blog /Random-IT /Principi SOLIDI che renderanno il tuo codice più pulito
Paul Soia
Livello 26
Kiyv

Principi SOLIDI che renderanno il tuo codice più pulito

Pubblicato nel gruppo Random-IT
Cos'è SOLIDO? Ecco cosa significa l'acronimo SOLID: - S: Principio di Responsabilità Singola  . - O: Principio Aperto-Chiuso  . - L: Principio di sostituzione di Liskov  . - I: Principio di segregazione delle interfacce  . - D: Principio di inversione delle dipendenze  . I principi SOLID consigliano come progettare i moduli, ad es. mattoni da cui è costruita l'applicazione. Lo scopo dei principi è quello di progettare moduli che: - promuovano il cambiamento - siano facilmente comprensibili - siano riutilizzabili Quindi, vediamo cosa sono con degli esempi.
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())
        }
    }
}
Cosa vediamo in questo esempio? FirebaseAuth viene passato nel costruttore e lo utilizziamo per provare ad accedere. Se riceviamo un errore in risposta, scriviamo il messaggio in un file. Ora vediamo cosa c'è che non va in questo codice. 1. S: Principio di responsabilità unica  : una classe (o funzione/metodo) dovrebbe essere responsabile di una sola cosa. Se una classe è responsabile della risoluzione di più problemi, i suoi sottosistemi che implementano la soluzione di questi problemi sono collegati tra loro. I cambiamenti in uno di questi sottosistemi portano a cambiamenti in un altro. Nel nostro esempio, la funzione loginUser è responsabile di due cose: effettuare il login e scrivere un errore in un file. Affinché vi sia una responsabilità, la logica per la registrazione di un errore deve essere collocata in una classe separata.
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())
        }
    }
}
La logica di registrazione degli errori è stata spostata in una classe separata e ora il metodo loginUser ha una sola responsabilità: l'autorizzazione. Se è necessario modificare la logica di registrazione degli errori, lo faremo nella classe FileLogger e non nella funzione loginUser 2. O: Principio aperto-chiuso  : le entità software (classi, moduli, funzioni) devono essere aperte per l'estensione, ma non per la modifica. In questo contesto, l'apertura all'estensione è la capacità di aggiungere un nuovo comportamento a una classe, modulo o funzione se se ne presenta la necessità, e la chiusura al cambiamento è il divieto di modificare il codice sorgente delle entità software. A prima vista, ciò sembra complicato e contraddittorio. Ma se lo guardi, il principio è abbastanza logico. Secondo il principio OCP, il software viene modificato non modificando il codice esistente, ma aggiungendo nuovo codice. Cioè, il codice originariamente creato rimane “intatto” e stabile, e nuove funzionalità vengono introdotte attraverso l’ereditarietà dell’implementazione o attraverso l’uso di interfacce astratte e polimorfismo. Diamo quindi un'occhiata alla classe FileLogger. Se dobbiamo scrivere i log su un altro file, possiamo cambiare il nome:
class FileLogger{
    fun logError(error: String) {
        val file = File("errors2.txt")
        file.appendText(text = error)
    }
}
Ma poi tutti i log verranno scritti in un nuovo file, di cui molto probabilmente non abbiamo bisogno. Ma come possiamo cambiare il nostro logger senza cambiare la classe stessa?
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)
    }

}
Contrassegniamo la classe FileLogger e la funzione logError come aperte, creiamo una nuova classe CustomErrorFileLogger e scriviamo una nuova implementazione di registrazione. Di conseguenza, la nostra classe è disponibile per estendere le funzionalità, ma è chiusa per modifiche. 3. L: Principio di sostituzione di Liskov  . È necessario che le sottoclassi possano servire da sostituti delle rispettive superclassi. Lo scopo di questo principio è che le classi discendenti possono essere utilizzate al posto delle classi madri da cui derivano senza interrompere il programma. Se risulta che il codice controlla il tipo di una classe, allora il principio di sostituzione è violato. Se scrivessimo la classe successore in questo modo:
class CustomErrorFileLogger : FileLogger() {

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

}
E ora sostituiamo la classe FileLogger con CustomErrorFileLogger nel 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 questo caso, logError verrà chiamato dalla classe genitore, oppure dovrai modificare la chiamata da fileLogger.logError(e.message.toString()) a fileLogger.customErrorLog(e.message.toString()) 4. I : Principio di segregazione delle interfacce . Crea interfacce altamente specializzate progettate per un cliente specifico. I client non dovrebbero dipendere da interfacce che non utilizzano. Sembra complicato, ma in realtà è molto semplice. Ad esempio, trasformiamo la classe FileLogger in un'interfaccia, ma aggiungiamo un'altra funzione:
interface FileLogger{

    fun printLogs()

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

}
Ora a tutti i discendenti verrà richiesto di implementare la funzione printLogs, anche se non ne abbiamo bisogno in tutte le classi discendenti.
class CustomErrorFileLogger : FileLogger{

    override fun printLog() {

    }

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

}
e ora avremo funzioni vuote, il che è dannoso per la pulizia del codice. Invece, possiamo creare un valore predefinito nell'interfaccia e quindi sovrascrivere la funzione solo nelle classi in cui è necessaria:
interface FileLogger{

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

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

}
Ora le classi che implementeranno l'interfaccia FileLogger saranno più pulite. 5. D: Principio di inversione delle dipendenze  . L'oggetto della dipendenza dovrebbe essere un'astrazione, non qualcosa di concreto. Torniamo alla nostra lezione principale:
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())
        }
    }

}
Sembra che abbiamo già configurato e corretto tutto qui. Ma c’è ancora un altro punto che deve essere cambiato. Questo sta usando la classe FirebaseAuth. Cosa accadrà se a un certo punto dovessimo modificare l'autorizzazione e accedere non tramite Firebase, ma, ad esempio, utilizzando qualche tipo di richiesta API? Allora dovremo cambiare molte cose e non lo vorremmo. Per fare ciò, crea un'interfaccia con la funzione signInWithEmailAndPassword(email: String, password: String):
interface Authenticator{
    fun signInWithEmailAndPassword(email: String, password: String)
}
Questa interfaccia è la nostra astrazione. E ora realizziamo implementazioni specifiche del login
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) {
        //другой способ логина
    }

}
E nella classe "MainRepository" ora non c'è più dipendenza dall'implementazione specifica, ma solo dall'astrazione
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())
        }
    }

}
E ora, per cambiare il metodo di autorizzazione, dobbiamo cambiare solo una riga nella classe del modulo.
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION