Esse é um conteúdo para quem já desenvolve em Swift e será um pouco mais avançado. Fica aqui o aviso para quem ainda está começando: não se preocupe, leia com atenção que você conseguirá entender e, também, aprenderá algo bastante importante.
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 primeira peça deste quebra-cabeça, começaremos falando sobre o S: Single Responsibility Principle. O que em português conhecemos como o Princípio da Responsabilidade Única.
Mas, o que é esse princípio?
Uncle Bob escreveu em seu blog a seguinte definição:
O Princípio de Responsabilidade Única (SRP) afirma que cada módulo de software deve ter um e apenas um motivo para mudar.
Com base nisso, vamos pensar num caso de uso em que, a grande maioria dos aplicativos possuem em comum: Realizar Login.
Diagrama de fluxo
No fluxograma abaixo, podemos identificar alguns pontos de validação no qual tomaremos como base para desenvolvermos a lógica do Login. Essas validações são: “Verifica se os dados estão preenchidos”, “Verifica se os dados são válidos” e redireciona para “Erro” ou “Home”.
Implementação
Agora que sabemos quais regras de negócio precisamos testar, vamos criar um código que realize essas ações. Para este post, criarei uma classe LoginViewModel
para fazer toda a validação.
LoginViewModel.swift
import Foundation
struct LoginRequest: Codable {
let username, password: String?
}
struct LoginResponse: Codable {
let validated: Bool
}
final class LoginViewModel {
var loginRequest: LoginRequest?
func makeLogin(_ username: String?, _ password: String?, completion: @escaping (Bool, String?) -> Void) {
guard username?.isEmpty == false || password?.isEmpty == false else {
completion(false, "Informe os seus dados para login.")
return
}
loginRequest = LoginRequest(username: username, password: password)
requestLogin { success, errorMessage in
completion(success, errorMessage)
}
}
func requestLogin(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()
}
}
Neste view model, podemos ver que ele possui várias responsabilidades, como por exemplo:
– Validar os dados informados pelo usúario
– Instanciar o objeto para realizar login
– Criar a requisição para a rota de login
– Manipular o retorno da requisição
– Manipular as exceções/erros
Mesmo com todas essas responsabilidades em uma única classe, esse código funciona e atende à implementação do caso de uso “Realizar Login”, porém, olhando para este view model, faço a seguinte reflexão: Se eu precisar validar e/ou enviar mais um campo no login, vou ter que alterar o view model?
Se a sua resposta foi sim, então você pensou corretamente. Aqui vamos destacar um ponto de atenção a respeito da definição do SRP: cada módulo de software deve ter um e apenas um motivo para mudar.
Qualquer alteração em algum dos 5 pontos citados acima já seria um risco enorme de alterar o comportamento de toda a classe view model.
Refactoring
Para que possamos ajustar a implementação desta classe atendendo às definições do Princípio da Responsabilidade Única, vamos precisar responder a algumas perguntas.
– De quem é a responsabilidade de enviar a solicitação de login para o server?
– De quem é a responsabilidade de validar os dados de login?
Obs.: faça outras perguntas a respeito de quais as responsabilidades desse view model.
Nesa primeira parte do refactor, vamos ajustar o método makeLogin
para receber um objeto de LoginRequest
, e com isso podemos transferir a responsabilidade de validar os dados de request para a própria classe LoginRequest
. Essa modificação também fará com que o método makeLog
in não precise sofrer modificações caso uma nova informação precise ser incluída no request. Veja o trecho de código a seguir:
LoginViewModel.swift – revision 1
struct LoginRequest: Codable {
let username, password: String?
let isDocument: Bool
func isValid() -> Bool {
var validated = username?.isEmpty == false || password?.isEmpty == false
if isDocument {
validated = username?.count == 11
}
return validated
}
}
final class LoginViewModel {
func makeLogin(_ loginRequest: LoginRequest, completion: @escaping (Bool, String?) -> Void) {
guard loginRequest.isValid() else {
completion(false, "Informe os seus dados para login.")
return
}
requestLogin(with: loginRequest) { success, errorMessage in
completion(success, errorMessage)
}
}
...
}
Com esta pequena alteração já conseguimos deixar o código da nossa view model bem melhor e com menos responsabilidade. Para deixar a classe apenas com a responsabilidade de realizar o login, independente de qual será a forma como será feito, vamos realizar um novo refactoring para remover da view model a responsabilidade de preparar o request e solicitar a validação dos dados do login para a API.
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) { success, errorMessage in
completion(success, errorMessage)
}
}
}
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()
}
}
Nessa parte do refactor, transferimos a responsabilidade realizar a requisição de login na API para a classe LoginWorker
, isso faz com que, agora, a nossa view model fique responsável apenas por orquestrar o fluxo de login. Da forma como está garantimos que, se a validação do objeto loginRequest
precisar de qualquer atualização, a view model continuará funcionando, sem a necessidade de alterações. Do mesmo modo, o request do login também pode ser modificado à vontade, e a view model permanecerá inalterada.
Conclusão
Separar a responsabilidade entre as classes do projeto é uma enorme vantagem para facilitar a manutenção do código. Como podemos ver, essa abordagem também permite que a estrutura do código fique auto documentada, pois temos cada classe definida e nomeada de acordo com a sua responsabilidade.
As vantagens na utilização dos princípios S.O.L.I.D. são de grande valia para todos os tipos de projetos e equipes, pois possibilitam a distribuição do trabalho de acordo com a necessidade de cada módulo/feature.
Vale ressaltar que neste post consideramos apenas o SRP, 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.
Referências
Princípios, Padrões e Práticas Ágeis em C#
The Single Responsibility Principle
The S.O.L.I.D Principles in Pictures
O que é SOLID: O guia completo para você entender os 5 princípios da POO