Programação Orientada a Protocolo e Tipos de Valor em Swift

Embora o Swift seja uma língua jovem, é extremamente diversificado. Não há uma maneira particular de desenvolver em Swift, ele abre inúmeras possibilidades. A Apple anunciou que Swift é a primeira linguagem orientada a protocolo . Eu recomendo fortemente que você assista ao vídeo sobre este tópico da WWDC 2015. Então, o que é uma programação orientada a protocolo em seu coração?

Começando.

Em primeiro lugar, é difícil o suficiente dar uma definição precisa de qualquer paradigma de programação, quer se trate do paradigma orientado a objetos ou orientado a protocolo. Você precisa entender que ambos são projetados com base em unidades e conceitos simples e que ambos devem resolver determinado conjunto de problemas.

De qualquer forma, é importante entender o que cada paradigma representa e quando vale a pena aplicá-lo. Vou tentar demonstrar a diferença entre OOP (paradigma orientado a objetos) e POP (paradigma orientado a protocolo)

Qual é o meu objeto?

Agora vamos falar sobre a unidade base OOP – um objeto . Se você é especialista em qualquer linguagem baseada em OOP (como Java ), provavelmente todos os aplicativos iniciados com uma classe. As classes não são apenas modelos para objetos de seu tipo, elas são, em última análise, os mesmos objetos!

Mas o que é um objeto em si? Bem, é um tipo de entidade que opera os tipos de valor mais primitivos. Ser uma entidade significa basicamente unicidade que por sua vez diz que cada objeto pode se comportar de forma diferente dependendo de seus valores operados.

Claro, você sabe sobre encapsulamento. Em OOP nós o usamos projetando nossas Classes a fim de fornecer segurança aos objetos. Mas isso funciona?

Você pode encapsular alguns de seus dados de acesso ilegítimo. Mas seu objeto ainda tem acesso total e direto a qualquer parte. E acontece que seu pior inimigo é você mesmo.

Comecei a falar sobre o conceito de segurança para mostrar a você a principal diferença entre os tipos que usamos em dois paradigmas diferentes. Assim que você tiver um objeto mesmo com dados encapsulados, ele pode ser modificado e, dependendo do processo de modificação, dados importantes podem ser danificados. Os tipos de valor foram projetados como unidades primitivas. Isso não significa que eles sejam apenas imutáveis. Mas uma vez que você modificou um tipo de valor, é conceitualmente uma nova unidade.

Do ponto de vista matemático, os objetos são entidades de tal importância que criam novas séries probabilísticas ou simplesmente afetam o contexto em que foram criados.

Conceitos OOP.

É uma boa prática iniciar qualquer implementação de modelo a partir de seu design básico. Você pesquisa as conexões entre valores dados e, com base nessa pesquisa, pode criar um esquema de relações entre objetos.

Um relacionamento pode ser declarado como uma herança ou uma implementação de protocolo. A diferença é que a herança, como o nome sugere, dá aos objetos a oportunidade de herdar os membros da classe uns dos outros (propriedades, métodos e inicializadores) e, posteriormente, substituí-los. Quanto aos protocolos, um objeto que implementa um protocolo deve implementar seus membros requeridos (métodos e propriedades).

O polimorfismo é outra característica importante da OOP. Dá flexibilidade aos tipos. Por exemplo, se você projetar um método trabalhando com um Vehicletipo, esse método pode funcionar com qualquer subtipo (por exemplo Car).

Esses conceitos-chave fornecem uma maneira incrivelmente flexível de criar modelos de hierarquia para qualquer tipo de relacionamento. Mas em algum lugar não é flexível o suficiente …

Conheça o zoológico!

Para mostrar os conceitos de OOP, escreveremos um programa simples que define relacionamentos entre objetos no zoológico.

Primeiro, o zoológico tem animais . Portanto, precisamos de uma hierarquia de tipos de animais. Para simplificar, vamos ter apenas cães, elefantes e formigas em nosso zoológico. Agora precisamos nos lembrar do curso da escola de biologia para criar classes básicas e suas subclasses. Um cão é um canídeo que é um mamífero que é um animal. Com isso dito, um tipo Animal é o mais alto em nosso modelo.

Para simplificar, não vamos continuar a nos preocupar com agrupamentos familiares e simplesmente definir um Elefante como uma subclasse de Mamífero e uma Formiga como uma subclasse de Inseto, que é uma subclasse de Animal.

Foto. 1
Cenário

OK, agora temos nosso modelo desenhado. Vamos começar com o código.

Classes básicas.

// MARK: - Base classes 

class Animal {
var name: String
var weight: Double

init
(name: String, weight: Double) {
self.name = name
self.weight = weight
}
}

class Mammal: Animal {
var minimumNumberOfChildren: Int

init
(name: String, weight: Double, minimumNumberOfChildren: Int) {
self.minimumNumberOfChildren = minimumNumberOfChildren
super.init(name: name, weight: weight)
}
}

class Insect: Animal {
var feetPair: Int

init
(name: String, weight: Double, feetPair: Int) {
self.feetPair = feetPair
super.init(name: name, weight: weight)
}
}

class Canid: Mammal {
func bark
() {
println
("Bark")
}

func play
() {
println
("Canid's playing...")
}
}

Classes de exemplo.

// MARK: - Example classes

class Dog: Canid {

}

class Ant: Insect {

}

Coisas de zoológico. Para zoo, precisamos de um pouco mais de coisas do que para outras classes implementadas. Primeiro, trata-se de armazenar animais. Em segundo lugar, trata-se de pegar animais e filtrá-los por seus tipos.

// MARK: - Zoo stuff

class Zoo {
private var animals = [Animal]()

func addAnimal
(animal: Animal) {
animals
+= [animal]
}

func allAnimals
() -> [Animal] {
return animals
}

func allMammals
() -> [Mammal] {
// [1]
return animals.filter { $0 is Mammal }.map { $0 as! Mammal }
}

func mammals
<T: Mammal>() -> [T] {
// [2]
return animals.filter { $0 is T }.map { $0 as! T }
}
}

Preste atenção aos marcadores.

  1. Filtramos objetos cujo tipo é Mamífero e retornamos a matriz desse tipo (ou seja ).[Mammal]
  2. Filtramos objetos cujo tipo é um T genérico que está em conformidade com um tipo Mammal e retornamos um array do tipo apropriado.

Vamos brincar com o código.

// MARK: - Playground 

let zoo = Zoo()

let dog = Dog(name: "Dog", weight: 15, minimumNumberOfChildren: 1)
let ant = Ant(name: "Ant", weight: 0.003, feetPair: 4)

zoo
.addAnimal(dog)
zoo
.addAnimal(ant)

Isso funciona muito bem. OOP é ótimo, não é?

Diamante mortal da morte.

O zoológico é bem estranho. Temos cães lá, embora tenham sido domesticados por milhares de anos, o que os torna nossos animais de estimação. Parabéns, acabamos de receber um novo tipo.

Mas espere … Como estendemos nosso modelo? A maneira ingênua é fazer algo assim:

class Pet: Animal {
func play
() {
println
("Pet's playing...")
}
}

Mas acontece que essa classe não pode ser herdada por cachorro.

/**
Multiple inheritance is not allowed.


How to determine which method is the right one?

*/

class Dog: Canid, Pet {

}

Este é o chamado Diamante Mortal da Morte .

Foto. 2
Cenário

O código acima está errado, seu compilador não permitirá que isso aconteça. Caso contrário, seria impossível saber qual método herdar e qual substituir. A herança múltipla não é permitida no Swift.

Mas como consertamos isso?

protocol Pet {
func play
() {
println
("Pet's playing...")
}
}

Na verdade, esta é apenas uma solução alternativa para o problema. Agora a questão é se seria adequado para o nosso modelo? Minha resposta é não. Perdemos a flexibilidade com este protocolo. Imagine que você precise operar Pets, mas agora como trabalhar com seus tipos básicos? Temos um relacionamento em torno do modelo Animal , mas não do Pet .

Os protocolos são nossos heróis.

Seria estúpido continuar com o esquema anterior. Não é mais flexível, é claro que se Pet for apenas um protocolo secundário, você ficará bem, mas se não for, é melhor criar um modelo flexível para protocolos. Vamos falar sobre protocolos e por que eles desempenham um papel tão importante no Swift.

Os conceitos OOP são bons para as aulas. Mas, na maioria dos casos, eles não são tão bons para tipos de valor, como structs e enums.

Você se lembra do que dizíamos sobre os objetos? Portanto, é hora de conversarmos sobre os tipos de valor, pois no POP a unidade básica é um tipo de valor.

Um tipo de valor é um tipo primitivo que geralmente é operado por tipos de alto nível, como os de objetos. É semelhante a primitivos – inteiros, duplos e flutuantes que também são tipos de valor. Portanto, cada vez que você trabalha com uma estrutura (seja ela qual for), você pode imaginar que trabalha com o mesmo valor, digamos 10 (que é um primitivo). No futuro, os tipos de valor representam uma espécie de contêiner para outros tipos. Lembre-se disso, pois este é um conceito básico de POP.

Mas como os protocolos se relacionam com os tipos de valor? Diretamente! Lembre-se de que uma estrutura não pode herdar outra estrutura, assim como um enum não pode herdar outro enum. Sim, OOP não funciona aqui. O que fazemos sobre isso? Começamos com um protocolo! Os tipos de valor podem implementar protocolos, vários protocolos.

O Zoo contra-ataca!

Espero que neste momento você esteja mais familiarizado com os fundamentos do POP. Portanto, vamos voltar à implementação do zoo e colocá-la em funcionamento com o conceito POP.

Protocolos de base.

// MARK: - Base protocols

protocol
Animal {
var name: String { get set }
var weight: Double { get set }
}

protocol
Mammal: Animal {
var minimumCountOfChildren: Int { get set }
}

protocol
Insect: Animal {
var feetPair: Int { get set }
}

protocol
Canid: Mammal {
func bark
()
func play
()
}

protocol
Pet {
func play
()
}

Tipos de valor de exemplo.

// MARK: - Example value types 

struct Dog: Canid, Pet {
// MARK: - Animal
var name: String
var weight: Double

// MARK: - Mammal
var minimumCountOfChildren: Int

func play
() {
println
("Dog's playing...")
}

func bark
() {
println
("Bark!")
}
}

struct Elephant: Mammal {
// MARK: - Animal
var name: String
var weight: Double

// MARK: - Mammal
var minimumCountOfChildren: Int
}

struct Ant: Insect {
// MARK: - Animal
var name: String
var weight: Double

// MARK: - Insect
var feetPair: Int
}

Preste atenção que agora usamos tipos de valor para Cachorro, Elefante e Formiga. Na verdade, são unidades primitivas e, além disso, precisamos de um modelo mais flexível, por isso aplicamos o conceito de POP.

As novas coisas do zoológico . Abordarei suas especificações abaixo.

// MARK: - Zoo stuff

// [1]
struct Zoo<T> {
private var entities = [T]()

// [2]
subscript
(index: Int) -> T {
set {
entities
+= [newValue]
}

get {
return entities[index]
}
}

// [3]
mutating func append
(object: T) {
entities
+= [object]
}

var count: Int {
return entities.count
}

/**
Returns all animals.

*/

func all
() -> [T] {
return entities
}

/**
Returns all given types' values.

*/

func allTypes
<U>() -> [U] {
// [4]
return entities.filter { $0 is U }.map { $0 as! U }
}
}
  1. O Zoo tem um tipo genérico T . Isso significa que, ao criar uma nova instância de Zoo, especificamos qual tipo de valor ela deve operar.
  2. Se você não está familiarizado com os subscritos, aprenda com a linguagem de programação Swift .
  3. Este é um método mutante que adiciona um novo objeto ao armazenamento de entidades (objetos do mesmo tipo). Ele está sofrendo mutação porque alteramos nosso tipo de valor.
  4. A filtragem é a mesma de antes. Verificamos o tipo e, em seguida, lançamos objetos.

Vamos adicionar mais conveniência para adicionar objetos ao zoológico. Para isso iremos sobrecarregar o +=operador.

func +=<T>(inout lhs: Zoo<T>, rhs: T) -> Zoo<T> {
lhs
[lhs.count + 1] = rhs
return lhs
}

Aqui vamos nós!

// MARK: - Playground

var zoo = Zoo<Animal>()

let dog = Dog(name: "Dog", weight: 20, minimumCountOfChildren: 1)
let elephant = Elephant(name: "Elephant", weight: 10000, minimumCountOfChildren: 1)
let ant = Ant(name: "Ant", weight: 0.03, feetPair: 4)

zoo
+= (dog)
zoo
+= (elephant)
zoo
+= (ant)

let allDogs = zoo.allTypes() as [Dog]
let all = zoo.allTypes() as [Animal] // But better is to use `zoo.all()`

Flexibilidade do POP.

Lembre-se do problema que tivemos com o protocolo Pet para o modelo OOP. Imagine que precisamos de um zoológico robótico com robôs. Mas um RoboticDog ainda é um Pet!

Tudo o que precisamos fazer é adicionar novos tipos de valor.

struct RoboticDog: Robot, Pet {
// MARK: - Robot
var name: String
var weight: Double
var batteryLife: Double

func play
() {
println
("Dog's playing...")
}
}

struct RoboticAnt: Robot {
// MARK: - Robot
var name: String
var weight: Double
var batteryLife: Double
}

struct RoboticElephant: Robot {
// MARK: - Robot
var name: String
var weight: Double
var batteryLife: Double
}

O zoológico é flexível. Então, só precisamos criar um zoológico robótico!

/// Robotic zoo
var roboticZoo = Zoo<Robot>()

let roboticDog = RoboticDog(name: "Robotic Dog", weight: 30, batteryLife: 100)
let roboticElephant = RoboticElephant(name: "Robotic Elephant", weight: 100000, batteryLife: 1000)
let roboticAnt = RoboticAnt(name: "Robotic Ant", weight: 0.3, batteryLife: 0.1)

roboticZoo
+= roboticDog
roboticZoo
+= roboticElephant
roboticZoo
+= roboticAnt

let allRoboticDogs = roboticZoo.allTypes() as [RoboticDog]
let allRobots = roboticZoo.all()

Conclusão.

Examinamos os conceitos de OOP e POP e as diferenças entre eles. E ainda assim a parte coberta é muito pequena, já que o POP é muito mais do que estávamos falando. Além disso, o Swift 2 traz um novo recurso de extensões