Desenvolvimento

6 jul, 2018

Generics, o quebra galho para soluções genéricas

Publicidade

Enfrentamos um problema tanto em projetos pequenos, quanto grandes: o reuso de código, utilizado em inúmeros lugares. Ele faz com que não consigamos, às vezes, generalizar nossas classes para centralizar em um único algoritmo, ação ou comportamento padrão. O polimorfismo paramétrico, mais conhecido como Generics, no Swift, quebra um grande galho na hora de pensar no design de classes e algoritmos.

Vamos tomar como exemplo a construção de um Data Source e Delegate de uma UIcollectionView. Veremos como, geralmente, parece o código que utilizamos para configurar uma Collection View no Swift e então vamos refatorar esse código, deixando-o genérico para ser reutilizável em qualquer Collection View do nosso projeto.

import UIKit

class BeersCollectionViewDataSourceDelegate: NSObject, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {
    
    var beers: [BeerModel] = []
    var delegate: BeerListViewDelegate?
    
    override init() {
        super.init()
    }
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        
        return self.beers.count
        
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: BeerCollectionViewCellIdentifier, for: indexPath) as! BeerCollectionViewCell
        
        cell.setupCell(beer: self.beers[indexPath.row])
        
        return cell
        
    }
    
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        
        delegate?.didSelectedBeer(beer: self.beers[indexPath.row])
        
    }
    
}

No exemplo de código acima podemos encontrar alguns problemas que impedem que o nosso Data Source seja reutilizado pra qualquer Collection View. Um desses exemplos é o array de beers, que está tipado para ser um array de BeerModel, fazendo com que qualquer outro tipo de objeto que porventura queiramos apresentar, não seja suportado por esse Data Source.

O nosso dequeue de cell está com o Identifier da Cell, sendo buscado em uma constante e isso impede que qualquer outra cell possa ser instanciada por esse Data Source; temos ainda um delegate associado ao Data Source para nos informar qual foi a cerveja selecionada pelo usuário.

Todos esses são pontos que geram acoplamento, tornando a classe com pouco reuso, difícil de testar, suscetível a mudanças de implementação influenciadas por mudanças a objetos externos acoplados a ele, e assim por diante.

Vamos refatorar o nosso código e transformá-lo em um algoritmo genérico que poderá ser reutilizado por qualquer Collection View que queira exibir algum objeto e, porventura, capturar o objeto selecionado.

A primeira coisa que faremos é adaptar as nossas células para que elas possam prover algumas informações; vamos criar também um protocolo que vai garantir que nossa cell receba um tipo e saiba se configurar com ele:

protocol SetupableCell {
    associatedtype DataType
    func setupCell(data: DataType)
}

Nós vamos precisar que a nossa célula também forneça qual vai ser seu Identifier, mas para isso temos que criar mais um protocolo chamado ConfigurableCell:

protocol ConfigurableCell {
    static var cellIdentifier: String {get}
}

Vamos implementar esses protocolos em nossa Cell e conformar nossa cell com os protocolos que precisamos que ela implemente. Isso para que ela possa ser utilizada em qualquer Data Source que desejarmos:

class BeerCollectionViewCell: UICollectionViewCell, ViewCodingProtocol, SetupableCell, ConfigurableCell {
    
    typealias DataType = BeerModel
    static let cellIdentifier = "\(self)"
    
    let beerImageView: UIImageView = {
        let imageView = UIImageView()
        imageView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        imageView.contentMode = .scaleAspectFit
        imageView.clipsToBounds = true
        imageView.backgroundColor = .white
        imageView.layer.cornerRadius = 3.0
        imageView.layer.masksToBounds = true
        imageView.image = UIImage(named: "beer_placeholder")
        return imageView
    }()
    
    let backAlphaView: UIView = {
        let view = UIView()
        view.backgroundColor = UIColor.black.withAlphaComponent(0.65)
        view.layer.cornerRadius = 3.0
        view.layer.masksToBounds = true
        return view
    }()
    
    let beerName: UILabel = {
        let lbl = UILabel()
        lbl.font = UIFont(name: "System", size: 20.0)
        lbl.textColor = .white
        return lbl
    }()
    
    let beerAbv: UILabel = {
        let lbl = UILabel()
        lbl.font = UIFont(name: "System", size: 20.0)
        lbl.textColor = .white
        return lbl
    }()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        self.backgroundColor = .clear
        self.clipsToBounds = true
        self.setupViewConfiguration()
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func prepareForReuse() {
        super.prepareForReuse()
        
        self.beerImageView.af_cancelImageRequest()
        self.beerImageView.layer.removeAllAnimations()
        self.beerImageView.image = nil
        self.beerName.text = nil
        self.beerAbv.text = nil
    }
    
    // MARK: - Setup cell
    
    func setupCell(data: DataType) {
        
        self.alpha = 0
        UIView.animate(withDuration: 0.3) { 
            self.alpha = 1
        }
        
        if let url = data.beerImageUrl {
            self.loadImage(fromUrl: url)
        }
        
        if let name = data.beerName {
            self.beerName.text = "Name: \(name)"
        } else {
            self.beerName.text = "Name: - "
        }
        
        if let abv = data.beerAbv {
            self.beerAbv.text = "ABV: \(NSString(format: "%.2f", abv))"
        } else {
            self.beerAbv.text = "ABV: - "
        }

    }
    
    func loadImage(fromUrl url: String) {
        
        self.beerImageView.af_setImage(
            withURL: URL(string: url)!,
            placeholderImage: UIImage(named: "beer_placeholder")
        )
        
    }
    
    
    // MARK: - ViewCodingProtocol
    
    func buildViewHierarchy() {
        self.contentView.addSubview(self.beerImageView)
        self.backAlphaView.addSubview(self.beerName)
        self.backAlphaView.addSubview(self.beerAbv)
        self.contentView.addSubview(self.backAlphaView)
    }
    
    func setupConstraints() {
        
        self.contentView.snp.makeConstraints { (make) in
            make.width.equalTo(UIScreen.main.bounds.size.width)
            make.height.equalTo(250.0)
            make.leading.equalTo(0)
            make.top.equalTo(0)
        }
        
        self.beerImageView.snp.makeConstraints { (make) in
            make.top.equalToSuperview()
            make.bottom.equalToSuperview()
            make.left.equalTo(self.contentView.snp.left).offset(10.0)
            make.right.equalTo(self.contentView.snp.right).inset(10.0)
        }
        
        self.backAlphaView.snp.makeConstraints { (make) in
            make.height.equalTo(70.0)
            make.bottom.equalTo(self.contentView.snp.bottom).inset(0.0)
            make.left.equalTo(self.contentView.snp.left).offset(10.0)
            make.right.equalTo(self.contentView).inset(10.0)
        }
        
        self.beerName.snp.makeConstraints { (make) in
            make.height.equalTo(20.0)
            make.top.equalTo(self.backAlphaView.snp.top).offset(10.0)
            make.left.equalTo(self.backAlphaView.snp.left).offset(10.0)
            make.right.equalTo(self.backAlphaView.snp.right).inset(10.0)
        }
        
        self.beerAbv.snp.makeConstraints { (make) in
            make.height.equalTo(20.0)
            make.top.equalTo(self.beerName.snp.bottom).offset(10.0)
            make.left.equalTo(self.backAlphaView.snp.left).offset(10.0)
            make.right.equalTo(self.backAlphaView.snp.right).inset(10.0)
            make.bottom.equalTo(self.backAlphaView.snp.bottom).inset(10.0)
        }
        
    }
    
}

Podemos observar que concretizamos o nosso associatedtype DataType como o tipo BeerModel, para que quando a célula for se configurar através da func setupCell(data: DataType), ela saiba exatamente o que esperar para apresentar em suas propriedades.

Observamos também que atribuímos valor à propriedade cellIdentifier: static let cellIdentifier = “\(self)”. Nesse caso estamos retornando o próprio nome da classe como identifier da cell.

Tendo configurado a nossa Cell, vamos agora fazer as alterações no Data Source:

import UIKit

class CollectionViewDataSourceDelegate<T: UICollectionViewCell & SetupableCell & ConfigurableCell>: NSObject, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
    
    typealias DataType = T.DataType
    private var data: [DataType] = []
    private let selectionBlock: SelectedItem
    typealias SelectedItem = (_ selectedItem: DataType) -> ()
    
    init(data:[DataType] = [], selectedItem: @escaping SelectedItem) {
        self.selectionBlock = selectedItem
        self.data = data
    }
    
    func setDataSource(data: [DataType]) {
        self.data = data
    }
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        
        return self.data.count
        
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: T.cellIdentifier, for: indexPath) as! T
        
        let data = self.data[indexPath.row]
        
        cell.setupCell(data: data)
        
        return cell
        
    }
    
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        self.selectionBlock(self.data[indexPath.row])
    }
    
}

Em nosso refactor vamos apontar as principais mudanças e os impactos que isso traz para nós.

Primeiramente, podemos observar que a declaração do nosso Data Source mudou um pouquinho, já que adicionamos a ele a seguinte parte: <T: UICollectionViewCell & SetupableCell & ConfigurableCell>. Nesse caso, estamos fazendo uso do polimorfismo paramétrico; estamos informando para a nossa classe que vamos usar um tipo genérico T.

Mas não é só isso: estamos também assinando um contrato com a nossa classe, estabelecendo que T sempre vai ser um tipo que representa uma UICollectionViewCell, que implementa SetupableCell e ConfigurableCell, protocolos que implementamos anteriormente em nossa Cell que vai prover algumas informações para o nosso Data Source.

Já o nosso array de objetos, para ser apresentado agora, não é mais um array de beers com o tipo array de BeerModel, agora o nosso array se chama data e é um array do tipo T.DataType, ou seja, o tipo do modelo esperado pela nossa célula para se configurar.

Substituimos o nosso delegate, que informava o objeto selecionado da coleção por um callback, que vai fazer exatamente esse mesmo trabalho, nos devolvendo um objeto selecionado também do tipo que estamos trabalhando ali naquele momento.

Todas essas alterações fazem com que o nosso Data Source possa apresentar qualquer célula de qualquer tipo de dados, sejam eles quais forem. Nosso Data Source é genérico o suficiente para prover essa interface de apresentação a qualquer célula que deseje apresentar algum conteúdo.

E para utilizar esse Data Source, precisamos apenas concretizar T. Temos que passar uma cell que esteja em conforme com os protocolos SetupableCell e ConfigurableCell, para decidir o que fazer com o objeto selecionado, como no código abaixo:

self.dataSourceDelegate = CollectionViewDataSourceDelegate<BeerCollectionViewCell> { selectedItem in
    print(selectedItem)
}

Espero ter ajudado vocês a entenderem um pouco mais sobre polimorfismo paramétrico, reuso de código e design de classes. Se quiser, deixe um comentário nos campos abaixo.

Grande abraço!

***

Este artigo foi publicado originalmente em: https://www.concrete.com.br/2018/06/25/polimorfismo-parametrico-para-solucoes-genericas/