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.
GO TO FULL VERSION