Equinócio
Fenômeno onde a duração do dia é idêntica à da noite e os hemisférios Norte e Sul recebem a mesma quantidade de luz.
O objetivo desse artigo é mostrar como podemos usar Protocol Oriented Programming para melhorarmos as aplicações, criando bibliotecas mais reutilizáveis e genéricas.
Apesar de todo o hype criado pela palestra na WWDC de 2015 sobre protocolos, esse não é um assunto novo no mundo de desenvolvimento iOS. Sempre usamos protocolos quando queremos por exemplo usar uma UITableView
ou uma UICollectionView
e é um padrão adotado em várias outras bibliotecas de código aberto.
Mesmo que Swift e Objective-C usem protocolos de forma extensa, as duas linguagens usam protocolos de forma bem diferente. Objective-C usa protocolos geralmente como uma maneira de diminuir o acoplamento entre objetos. Por exemplo, uma UITableView
não precisa saber quais tipos de objetos são responsáveis por ser sua data source ou delegate, tudo que ela precisa saber é que esse objetos implementam os protocolos UITableViewDataSource
e UITableViewDelegate
.
@interface ViewController : UIViewController <UITableViewDataSource, UITableViewDelegate>
@property (strong, nonatomic) IBOutlet UITableView *tableView;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.tableView.dataSource = self;
self.tableView.delegate = self;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return numberOfCellsForTable;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath];
// Customização da célula
return cell;
}
@end
Protocolos em Swift
Swift também incorpora o conceito de desacoplamento de objetos através do uso de protocolos, mas adiciona uma nova dimensão a estes. Um exemplo é quando são usados para generalizar código, através de Associated Types ou com o uso de Self na definição de métodos.
Vamos começar analisando o uso de Associated Types com o protocolo GeneratorType
e como ele encapsula a interação de objetos dentro de uma sequência. Para isso podemos separar sua definição em duas partes: primeiro ele define um Associated Type Element
, que terá seu tipo definido posteriormente por quem adote esse protocolo; em seguida, define a função next()
que retorna um valor opcional do tipo Element
.
protocol GeneratorType {
typealias Element
mutating func next() -> Self.Element?
}
Vale ressaltar que em Swift 2.2 a palavra reservada typealias
será substituída pela palavra associatedtype
na definição de tipos associados. Para ilustrar o uso de Associated Types, criamos um Generator que produz constantes quando seu método next()
é executado.
class ConstantGenerator: GeneratorType {
typealias Element = Int
func next() -> Int? {
return 100
}
}
let generator = ConstantGenerator()
while let c = generator.next() {
print("Gerando a constante \(c) ao infinito e além!")
// Gerando a constante 100 ao infinito e além!
}
Code Smell
Quando trabalhamos com APIs antigas, nem sempre podemos contar com o suporte de genéricos, o que vai contra a nova filosofia que Swift traz.
Swift is intended to be a small, expressive language with great support for building libraries. We’ll need generics to be able to build those libraries well.
Um exemplo dessa diferença é observado quando usamos CoreData. Para obtermos registros em nossa base de dados, usamos um NSFetchRequest que retorna um Array de AnyObject.
let fetchRequest = NSFetchRequest(entityName: "Person")
fetchRequest.predicate = NSPredicate(format: "name = %@", "João")
let results = try! context.executeFetchRequest(fetchRequest)
print(results.dynamicType) // Array<AnyObject>
Como vemos no código acima, precisamos usar uma String para indicar qual a entidade vamos buscar na base de dados e posteriormente é necessário um casting para acessarmos os atributos do tipo Person
.
let fetchRequest = NSFetchRequest(entityName: "Person")
fetchRequest.predicate = NSPredicate(format: "name = %@", "João")
let results = try! context.executeFetchRequest(fetchRequest) as! [Person]
print(results.dynamicType) // Array<Person>
print(results[0].name) // "João"
Quando vemos muitos casting ou uso de literais de forma desordenada podemos considerar isso um code smell. Felizmente com o uso de Protocol-Oriented Programming podemos deixar esse código um pouco mais Swifty.
Boa parte dos aplicativos que desenvolvemos usa algum tipo de persistência de dados local e um dos frameworks mais usados para atender esse requisito é o CoreData. Para o exemplo que vamos mostrar nesse artigo, vamos precisar entender outra maneira de como declarar protocolos genéricos.
Self Requirement
Podemos criar protocolos genéricos usando a palavra reservada Self na declaração de métodos e/ou variáveis. Ao incluir Self na declaração de métodos, estamos dizendo que aquele valor é apenas um placeholder e será substituído pelo tipo que adota este protocolo.
Para entender melhor o uso da palavra Self, vamos imaginar um protocolo que representa tipos de dados que serão lidos de alguma fonte de dados.
protocol Readeable {
static func byName(name: String) -> [Self]
}
class Person: Readeable {
static func byName(name: String) -> [Person] {
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
let context = appDelegate.managedObjectContext
let fetchRequest = NSFetchRequest(entityName: "Person")
fetchRequest.predicate = NSPredicate(format: "name = %@", name)
return try! context.executeFetchRequest(fetchRequest) as! [Person]
}
}
Quando adotamos o protocolo Readeable
na classe Person
podemos substituir o retorno da função byName(name:)
pelo tipo que implementa o protocolo.
Protocol Extension
Até o Swift 1.2 não podíamos fornecer uma implementação genérica para protocolos, o que muitas vezes resultava em duplicação de código. No exemplo abaixo se quisermos que a classe Pet
tenha as funções de Readable
, temos basicamente os mesmo código com pequenas modificações.
class Pet: Readeable {
static func byName(name: String) -> [Pet] {
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
let context = appDelegate.managedObjectContext
let fetchRequest = NSFetchRequest(entityName: "Pet")
fetchRequest.predicate = NSPredicate(format: "nickname = %@", name)
return try! context.executeFetchRequest(fetchRequest) as! [Pet]
}
}
Felizmente Swift 2.0 possibilitou através de Protocol Extension implementações padrão para métodos ou propriedades de protocolos, possibilitando a criação de código mais genérico e reutilizável.
Active Record
Nosso objetivo final nesse artigo é criar uma biblioteca que implemente parte do padrão arquitetural Active Record, utilizando CoreData como nossa base de dados.
Então vamos em frente e adicionar mais um protocolo que será responsável pela persistência dos dados em nossa base.
protocol Writeable {
func save()
}
Um outro protocolo que irá ser responsável por deletar objetos da nossa base de dados.
protocol Deletable {
func destroy()
}
E vamos modificar o protocolo Readable
deixando ele mais completo.
protocol Readable {
static func all() -> [Self]
static func find(predicate: NSPredicate) -> [Self]
}
Como já foi dito, iremos usar CoreData como forma de persistência, porém vale ressaltar que os protocolos Writeable
, Deletable
, Readable
não fazem nenhuma premissa de qual base de dados vamos usar ao longo do projeto.
A separação de operações em protocolos nos possibilita a criação de outros tipos que contenham apenas algumas funcionalidades.
Logo em seguida vamos usar herança entre protocolos para compor um novo protocolo chamado ActiveRecordType
. Este por sua vez, agrega as operações que um Active Record pode realizar.
protocol ActiveRecordType: Writeable, Deletable, Readable {
}
Abstraindo ainda mais, vamos definir o protocolo ModelType
, representando tipos que são armazenados em uma base de dados. Esse protocolo herda as funcionalidades de ActiveRecordType
, declara um Associated Type Context
, representando o contexto usado em operações na base de dados, e por fim uma variável para acessar esse contexto.
protocol ModelType: ActiveRecordType {
typealias Context
static var context: Self.Context { get }
}
Por fim, criamos o protocolo CoreDataModel
que herda de ModelType
e define o tipo de Context
que será utilizado. Já que estamos trabalhando com CoreData, naturalmente, nosso Context
será um NSManagedObjectContext
protocol CoreDataModel: ModelType {
typealias Context = NSManagedObjectContext
}
Revisando nossa arquitetura
Agora é uma boa hora para revisar nossa hierarquia antes de irmos em frente. Definimos protocolos para leitura (Readable
), escrita (Writeable
), remoção (Deleteable
) e agregamos todas as operações em um ActiveRecordType
. No final da hierarquia temos os protocolos ModelType
e CoreDataModel
que representam tipos que usam um contexto para armazenar dados.
Extension Time
Criando uma extension de NSManagedObject, podemos nos “livrar” de Strings com nomes de classe quando criamos um NSFetchRequest. Para isso, vamos adicionar a variável estática className
que retorna o nome da classe.
extension NSManagedObject {
static var className: String {
return String(self)
}
}
Agora vamos adicionar a implementação padrão para o protocolo CoreDataModel
. A primeira Protocol Extension que vamos criar vai definir qual tipo de contexto vamos usar e como ele será obtido. Extensões podem ser adicionadas em qualquer arquivo .swift
do projeto. Por motivo de simplicidade iremos assumir nesse artigo que o NSManagedObjectContext
que vamos retornar está declarado no AppDelegate
da nossa aplicação.
extension CoreDataModel where Self: NSManagedObject {
static var context: NSManagedObjectContext {
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
return appDelegate.managedObjectContext
}
}
Vamos começar simples com uma extensão que implementa o método save()
do protocolo Writeable
. Esta implementação é bem fácil - tudo que ela faz é acessar a variável estática context que definimos anteriormente e invocar o método save()
de NSManagedObjectContext
.
extension CoreDataModel where Self: NSManagedObject {
func save() {
try! Self.context.save()
}
}
Nossa segunda extensão também é bastante simples. Vamos implementar agora o métododestroy()
do protocolo Deletable
. Como fizemos anteriormente, vamos acessar a variável context e invocar o método deleteObject(_:)
e salvar a alteração.
extension CoreDataModel where Self: NSManagedObject {
func destroy() {
Self.context.deleteObject(self)
try! Self.context.save()
}
}
No método destroy()
também vemos self, dessa vez com letra minúscula, representando a instância do objeto que será deletado da base de dados.
Agora vamos criar a extensão para os métodos do protocolo Readable
que adiciona a implementação de all()
e find(predicate:)
. Nessa extensão, usamos Self em dois casos distintos: para acessar a variável className
e posteriormente fazer o casting para o tipo do objeto que está sendo pesquisado.
extension CoreDataModel where Self: NSManagedObject {
static func all() -> [Self] {
let fetchRequest = NSFetchRequest(entityName: Self.className)
fetchRequest.predicate = NSPredicate(value: true)
return try! context.executeFetchRequest(fetchRequest) as! [Self]
}
static func find(predicate: NSPredicate) -> [Self] {
let fetchRequest = NSFetchRequest(entityName: Self.className)
fetchRequest.predicate = predicate
return try! context.executeFetchRequest(fetchRequest) as! [Self]
}
}
Agora basta que nosso tipo adote o protocolo CoreDataModel
. Assim temos todos os métodos definidos em nossos protocolos, para qualquer tipo que adote CoreDataModel
. E o melhor de tudo, agora temos uma forma de acessar nossos dados de forma genérica, tipada e sem literais espalhados pelo código.
final class Person: NSManagedObject, CoreDataModel {
@NSManaged var name: String
}
let people = Person.all()
print(people.dynamicType) // Array<Person>
let namePredicate = NSPredicate(format: "name CONTAINS[cd] %@", "João")
let people = Person.find(namePredicate)
if let person = people.first {
print(person.name)
}
Isso é tudo pessoal! Espero ter ajudado nossa comunidade a crescer um pouco mais, mostrando como Protocol-Oriented Programming pode melhorar nossas vidas no dia-a-dia. Também criei um exemplo que pode ser acessado aqui.
Podemos conversar mais aqui @lopima e aqui!