Este artigo foi publicado originalmente em: https://www.concrete.com.br/2018/11/02/usando-padroes-decorator-e-builder-em-um-apiclient/
***
Quase todos os aplicativos móveis precisam de conexão com a internet para fazer algo como atualizar as informações do usuário, verificar notícias, salvar dados e muito mais, então podemos dizer que é uma parte importante do nosso aplicativo.
Eu costumo usar Alamofire para fazer esse trabalho, e é uma boa prática isolar bibliotecas externas para que possamos alterá-las a qualquer momento, e acredite: essa hora vai chegar. Podemos isolar uma biblioteca através de um protocolo. Então vamos criar um simples APIClient que vai receber os parâmetros, headers e métodos para realizar um request.
Abaixo você pode ver um exemplo deste APIClient:
enum MethodHTTP: String {
case get = "GET"
case post = "POST"
case put = "PUT"
case patch = "PATCH"
}
public enum Result<T> {
case success(T)
case failure(Error)
}
protocol APIClientProtocol {
func request(parameters: [String: Any]?, headers: [String: String]?, method: MethodHTTP, completion: @escaping (Result<Data>) -> Void)
}
class CustomAPIClient: APIClientProtocol {
let sessionManager: SessionManager
let connectivity: NetworkReachabilityManager
init(sessionManager: SessionManager, connectivity: NetworkReachabilityManager) {
self.sessionManager = sessionManager
self.connectivity = connectivity
}
func request(parameters: [String: Any]?, headers: [String: String]?, method: MethodHTTP, completion: @escaping (Result<Data>) -> Void) {
//handle internet connectivity
if !connectivity.isReachable {
completion(.failure(CustomError.no_connection))
} else {
//do request
let fullPath = URL(string: "http://my.url.com")!
let method = Alamofire.HTTPMethod(rawValue: method.rawValue)!
sessionManager.request(fullPath, method: method, parameters: parameters, encoding: JSONEncoding(), headers: headers).validate(statusCode: 200 ..< 300).responseData { response in
switch response.result {
case .success:
if let data = response.data {
completion(.success(data))
break
}
fallthrough
case .failure:
//handle error
if let statusCode = response.response?.statusCode {
completion(.failure(CustomError.networking(statusCode)))
} else {
completion(.failure(CustomError.unknown))
}
}
}
}
}
}
enum CustomError: Error {
case no_connection
case networking(Int)
case unknown
var localizedDescription: String {
switch self {
case .networking:
return "Networking"
case .unknown:
return "Unknown"
case .no_connection:
return "No connection"
}
}
}
Este APIClient faz um monte de coisas, como verificar se há conexão com a internet, lidar com um erro e fazer uma solicitação ao servidor. Ele pode fazer muito mais, como injetar headers personalizados em cada solicitação ou manipular um token inválido para atualizá-lo. Imagine o nosso APIClient fazendo todas essas coisas. Ele vai ficar maior, confuso e bem difícil de dar manutenção.
É comum encontrar uma classe que faz muitas coisas, fazendo mais coisas pelas quais é responsável. E isso vai contra o princípio da responsabilidade única do SOLID.
Dessa forma podemos usar o padrão Decorator para tornar nosso APIClient menor e mais específico.
O padrão Decorator é um padrão de design que permite que um comportamento seja adicionado a um objeto individual, dinamicamente, sem afetar o comportamento de outros objetos da mesma classe.
No exemplo acima podemos quebrar nossas funções APIClient em três:
- Verificar se há conexão com a Internet
- Lidar com erro
- Fazer a nossa solicitação
Um Decorator deve ter uma referência para outro objeto do mesmo tipo e esse objeto vai ser ‘decorado’. O objetivo desses padrões é adicionar um comportamento a outro objeto sem a necessidade de modificar a sua classe, então podemos refatorá-lo em dois ‘decorators’: um para verificar se há conexão com a internet e o outro para lidar com os erros.
lass ReachabilityAPIClient: APIClientProtocol {
let apiClient: APIClientProtocol
let connectivity: NetworkReachabilityManager
init(apiClient: APIClientProtocol, connectivity: NetworkReachabilityManager) {
self.apiClient = apiClient
self.connectivity = connectivity
}
func request(parameters: [String: Any]?, headers: [String: String]?, method: MethodHTTP, completion: @escaping (Result<Data>) -> Void) {
if !connectivity.isReachable {
completion(.failure(CustomError.no_connection))
} else {
self.apiClient.request(parameters: parameters, headers: headers, method: method) { (result) in
completion(result) }
}
}
}
class HandleErrorAPIClient: APIClientProtocol {
let apiClient: APIClientProtocol
init(apiClient: APIClientProtocol) {
self.apiClient = apiClient
}
func request(parameters: [String: Any]?, headers: [String: String]?, method: MethodHTTP, completion: @escaping (Result<Data>) -> Void) {
self.apiClient.request(parameters: parameters, headers: headers, method: method) { (result) in
switch result {
case .success:
completion(result)
case .failure(let error):
if let error = error as? RequestCodeError,
case let .status(code) = error,
let statusCode = code {
completion(.failure(CustomError.networking(statusCode)))
} else {
completion(.failure(CustomError.unknown))
}
}
}
}
}
Depois disso refatoramos o nosso APIClient para um RequestAPIClient:
enum RequestCodeError: Error {
case status(code: Int?)
}
class RequestAPIClient: APIClientProtocol {
let sessionManager: SessionManager
init(sessionManager: SessionManager) {
self.sessionManager = sessionManager
}
func request(parameters: [String: Any]?, headers: [String: String]?, method: MethodHTTP, completion: @escaping (Result<Data>) -> Void) {
let fullPath = URL(string: "http://my.url.com")!
let method = Alamofire.HTTPMethod(rawValue: method.rawValue)!
sessionManager.request(fullPath, method: method, parameters: parameters, encoding: JSONEncoding(), headers: headers).validate(statusCode: 200 ..< 300).responseData { response in
switch response.result {
case .success:
if let data = response.data {
completion(.success(data))
break
}
fallthrough
case .failure:
let statusCode = response.response?.statusCode
completion(.failure(RequestCodeError.status(code: statusCode)))
}
}
}
}
Nosso último APIClient será uma mistura de Decorators. Nosso ReachabilityAPIClient vai solicitar ao nosso ErrorHandlerAPIClient que por sua vez solicitará ao nosso RequestAPIClient.
Quando o servidor retorna com uma resposta, primeiro ele vai ser manipulado pelo nosso RequestAPIClient, depois pelo ErrorHandlerAPIClient e, finalmente, pelo nosso ReachabilityAPIClient.
let apiClient = RequestAPIClient(sessionManager: SessionManager())
let errorHandlerAPIClient = HandleErrorAPIClient(apiClient: apiClient)
let connectivityAPIClient = ReachabilityAPIClient(apiClient: errorHandlerDecorator, connectivity: NetworkReachabilityManager())
//example
connectivityAPIClient.request(...)
A forma como construímos o nosso APIClient pode ser um pouco confusa e, facilmente, alguém pode cometer um erro e esquecer de usar um Decorator. Por exemplo, esquecer de adicionar o comportamento de lidar com erros. Assim, podemos criar um builder para ajudar nossos desenvolvedores a decidir qual comportamento eles vão adicionar ao objeto APIClient.
O Builder é um padrão de design projetado para fornecer uma solução flexível para vários problemas de criação de objetos na programação orientada a objetos. A intenção do padrão de design do Builder é separar a construção de um objeto complexo de sua representação.
class APIClientBuilder {
private var apiClient: APIClient
init(sessionManager: SessionManager = SessionManager()) {
apiClient = RequestAPIClient(sessionManager: sessionManager)
}
func withErrorHandler() -> APIClientBuilder {
apiClient = HandlerErrorAPIClient(apiClient: apiClient)
return self
}
func connectivity(connectivity: NetworkReachabilityManager = NetworkReachabilityManager()) -> APIClientBuilder {
apiClient = ReachabilityAPIClient(apiClient: apiClient, connectivity: connectivity)
return self
}
func build() -> APIClient {
return apiClient
}
}
//instance
let apiClient = APIClientBuilder().withErrorHandler().connectivity().build()
apiClient.request(...)
Se você gostou deste artigo, compartilhe-o no Twitter, recomende-o no LinkedIn, ou ambos. Isso realmente me ajuda a alcançar mais pessoas.
Muito obrigado!