Este é o segundo de uma série de posts a respeito dos princípios S.O.L.I.D., caso ainda não tenha lido, a primeira parte pode ser vista aqui: Criando uma lógica de Login usando SOLID no Swift – Parte 1 de 5.
Nesta série de posts sobre o uso do SOLID no Swift, irei abordar cada um dos princípios de forma única. Portanto, sem mais delongas, como segunda peça deste quebra-cabeça, falaremos sobre o O: Open and Closed Principle. O que em português conhecemos como o Princípio Aberto e Fechado.
Mas, o que é esse princípio?
Segundo o pai do SOLID, Uncle Bob, em um de seus famosos livros “Princípios, Padrões e Práticas Ágeis”, a definição do OCP (Open-Closed Principle), é:
“Aberto para extensão.” Isso significa que o comportamento do módulo pode ser estendido.
“Fechado para modificação.” Estender o comportamento de um módulo não resulta em alterações no código-fonte ou binário do módulo.
Continuando o refactor do caso de uso “Realizar Login”, que exploramos no post anterior, vamos analisar como está o código atual para identificarmos quais serão as melhorias aplicadas.
Hands on
LoginViewModel.swift & LoginWorker.Swift – revision 2
final class LoginViewModel {
var worker: LoginWorker?
init(worker: LoginWorker? = LoginWorker()) {
self.worker = worker
}
func makeLogin(_ loginRequest: LoginRequest, completion: @escaping (Bool, String?) -> Void) {
guard loginRequest.isValid() else {
completion(false, "Informe os seus dados para login.")
return
}
worker?.requestLogin(with: loginRequest, completion: completion)
}
}
final class LoginWorker {
func requestLogin(with loginRequest: LoginRequest, completion: @escaping (Bool, String?) -> Void) {
let config: URLSessionConfiguration = URLSessionConfiguration.default
let session: URLSession = URLSession(configuration: config)
guard let url = URL(string: "url-do-login") else {
completion(false, "não foi possível realizar o login")
return
}
let urlRequest = NSMutableURLRequest(url: url)
let bodyParams: [String: Any?] = ["username": loginRequest.username, "password": loginRequest.password]
guard let postData = try? JSONSerialization.data(withJSONObject: bodyParams, options: []) else { return }
urlRequest.httpBody = postData as Data
let task = session.dataTask(with: urlRequest as URLRequest) { (result, _, error) in
guard error == nil else {
completion(false, "Request error: \(error?.localizedDescription ?? "generic error")")
return
}
guard let data = result else {
completion(false, "Nenhum dado retornado.")
return
}
do {
let decoder = JSONDecoder()
let decodableData: LoginResponse = try decoder.decode(LoginResponse.self, from: data)
DispatchQueue.main.async {
completion(decodableData.validated, nil)
}
} catch let exception {
let resultString = String(data: data, encoding: .utf8) ?? "empty data"
completion(false, "Decode error: " + exception.localizedDescription + "\nResult: \(resultString)")
}
}
task.resume()
}
}
Nossa classe LoginViewModel
está dependendo de uma classe concreta, a LoginWorker
, e isso a torna um pouco frágil, pois ficamos fortemente dependentes de como a classe LoginWorker
está implementada, ou usando o termo oficial da orientação a objetos, o view model está fortemente acoplado ao worker. Esse tipo de acoplamento faz com que o princípio open-closed seja quebrado na view model, pois ficaremos suscetíveis às possíveis mudanças no worker.
Um bom exemplo disso, seria o seguinte: se precisarmos que, antes de realizar o login via API, seja feita uma validação para verificar se o login já está registrado no banco de dados do dispositivo? Lembrando do SRP, sabemos que essa responsabilidade não é do view model e não podemos alterar a classe LoginWorker pois estaremos atropelando o OCP.
Então, qual seria a solução?
Refactoring
Para solucionar essa nova regra de login sem que a classe LoginViewModel
seja alterada, iremos criar um novo worker que implementará o mesmo protocol de worker que é informado no init da LoginViewModel
.
Esse novo worker será o responsável por validar se o login já se encontra registrado no banco de dados do device. Após criarmos este novo worker, basta inicializarmos a LoginViewModel
passando uma instância do LoginInDeviceWorker
no init da view model. Com isso, quando a view model executar o método requestLogin
, o código executado será o bloco de instrução que consulta o banco de dados local do aplicativo.
Vamos verificar como fica a utilização desse novo worker:
LoginInDeviceWorker & LoginViewModel – revision 3
final class LoginInDeviceWorker: LoginWorkerProtocol {
func requestLogin(with loginRequest: LoginRequest, completion: @escaping (Result<Bool, ApiError>) -> Void) {
guard loginRequest.username == "xpto" else {
completion(.failure(.invalidCredentials))
return
}
completion(.success(true))
}
}
final class LoginViewModel {
weak var worker: LoginWorkerProtocol?
init(worker: LoginWorkerProtocol? = LoginInDeviceWorker()) {
self.worker = worker
}
func makeLogin(_ loginRequest: LoginRequest, completion: @escaping (Result<Bool, ApiError>) -> Void) {
guard loginRequest.isValid() else {
completion(.failure(.invalidCredentials))
return
}
worker?.requestLogin(with: loginRequest, completion: completion)
}
}
Pronto, agora temos o código consultando o login no banco de dados do device. Podemos verificar que a classe LoginViewModel
permanece inalterada, com o mesmo comportamento de antes e, a partir de agora, realizando o login de uma forma diferente. Dessa forma, conseguimos assegurar que o OCP está sendo atendido com essa implementação.
Conclusão
Ao incorporar o OCP no desenvolvimento, podemos criar softwares que são mais robustos, flexíveis e fáceis de manter, proporcionando uma base sólida para o crescimento e evolução contínua da nossa aplicação. Em ambientes de desenvolvimento onde os requisitos mudam frequentemente (o que é muito comum no dia a dia de trabalho), o OCP permite que o sistema seja adaptado às novas necessidades de maneira eficiente e segura, sem comprometer a integridade do código existente.
Um ponto de destaque para a adoção deste princípio é quando várias equipes estão trabalhando no mesmo projeto. Seguir o OCP facilita a colaboração, pois cada equipe pode trabalhar em novas funcionalidades independentemente, sem a necessidade de modificar o código base.
Vale ressaltar que neste post consideramos apenas o OCP, nos próximos posts da série iremos continuar aprimorando esse caso de uso “Realizar Login”, para que esteja em conformidade com todos os princípios.
Se você chegou até aqui, obrigado pela atenção.
Quaisquer dúvidas, deixe nos comentários para enriquecermos o aprendizado.
Compartilhe este post com outros devs para que possamos trocar mais experiências.
Obs.: alguns padrões de projeto e outros princípios estão sendo utilizados nesta série de posts, porém, iremos abordar sobre eles em outra série de postagens, em breve.
Referências
Princípios, Padrões e Práticas Ágeis em C#
The S.O.L.I.D Principles in Pictures
O que é SOLID: O guia completo para você entender os 5 princípios da POO