Back-End

20 abr, 2016

Paginando objetos usando enums e generics em Swift

Publicidade

Recentemente criei uma pequena classe em Swift para trabalhar com objetos paginados. A classe usa generics, portanto não precisa de subclasses para ser usada. Nesse projeto usamos o ObjectMapper para fazer o mapeamento JSON<-> modelo Swift, e inicialmente a classe ficou assim:

import ObjectMapper
class Paginated<T: Mappable>: Mappable {
    var hasNext: Bool?
    var limit: Int?
    var page: Int?
    var data: [T]?
    
    required init?(_ map: Map) {
        
    }
    
    func mapping(map: Map) {
        hasNext <- map["hasNext"]
        limit <- map["limit"]
        page <- map["page"]
        data <- map["data"]
    }
}

Com isso, o data recebe um array com objetos que podem ser mapeados respeitando algum tipo T que também obedece ao protocolo Mappable, criado pelo ObjectMapper.

Digamos que temos uma lista de produtos vindo no array data e que criamos a classeProduct, que obedece ao protocolo Mappable. Ao receber o JSON, basta transformar para um objeto local assim:

if let paginated = data as? Paginated<Product> {
    //paginated.data contem array de Product

Mas depois eu passei a usar essa classe em um outro projeto que tinha uma “feature“: o nome do campo array no JSON de resposta do backend não é sempre o mesmo.

Decidi usar enum como forma de forçar segurança de tipo e garantir mais qualidade no código, evitando a “String Oriented Programming“. Criei, então, a variação abaixo:

enum PaginatedItemType: String {
    case Product
    case Category
    
    func itemsFieldName() -> String {
        switch self {
        case .Product:
            return "products"
        case .Category:
            return "categories"
        }
    }
}
 
class Paginated<T: Mappable>: Mappable {
    
    var currentPage: Int?
    var quantityItems: Int?
    var quantityPages : Int?
    var pageSize : Int?
    var lastPrice: String?
    var items: [T]?
    
    var itemsFieldName = ""
    
    required init?(_ map: Map) {
        if let name = PaginatedItemType(rawValue: String(T)) {
            itemsFieldName = name.itemsFieldName()
        }
        else {
            fatalError("It's not current possible to paginate")
        }
    }
    
    func mapping(map: Map) {
        items <- map[itemsFieldName]
        currentPage <- map["pagina_atual"]
        quantityItems <- map["quantidade_itens"]
        quantityPages <- map["quantidade_paginas"]
        pageSize <- map["tamanho_pagina"]
        lastPrice <- map["ultimo_preco"]
    }
    
}

A linha if let name = PaginatedItemType(rawValue: String(T)) garante que o nome tipo T pode ser usada para inicializar uma variável do tipo PaginatedItemType, ou seja, todo tipo paginado deve estar incluído em PaginatedItemType. Isso força o desenvolvedor – no caso, eu – a corrigir a implementação e falar qual o nome do campo que vem no JSON dentro do método itemsFieldName().

Os cases do enum PaginatedItemType são os nomes das classes que podem ser passados para Paginated, e como pode ser visto no init? (usado pelo ObjectMapper), se não for uma das classes “suportadas” (case definido no enum e o nome do campo definido no método itemsFieldName()), vai dar problema já na compilação.

String(T) consegue converter o nome da classe usada com esse genérico para uma String. Como defini o enum “herdando” de String, posso usar oPaginatedItemType(rawValue:) para iniciar via o nome da classe, mas se o nome da classe não estiver definida em um case, o enum retorna nil.

A princípio, não lembrei um jeito mais direto de converter direto do enum iniciando com o nome do tipo e devolvendo o nome do campo, então resolvi discutir com o pessoal aqui da Concrete Solutions. Foi então que o Daniel sugeriu adotar o protocoloCustomStringConvertible no enum. Tentei, então, esse código:

extension PaginatedItemType: CustomStringConvertible {
    var description: String {
        switch self {
        case .Product:
            return "products"
        case .Category:
            return "categories"
        }
    }
}

Com a inicialização assim:

   if let name = PaginatedItemType(rawValue: String(T)) as? String {
            itemsFieldName = name
        }
        else {
            itemsFieldName = "fuem"
        }

Mas não funcionou. A compilação falhava com a seguinte mensagem: Cast from ‘PaginatedItemType’ to unrelated type ‘String’ always fails.

Apesar de adotar o protocolo CustomStringConvertible, não é possível fazer a conversão direta usando um simples type casting. O correto é inicializar uma String passando como parâmetro um objeto que obedeça ao protocolo CutomStringConvertible. Assim, ficamos com:

let name = String(PaginatedItemType(rawValue: String(T)))

Melhorando a implementação com o CustomStringConvertible:

       if let page = PaginatedItemType(rawValue: String(T)) {
            let name = String(page)
            itemsFieldName = name
        }

Com isso, acabei com a versão final:

enum PaginatedItemType: String {
    case Product
    case Category
}
 
extension PaginatedItemType: CustomStringConvertible {
    var description: String {
        switch self {
        case .Product:
            return "products"
        case .Category:
            return "categories"
        }
    }
}
 
class Paginated<T: Mappable>: Mappable {
    
    var currentPage: Int?
    var quantityItems: Int?
    var quantityPages : Int?
    var pageSize : Int?
    var lastPrice: String?
    var items: [T]?
    
    var itemsFieldName = ""
    
    required init?(_ map: Map) {
        if let page = PaginatedItemType(rawValue: String(T)) {
            itemsFieldName = String(page)
        }
        else {
            fatalError("It's not current possible to paginate \(String(T))")
        }
    }
    
    func mapping(map: Map) {
        items <- map[itemsFieldName]
        currentPage <- map["pagina_atual"]
        quantityItems <- map["quantidade_itens"]
        quantityPages <- map["quantidade_paginas"]
        pageSize <- map["tamanho_pagina"]
        lastPrice <- map["ultimo_preco"]
    }
    
}

Muito mais seguro e flexível, usando alguns truques que não tínhamos em Objective-C (no passado, porque Objective-C tem evoluído e adicionou recentemente suporte agenerics).

Obrigado ao Daniel pela discussão, e espero que gostem.

Qualquer dúvida, sugestão ou comentário, só aproveitar os campos abaixo.