JavaRush /Blog Java /Random-ES /Principios SÓLIDOS que harán que tu código sea más limpio...
Paul Soia
Nivel 26
Kiyv

Principios SÓLIDOS que harán que tu código sea más limpio

Publicado en el grupo Random-ES
¿Qué es SÓLIDO? Esto es lo que significa el acrónimo SOLID: - S:  Principio de Responsabilidad Única . - O: Principio Abierto-Cerrado  . - L: Principio de sustitución de Liskov  . - I: Principio de segregación de interfaces  . - D: Principio de inversión de dependencia  . Los principios SOLID aconsejan cómo diseñar módulos, es decir, ladrillos a partir de los cuales se construye la aplicación. El propósito de los principios es diseñar módulos que: - promuevan el cambio - sean fácilmente comprensibles - sean reutilizables Entonces, veamos cuáles son con ejemplos.
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())
        }
    }
}
¿Qué vemos en este ejemplo? FirebaseAuth se pasa en el constructor y lo usamos para intentar iniciar sesión. Si recibimos un error como respuesta, escribimos el mensaje en un archivo. Ahora veamos qué hay de malo en este código. 1. S: Principio de responsabilidad única  : una clase (o función/método) debe ser responsable de una sola cosa. Si una clase es responsable de resolver varios problemas, sus subsistemas que implementan la solución a estos problemas están conectados entre sí. Los cambios en uno de esos subsistemas conducen a cambios en otro. En nuestro ejemplo, la función loginUser es responsable de dos cosas: iniciar sesión y escribir un error en un archivo. Para que haya una responsabilidad, la lógica para registrar un error debe ubicarse en una clase separada.
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 lógica de registro de errores se ha movido a una clase separada y ahora el método loginUser tiene solo una responsabilidad: la autorización. Si es necesario cambiar la lógica de registro de errores, lo haremos en la clase FileLogger y no en la función loginUser 2. O: Principio abierto-cerrado  : las entidades de software (clases, módulos, funciones) deben estar abiertas para su extensión. pero no para modificación. En este contexto, la apertura a la extensión es la capacidad de agregar un nuevo comportamiento a una clase, módulo o función si surge la necesidad, y la apertura al cambio es la prohibición de cambiar el código fuente de las entidades de software. A primera vista esto suena complicado y contradictorio. Pero si lo miras bien, el principio es bastante lógico. Seguir el principio de OCP es que el software se cambia no cambiando el código existente, sino agregando código nuevo. Es decir, el código creado originalmente permanece “intacto” y estable, y se introducen nuevas funcionalidades ya sea mediante herencia de implementación o mediante el uso de interfaces abstractas y polimorfismo. Entonces, veamos la clase FileLogger. Si necesitamos escribir registros en otro archivo, podemos cambiar el nombre:
class FileLogger{
    fun logError(error: String) {
        val file = File("errors2.txt")
        file.appendText(text = error)
    }
}
Pero luego todos los registros se escribirán en un archivo nuevo, que probablemente no necesitemos. Pero, ¿cómo podemos cambiar nuestro registrador sin cambiar la clase misma?
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)
    }

}
Marcamos la clase FileLogger y la función logError como abiertas, creamos una nueva clase CustomErrorFileLogger y escribimos una nueva implementación de registro. Como resultado, nuestra clase está disponible para ampliar la funcionalidad, pero está cerrada a modificaciones. 3. L: Principio de sustitución de Liskov  . Es necesario que las subclases puedan servir como reemplazo de sus superclases. El propósito de este principio es que las clases descendientes se puedan utilizar en lugar de las clases principales de las que se derivan sin interrumpir el programa. Si resulta que el código está verificando el tipo de una clase, entonces se viola el principio de sustitución. Si escribiéramos la clase sucesora así:
class CustomErrorFileLogger : FileLogger() {

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

}
Y ahora reemplacemos la clase FileLogger con CustomErrorFileLogger en el repositorio.
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())
        }
    }

}
En este caso, se llamará a logError desde la clase principal, o tendrá que cambiar la llamada a fileLogger.logError(e.message.toString()) a fileLogger.customErrorLog(e.message.toString()) 4. I : Principio de segregación de interfaces . Cree interfaces altamente especializadas diseñadas para un cliente específico. Los clientes no deberían depender de interfaces que no utilizan. Suena complicado, pero en realidad es muy sencillo. Por ejemplo, hagamos de la clase FileLogger una interfaz, pero agreguemosle una función más:
interface FileLogger{

    fun printLogs()

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

}
Ahora todos los descendientes deberán implementar la función printLogs, incluso si no la necesitamos en todas las clases descendientes.
class CustomErrorFileLogger : FileLogger{

    override fun printLog() {

    }

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

}
y ahora tendremos funciones vacías, lo cual es malo para la limpieza del código. En su lugar, podemos crear un valor predeterminado en la interfaz y luego anular la función solo en aquellas clases en las que sea necesaria:
interface FileLogger{

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

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

}
Ahora las clases que implementarán la interfaz FileLogger serán más limpias. 5. D: Principio de inversión de dependencia  . El objeto de la dependencia debe ser una abstracción, no algo concreto. Volvamos a nuestra clase principal:
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())
        }
    }

}
Parece que ya hemos configurado y arreglado todo aquí. Pero aún queda un punto más que es necesario cambiar. Esto está usando la clase FirebaseAuth. ¿Qué pasará si en algún momento necesitamos cambiar la autorización e iniciar sesión no a través de Firebase, sino, por ejemplo, mediante algún tipo de solicitud de API? Entonces tendremos que cambiar muchas cosas y no querríamos eso. Para hacer esto, cree una interfaz con la función signInWithEmailAndPassword(correo electrónico: String, contraseña: String):
interface Authenticator{
    fun signInWithEmailAndPassword(email: String, password: String)
}
Esta interfaz es nuestra abstracción. Y ahora hacemos implementaciones específicas del inicio de sesión.
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) {
        //другой способ логина
    }

}
Y en la clase `MainRepository` ahora no hay dependencia de la implementación específica, sino solo de la abstracción.
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())
        }
    }

}
Y ahora, para cambiar el método de autorización, necesitamos cambiar solo una línea en la clase del módulo.
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION