Front End

14 nov, 2018

Usando padrões Decorator e Builder em um APIClient

Publicidade

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!