JavaRush /Blog Java /Random-FR /Des principes SOLIDES qui rendront votre code plus propre...
Paul Soia
Niveau 26
Kiyv

Des principes SOLIDES qui rendront votre code plus propre

Publié dans le groupe Random-FR
Qu’est-ce que SOLIDE ? Voici ce que signifie l'acronyme SOLID : - S :  Principe de responsabilité unique . - O : Principe Ouvert-Fermé  . - L : Principe de substitution de Liskov  . - I : Principe de Ségrégation des Interfaces  . - D : Principe d'inversion de dépendance  . Les principes SOLID indiquent comment concevoir des modules, c'est-à-dire briques à partir desquelles l’application est construite. Le but des principes est de concevoir des modules qui : - favorisent le changement - sont faciles à comprendre - sont réutilisables Voyons donc ce qu'ils sont avec des exemples.
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())
        }
    }
}
Que voit-on dans cet exemple ? FirebaseAuth est passé dans le constructeur et nous l'utilisons pour essayer de nous connecter. Si nous recevons une erreur en réponse, nous écrivons le message dans un fichier. Voyons maintenant quel est le problème avec ce code. 1. S : Principe de responsabilité unique  : Une classe (ou fonction/méthode) ne doit être responsable que d'une seule chose. Si une classe est chargée de résoudre plusieurs problèmes, ses sous-systèmes qui mettent en œuvre la solution à ces problèmes sont connectés les uns aux autres. Les changements dans l’un de ces sous-systèmes entraînent des changements dans un autre. Dans notre exemple, la fonction loginUser est responsable de deux choses : la connexion et l'écriture d'une erreur dans un fichier. Pour qu'il y ait une seule responsabilité, la logique d'enregistrement d'une erreur doit être placée dans une classe distincte.
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 logique d'enregistrement des erreurs a été déplacée vers une classe distincte et la méthode loginUser n'a désormais qu'une seule responsabilité : l'autorisation. Si la logique de journalisation des erreurs doit être modifiée, nous le ferons dans la classe FileLogger, et non dans la fonction loginUser 2. O : principe ouvert-fermé : les entités logicielles (classes, modules, fonctions) doivent être ouvertes pour l'extension, mais pas pour modification. Dans ce contexte, l'ouverture à l'extension est la capacité d'ajouter un nouveau comportement à une classe, un module ou une fonction si le besoin s'en fait sentir, et la fermeture au changement est l'interdiction de modifier le code source des entités logicielles. À première vue, cela semble compliqué et contradictoire. Mais à bien y regarder, le principe est assez logique. Selon le principe OCP, le logiciel est modifié non pas en modifiant le code existant, mais en ajoutant un nouveau code. Autrement dit, le code créé à l'origine reste « intact » et stable, et de nouvelles fonctionnalités sont introduites soit par l'héritage de l'implémentation, soit par l'utilisation d'interfaces abstraites et de polymorphisme. Regardons donc la classe FileLogger. Si nous devons écrire des journaux dans un autre fichier, nous pouvons changer le nom :
class FileLogger{
    fun logError(error: String) {
        val file = File("errors2.txt")
        file.appendText(text = error)
    }
}
Mais alors tous les journaux seront écrits dans un nouveau fichier, dont nous n'avons probablement pas besoin. Mais comment changer notre logger sans changer la classe elle-même ?
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)
    }

}
Nous marquons la classe FileLogger et la fonction logError comme ouvertes, créons une nouvelle classe CustomErrorFileLogger et écrivons une nouvelle implémentation de journalisation. En conséquence, notre classe est disponible pour étendre les fonctionnalités, mais est fermée pour modification. 3. L : Principe de substitution de Liskov  . Il est nécessaire que les sous-classes puissent remplacer leurs superclasses. Le but de ce principe est que les classes descendantes peuvent être utilisées à la place des classes parentes dont elles sont dérivées sans interrompre le programme. S'il s'avère que le code vérifie le type d'une classe, alors le principe de substitution est violé. Si nous écrivions la classe successeur comme ceci :
class CustomErrorFileLogger : FileLogger() {

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

}
Et maintenant remplaçons la classe FileLogger par CustomErrorFileLogger dans le référentiel
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())
        }
    }

}
Dans ce cas, logError sera appelé depuis la classe parent, ou vous devrez modifier l'appel à fileLogger.logError(e.message.toString()) en fileLogger.customErrorLog(e.message.toString()) 4. Je : Principe de ségrégation des interfaces. Créez des interfaces hautement spécialisées conçues pour un client spécifique. Les clients ne doivent pas dépendre d'interfaces qu'ils n'utilisent pas. Cela semble compliqué, mais c'est en réalité très simple. Par exemple, faisons de la classe FileLogger une interface, mais ajoutons-y une fonction supplémentaire :
interface FileLogger{

    fun printLogs()

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

}
Désormais, tous les descendants devront implémenter la fonction printLogs, même si nous n'en avons pas besoin dans toutes les classes descendantes.
class CustomErrorFileLogger : FileLogger{

    override fun printLog() {

    }

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

}
et maintenant nous aurons des fonctions vides, ce qui est mauvais pour la propreté du code. Au lieu de cela, nous pouvons définir une valeur par défaut dans l'interface, puis remplacer la fonction uniquement dans les classes dans lesquelles elle est nécessaire :
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)
    }

}
Désormais, les classes qui implémenteront l'interface FileLogger seront plus propres. 5. D : Principe d'inversion de dépendance  . L'objet de la dépendance doit être une abstraction et non quelque chose de concret. Revenons à notre classe 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())
        }
    }

}
Il semble que nous ayons déjà tout configuré et corrigé ici. Mais il reste encore un point qui doit être modifié. Cela utilise la classe FirebaseAuth. Que se passera-t-il si, à un moment donné, nous devons modifier l'autorisation et nous connecter non pas via Firebase, mais, par exemple, en utilisant une sorte de requête API ? Ensuite, nous devrons changer beaucoup de choses, et nous ne voudrions pas cela. Pour cela, créez une interface avec la fonction signInWithEmailAndPassword(email : String, password : String) :
interface Authenticator{
    fun signInWithEmailAndPassword(email: String, password: String)
}
Cette interface est notre abstraction. Et maintenant nous réalisons des implémentations spécifiques de la connexion
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) {
        //другой способ логина
    }

}
Et dans la classe `MainRepository`, il n'y a désormais plus de dépendance à l'implémentation spécifique, mais uniquement à l'abstraction
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())
        }
    }

}
Et maintenant, pour changer la méthode d'autorisation, nous devons modifier une seule ligne dans la classe du module.
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION