JavaRush /Blogue Java /Random-PT /Princípios SÓLIDOS que tornarão seu código mais limpo
Paul Soia
Nível 26
Kiyv

Princípios SÓLIDOS que tornarão seu código mais limpo

Publicado no grupo Random-PT
O que é SÓLIDO? Aqui está o que significa o acrônimo SOLID: - S:  Princípio de Responsabilidade Única . - O: Princípio Aberto-Fechado  . - L: Princípio da Substituição de Liskov  . - I: Princípio de segregação de interface  . - D: Princípio de Inversão de Dependência  . Os princípios SOLID aconselham como projetar módulos, ou seja, tijolos a partir dos quais o aplicativo é construído. O objetivo dos princípios é projetar módulos que: - promovam mudanças - sejam fáceis de entender - sejam reutilizáveis ​​Então, vamos ver o que são com exemplos.
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())
        }
    }
}
O que vemos neste exemplo? FirebaseAuth é passado no construtor e o usamos para tentar fazer login. Se recebermos um erro em resposta, gravamos a mensagem em um arquivo. Agora vamos ver o que há de errado com esse código. 1. S: Princípio da Responsabilidade Única  : Uma classe (ou função/método) deve ser responsável por apenas uma coisa. Se uma classe é responsável pela resolução de vários problemas, seus subsistemas que implementam a solução desses problemas estão interligados. Mudanças em um desses subsistemas levam a mudanças em outro. Em nosso exemplo, a função loginUser é responsável por duas coisas - fazer login e gravar um erro em um arquivo. Para que haja uma responsabilidade, a lógica de registro de um erro deve ser colocada em uma classe 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())
        }
    }
}
A lógica de registro de erros foi movida para uma classe separada e agora o método loginUser tem apenas uma responsabilidade - autorização. Se a lógica de registro de erros precisar ser alterada, faremos isso na classe FileLogger, e não na função loginUser 2. O: Princípio Aberto-Fechado  : Entidades de software (classes, módulos, funções) devem estar abertas para extensão, mas não para modificação. Neste contexto, a abertura à extensão é a capacidade de adicionar novo comportamento a uma classe, módulo ou função se necessário, e a abertura à mudança é a proibição de alterar o código-fonte das entidades de software. À primeira vista, isto parece complicado e contraditório. Mas se você olhar bem, o princípio é bastante lógico. Seguindo o princípio OCP é que o software é alterado não pela alteração do código existente, mas pela adição de novo código. Ou seja, o código originalmente criado permanece “intacto” e estável, e novas funcionalidades são introduzidas através de herança de implementação ou através do uso de interfaces abstratas e polimorfismo. Então, vamos dar uma olhada na classe FileLogger. Se precisarmos gravar logs em outro arquivo, podemos alterar o nome:
class FileLogger{
    fun logError(error: String) {
        val file = File("errors2.txt")
        file.appendText(text = error)
    }
}
Mas então todos os logs serão gravados em um novo arquivo, do qual provavelmente não precisamos. Mas como podemos mudar nosso logger sem mudar a própria classe?
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 a classe FileLogger e a função logError como abertas, criamos uma nova classe CustomErrorFileLogger e escrevemos uma nova implementação de log. Como resultado, nossa classe está disponível para extensão de funcionalidade, mas está fechada para modificação. 3. L: Princípio da Substituição de Liskov  . É necessário que as subclasses possam servir como substitutas de suas superclasses. O objetivo deste princípio é que as classes descendentes possam ser usadas no lugar das classes pai das quais são derivadas, sem interromper o programa. Se acontecer que o código está verificando o tipo de uma classe, o princípio da substituição será violado. Se escrevermos a classe sucessora assim:
class CustomErrorFileLogger : FileLogger() {

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

}
E agora vamos substituir a classe FileLogger por CustomErrorFileLogger no repositório
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())
        }
    }

}
Nesse caso, logError será chamado da classe pai, ou você terá que alterar a chamada de fileLogger.logError(e.message.toString()) para fileLogger.customErrorLog(e.message.toString()) 4. I : Princípio de Segregação de Interface . Crie interfaces altamente especializadas projetadas para um cliente específico. Os clientes não devem depender de interfaces que não utilizam. Parece complicado, mas na verdade é muito simples. Por exemplo, vamos transformar a classe FileLogger em uma interface, mas adicionar mais uma função a ela:
interface FileLogger{

    fun printLogs()

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

}
Agora todos os descendentes serão obrigados a implementar a função printLogs, mesmo que não precisemos dela em todas as classes descendentes.
class CustomErrorFileLogger : FileLogger{

    override fun printLog() {

    }

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

}
e agora teremos funções vazias, o que é ruim para a limpeza do código. Em vez disso, podemos definir um valor padrão na interface e então substituir a função apenas nas classes em que ela é necessária:
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)
    }

}
Agora as classes que irão implementar a interface do FileLogger ficarão mais limpas. 5. D: Princípio de Inversão de Dependência  . O objeto da dependência deve ser uma abstração, não algo concreto. Voltemos à nossa aula 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 já configuramos e consertamos tudo aqui. Mas ainda há mais um ponto que precisa ser mudado. Isso está usando a classe FirebaseAuth. O que acontecerá se em algum momento precisarmos alterar a autorização e fazer login não através do Firebase, mas, por exemplo, usando algum tipo de solicitação de API? Então teremos que mudar muitas coisas, e não quereríamos isso. Para isso, crie uma interface com a função signInWithEmailAndPassword(email: String, password: String):
interface Authenticator{
    fun signInWithEmailAndPassword(email: String, password: String)
}
Esta interface é nossa abstração. E agora fazemos implementações específicas do 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 na classe `MainRepository` agora não há dependência da implementação específica, mas apenas da abstração
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 agora, para alterar o método de autorização, precisamos alterar apenas uma linha na classe do módulo.
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION