Entendendo a Programação Orientada a Protocolos em Swift
Fala comunidade! Tudo certo com vocês?
Para quem não me conhece eu sou a Karol Atekitta, Eu falo sobre tecnologia e ajudo a transformar vidas através da programação. Sou Senior iOS Engineer at Riot Games via Xteam, e hoje estou aqui para falar um pouco sobre Programação Orientada a Protocolos em Switf.
Os protocolos são extremamente poderosos para garantir o desacoplamento, reaproveitamento, flexibilidade e a testabilidade do seu código. Sua utilização acabou ganhando uma aderência muito grande depois que o conceito de Programação Orientada a Protocolos foi apresentado por Dave Abrahams na Apple Worldwide Developers Conference (WWDC) de 2015.
Esse novo paradigma veio para resolver alguns problemas da Programação Orientada a Objetos, como por exemplo o uso exagerado de subclasses sem o conhecimento das consequências da herança dessas classes. À medida que aumentamos a complexidade do software, era muito comum cairmos em situações de efeitos colaterais onde alterações na classe herdada poderiam resultar em comportamentos inesperados nas subclasses. Com o uso dos protocolos, além de poder resolver essa questão da herança, eliminamos também a dependência entre classes, aumentando o nível de abstração.
No entanto, seu uso exagerado até levantou questionamentos na comunidade, como sugere Chris Eidhof em seu artigo "Protocol Oriented Programming is Not a Silver Bullet", que acaba sendo bem sensato nesse ponto. A tentativa de abstração extrema pode nos levar a um código que apesar de flexível seja extremamente complexo e nem sempre aumentar a complexidade nos trará um ganho compatível. De fato não existe bala de prata, porém vamos entender a seguir como esse recurso pode te ajudar a aumentar a qualidade e flexibilidade do seu código.
Segundo a documentação oficial da linguagem Swift:
"Um protocolo define um esquema de métodos, propriedades e outros requisitos que atendem a uma determinada tarefa ou parte de uma funcionalidade. O protocolo pode, então, ser adotado por uma classe, struct ou enum para fornecer uma implementação real desses requisitos. Qualquer tipo que satisfaça os requisitos de um protocolo é considerado em conformidade com o mesmo."
Em outras palavras, um protocolo é basicamente um contrato, onde estabeleço requisitos que devem ser implementados pelas representações que adotem esse protocolo, seja uma classe ou uma struct. Com esse contrato temos a garantia de que qualquer objeto que esteja em conformidade com o mesmo, implemente as propriedades e métodos definidos como requisitos.
protocol MusicalInstrument {
let name: String { get set }
func makeSound() -> String?
}
Nesse exemplo acima temos um protocolo que define os requisitos de um instrumento musical, que deve possuir nome e também implementar a função de emitir o som '’makeSound". É legal entender que o protocolo é uma concepção abstrata, então ele define as regras mas não as implementa. Baseado nesses requisitos podemos criar diversas classes que adotem esse mesmo protocolo.
struct Drum: MusicalInstrument {
let name: String
func makeSound() -> String? {
return "Tu dum tatz!"
}
}
struct Guitar: MusicalInstrument {
let name: String
func makeSound() -> String? {
return "Blen blen!"
}
}
Imagine que tenhamos uma classe que representasse um músico e ele tivesse a habilidade de tocar instrumentos. Inicialmente ele poderia tocar bateria e guitarra, porém outros instrumentos poderiam ser adicionados no futuro.
class Musician {
func playDrum(drum: Drum) {
drum.makeSound()
}
func playGuitar(guitar: Guitar) {
guitar.makeSound()
}
}
Através do uso de protocolos poderíamos criar apenas um método genérico que aceitaria qualquer instrumento, afinal o protocolo nos garante um contrato de requisitos que deve ser seguido e por isso sabemos que qualquer objeto que adote esse protocolo, irá implementar adequadamente o método '’makeSound'.
class Musician {
func play(instrument: MusicalInstrument) {
instrument.makeSound()
print("Playing \(instrument.name)")
}
}
A mágica aqui está na questão do músico em si não precisar saber que tipo de objeto está lidando, ou seja, ele não depende de uma implementação concreta mas sim de uma definição abstrata. Não importa qual tipo de classe seja passada em seu método "play desde que ela adote o protocolo de instrumento musical, cumprindo seus requisitos.
É possível que um objeto se conforme com vários protocolos e dessa forma podemos também segregar os comportamentos, algo que não poderíamos fazer com herança de classes.
protocol MusicalInstrument {
let name: String { get set }
func makeSound() -> String?
}
protocol EletricObject {
var isOn: Bool { get set }
func plugBattery()-> Void
}
struct Guitar: MusicalInstrument, EletricObject {
let name: String
var isOn: Bool = false
func plugBattery(){
isOn = true
}
func makeSound() -> String? {
guard isOn else { return nil }
return "Blem blem!"
}
}
No exemplo acima, criamos um novo protocolo para objetos elétricos onde teremos como requisito a implementação de uma flag para verificar se o objeto está conectado à bateria. Adicionamos então uma nova lógica na função "makeSound" para saber se o instrumento está ligado ou não, e caso não esteja, não retornaremos nenhum som emitido.
Essa possibilidade de segregar esses comportamentos criando protocolos específicos, vai ao encontro de um princípio importante do SOLID, o princípio da segregação de interfaces. Este princípio diz que as interfaces, ou protocolos, devem ser pequenas e específicas, e que cada classe deve implementar apenas as interfaces que ela precisa. Ou seja, é melhor ter várias interfaces pequenas do que uma única interface grande. Se colocássemos a flag 'isOn' em nosso protocolo de instrumento musical, os instrumentos que não fossem elétricos implementariam uma flag que não seria utilizada.
A Programação Orientada a Protocolos é um paradigma de desenvolvimento de software que permite a criação de abstrações de comportamentos esperados de um determinado tipo de objeto. A vantagem desse paradigma é que ele permite que você crie código genérico que pode ser aplicado a muitos tipos diferentes, desde que esses tipos sejam conformes ao protocolo. Isso significa que você pode escrever código que trabalhe com qualquer tipo que satisfaça as especificações do protocolo, sem precisar conhecer o tipo concreto ao escrever o código e essa é base quando pensamos em criar implementações com um maior nível de abstração.
Essa abstração acaba sendo fundamental na implementação de testes, pois isso permite que você crie, por exemplo, objetos que vão simular os comportamentos esperados, sendo possível testar partes isoladas do seu código sem necessitar da implementação concreta das suas dependências. Se quisessemos testar a nossa classe que representa o músico para saber se estamos tocando o instrumento devidamente, a partir do protocolo, poderíamos criar uma nova implementação que registra se o instrumento foi tocado ou não através de uma flag.
struct MusicalInstrumentMock: MusicalInstrument {
let name: String
var makeSoundWasCalled: Bool = false
func makeSound() -> String? {
makeSoundWasCalled = true
return ""
}
}
No exemplo acima, sabemos que o instrumento foi tocado pois estamos alterando a flag "makeSoundWasCalled" sempre que o método "makeSound" for chamado.
func testPlayInstrument() {
let musician = Musician()
let instrument = MusicalInstrumentMock()
musician.play(instrument)
XCTAssertTrue(instrument.makeSoundWasCalled)
}
Em nosso caso de teste então poderemos verificar pela flag, para saber se ao chamar o método play do nosso músico, ele irá corretamente chamar o método "makeSound" do nosso instrumento. Ao escrever testes com protocolo, você pode usar tipos fictícios para testar o comportamento esperado do código. Dessa forma, você pode garantir que o seu código está funcionando corretamente, independentemente de como ele é implementado. Por esse motivo a abstração de um código é tão importante para a qualidade do mesmo.
Isso também permite com que possamos implementar um outro conceito muito importante do SOLID que é a inversão de dependência. Este princípio diz que as classes devem depender de abstrações, e não de implementações concretas. Isso significa que as classes devem ser projetadas de forma a serem independentes das implementações concretas de outras classes, e justamente através dos protocolos conseguimos chegar a esse nível de abstração.
O Swift é uma linguagem de programação que oferece suporte a vários paradigmas: Programação Orientada a Objetos, Programação Orientada a Protocolos e Programação Funcional. Isso nos permite ter justamente a liberdade para escolher o paradigma que melhor se adequa às nossas necessidades. Além disso, é possível misturar e combinar diferentes paradigmas para obter soluções ainda mais flexíveis e adaptáveis.
Compreender a Programação Orientada a Protocolos, é a base para o desenvolvimento iOS. Isso porque a Programação Orientada a Protocolos é amplamente utilizada nos próprios frameworks nativos da plataforma, e ter conhecimento aprofundado sobre esse conceito certamente ajudará a se aprimorar e evoluir como desenvolvedor iOS.