Programação Orientada a Protocolo no Swift 5.1

Os protocolos são uma propriedade fundamental do Swift. Eles desempenham um papel importante nas bibliotecas Swift padrão e são uma maneira comum de codificar abstração. De muitas maneiras, eles são semelhantes às interfaces em outras linguagens de programação.

Neste tutorial, apresentaremos uma abordagem de desenvolvimento de aplicativo chamada programação orientada a protocolo, que se tornou quase o principal no Swift. Isso é realmente o que você precisa entender ao aprender Swift!

Neste guia, você:

  • entender a diferença entre programação orientada a objeto e orientada a protocolo;
  • entender as implementações de protocolo padrão;
  • Aprenda como estender a funcionalidade da biblioteca Swift padrão;
  • Aprenda a estender protocolos com genéricos.

Começando


Imagine que você está desenvolvendo um jogo de corrida. Seus jogadores podem dirigir carros, motos e aviões. E mesmo voando em pássaros, isso é um jogo, certo? O principal aqui é que há uma dofiga de todos os tipos de "coisas" nas quais você pode dirigir, voar, etc.

Uma abordagem comum para o desenvolvimento de tais aplicativos é a programação orientada a objetos. Nesse caso, incluímos toda a lógica em algumas classes base, das quais herdamos a partir de então. As classes base, portanto, devem conter a lógica de "lead" e "pilot".

Começamos o desenvolvimento criando classes para cada veículo. Adiaremos os "Pássaros" para mais tarde, retornaremos a eles um pouco mais tarde.

Vemos que carro e motocom funcionalidade muito semelhante, criamos a classe base MotorVehicle . Carro e motocicleta serão herdados do MotorVehicle. Da mesma forma, criamos a classe base Aircraft , a partir da qual criaremos a classe Plane .

Você acha que está tudo bem, mas - bam! - A ação do seu jogo ocorre no século XXX, e alguns carros já podem voar .

Então, nós tivemos uma mordaça. Swift não tem herança múltipla. Como seus carros voadores podem ser herdados do MotorVehicle e Aircraft? Criar outra classe base que combina as duas funcionalidades? Provavelmente não, pois não há uma maneira clara e simples de conseguir isso.

E o que salvará nosso jogo nessa situação terrível? A programação orientada a protocolos tem pressa para ajudar!

Como é a programação orientada a protocolo?


Os protocolos permitem agrupar métodos, funções e propriedades semelhantes para classes, estruturas e enumerações. No entanto, apenas as classes permitem usar a herança da classe base.

A vantagem dos protocolos no Swift é que um objeto pode estar em conformidade com vários protocolos.

Ao usar esse método, seu código se torna mais modular. Pense em protocolos como blocos de construção de funcionalidade. Quando você adiciona uma nova funcionalidade a um objeto, tornando-o em conformidade com um determinado protocolo, você não está criando um objeto completamente novo a partir do zero, isso seria muito longo. Em vez disso, você adiciona diferentes blocos de construção até que o objeto esteja pronto.

Passar de uma classe base para protocolos resolverá o nosso problema. Com os protocolos, podemos criar uma classe FlyingCar que corresponda ao MotorVehicle e Aircraft. Legal, hein?

Vamos fazer o código


Execute o Xcode, crie um playground, salve como SwiftProtocols.playground, adicione este código:

protocol Bird {
  var name: String { get }
  var canFly: Bool { get }
}

protocol Flyable {
  var airspeedVelocity: Double { get }
}


Compile com Command-Shift-Return para garantir que tudo esteja em ordem.

Aqui, definimos um protocolo Bird simples , com as propriedades name e canFly . Em seguida, definimos o protocolo Flyable com a propriedade airspeedVelocity .

Na "era pré-protocolo", o desenvolvedor começaria com a classe Flyable como a classe base e, usando a herança, definiria Bird e tudo o mais que pudesse voar.

Mas na programação orientada a protocolo, tudo começa com um protocolo. Essa técnica nos permite encapsular um esboço de uma funcional sem uma classe base.

Como você verá agora, isso torna o processo de criação de tipos muito mais flexível.

Determine o tipo correspondente ao protocolo


Adicione este código na parte inferior do playground:

struct FlappyBird: Bird, Flyable {
  let name: String
  let flappyAmplitude: Double
  let flappyFrequency: Double
  let canFly = true

  var airspeedVelocity: Double {
    3 * flappyFrequency * flappyAmplitude
  }
}


Este código define uma nova estrutura FlappyBird que está em conformidade com o protocolo Bird e o protocolo Flyable. Sua propriedade airspeedVelocity é um trabalho de flappy Frequency e flappy Amplitude. A propriedade canFly retorna true.

Agora adicione as definições de mais duas estruturas:

struct Penguin: Bird {
  let name: String
  let canFly = false
}

struct SwiftBird: Bird, Flyable {
  var name: String { "Swift \(version)" }
  let canFly = true
  let version: Double
  private var speedFactor = 1000.0
  
  init(version: Double) {
    self.version = version
  }

  // Swift is FASTER with each version!
  var airspeedVelocity: Double {
    version * speedFactor
  }
}


O pinguim é um pássaro, mas não pode voar. É bom que não usemos herança e não tornamos todos os pássaros voadores !

Ao usar protocolos, você define os componentes do funcional e faz com que todos os objetos adequados estejam em conformidade com o protocolo.Em

seguida, definimos o SwiftBird , mas em nosso jogo existem várias versões diferentes dele. Quanto maior o número da versão, maior sua velocidade do arVelocity , que é definida como uma propriedade calculada.

No entanto, há alguma redundância. Cada tipo de Bird deve definir uma definição explícita da propriedade canFly, embora tenhamos uma definição do protocolo Flyable. Parece que precisamos de uma maneira de determinar a implementação padrão dos métodos de protocolo. Bem, existem extensões de protocolo para isso.

Estendendo o Protocolo com Comportamento Padrão


As extensões de protocolo permitem definir o comportamento padrão do protocolo. Escreva este código imediatamente após a definição do protocolo Bird:

extension Bird {
  // Flyable birds can fly!
  var canFly: Bool { self is Flyable }
}


Este código define a extensão do protocolo Bird . Esta extensão determina que a propriedade canFly retornará true quando o tipo estiver em conformidade com o protocolo Flyable . Em outras palavras, qualquer pássaro Flyable não precisa mais definir explicitamente o canFly.

Agora remova let canFly = ... das definições de FlappyBird, Penguin e SwiftBird. Compile o código e verifique se está tudo em ordem.

Vamos fazer as transferências


As enumerações no Swift podem estar de acordo com os protocolos. Adicione a seguinte definição de enumeração:

enum UnladenSwallow: Bird, Flyable {
  case african
  case european
  case unknown
  
  var name: String {
    switch self {
    case .african:
      return "African"
    case .european:
      return "European"
    case .unknown:
      return "What do you mean? African or European?"
    }
  }
  
  var airspeedVelocity: Double {
    switch self {
    case .african:
      return 10.0
    case .european:
      return 9.9
    case .unknown:
      fatalError("You are thrown from the bridge of death!")
    }
  }
}


Ao definir as propriedades apropriadas, o UnladenSwallow está em conformidade com dois protocolos - Bird e Flyable. Dessa forma, a definição padrão para o canFly é implementada.

Substituindo o comportamento padrão


Nosso tipo UnladenSwallow , de acordo com o protocolo Bird , recebeu automaticamente uma implementação do canFly . No entanto, precisamos que o UnladenSwallow.unknown retorne false para o canFly.

Adicione o seguinte código abaixo:

extension UnladenSwallow {
  var canFly: Bool {
    self != .unknown
  }
}

Agora, apenas .african e .european retornarão verdadeiro para o canFly. Confira! Adicione o seguinte código na parte inferior do nosso playground:

UnladenSwallow.unknown.canFly         // false
UnladenSwallow.african.canFly         // true
Penguin(name: "King Penguin").canFly  // false

Compile o playground e verifique os valores recebidos com os indicados nos comentários acima.

Assim, redefinimos propriedades e métodos da mesma maneira que o uso de métodos virtuais na programação orientada a objetos.

Protocolo de expansão


Você também pode fazer com que seu próprio protocolo esteja em conformidade com outro protocolo da biblioteca padrão Swift e defina o comportamento padrão. Substitua a declaração do protocolo Bird pelo seguinte código:

protocol Bird: CustomStringConvertible {
  var name: String { get }
  var canFly: Bool { get }
}

extension CustomStringConvertible where Self: Bird {
  var description: String {
    canFly ? "I can fly" : "Guess I'll just sit here :["
  }
}

A conformidade com o protocolo CustomStringConvertible significa que seu tipo deve ter uma propriedade de descrição. Em vez de adicionar essa propriedade no tipo Bird e em todas as suas derivadas, definimos a extensão de protocolo CustomStringConvertible, que será associada apenas ao tipo Bird.

Digite UnladenSwallow.african na parte inferior do playground. Compile e você verá "Eu posso voar".

Protocolos nas bibliotecas padrão Swift


Como você pode ver, os protocolos são uma maneira eficaz de estender e personalizar tipos. Na biblioteca padrão Swift, essa propriedade também é amplamente usada.

Adicione este código ao playground:

let numbers = [10, 20, 30, 40, 50, 60]
let slice = numbers[1...3]
let reversedSlice = slice.reversed()

let answer = reversedSlice.map { $0 * 10 }
print(answer)

Você provavelmente sabe o que esse código produzirá, mas poderá se surpreender com os tipos usados ​​aqui.

Por exemplo, a fatia não é Array, mas ArraySlice. Esse é um "invólucro" especial que fornece uma maneira eficiente de trabalhar com partes da matriz. Consequentemente, reversedSlice é um ReversedCollection <ArraySlice>.

Felizmente, a função map é definida como uma extensão do protocolo Sequence, que corresponde a todos os tipos de coleção. Isso nos permite aplicar a função de mapa para Array e ReversedCollection e não notar a diferença. Em breve você aproveitará esse truque útil.

Em suas marcas


Até o momento, identificamos vários tipos que cumprem o protocolo Bird. Agora vamos adicionar algo completamente diferente:

class Motorcycle {
  init(name: String) {
    self.name = name
    speed = 200.0
  }

  var name: String
  var speed: Double
}

Esse tipo não tem nada a ver com pássaros e vôos. Queremos organizar uma corrida de moto com pinguins. É hora de trazer essa empresa estranha para o começo.

Juntando tudo


Para, de alguma forma, unir pilotos tão diferentes, precisamos de um protocolo comum para as corridas. Podemos fazer tudo isso sem sequer tocar em todos os tipos que criamos antes, com a ajuda de uma coisa maravilhosa chamada modelagem retroativa. Basta adicionar isso ao playground:

// 1
protocol Racer {
  var speed: Double { get }  // speed is the only thing racers care about
}

// 2
extension FlappyBird: Racer {
  var speed: Double {
    airspeedVelocity
  }
}

extension SwiftBird: Racer {
  var speed: Double {
    airspeedVelocity
  }
}

extension Penguin: Racer {
  var speed: Double {
    42  // full waddle speed
  }
}

extension UnladenSwallow: Racer {
  var speed: Double {
    canFly ? airspeedVelocity : 0.0
  }
}

extension Motorcycle: Racer {}

// 3
let racers: [Racer] =
  [UnladenSwallow.african,
   UnladenSwallow.european,
   UnladenSwallow.unknown,
   Penguin(name: "King Penguin"),
   SwiftBird(version: 5.1),
   FlappyBird(name: "Felipe", flappyAmplitude: 3.0, flappyFrequency: 20.0),
   Motorcycle(name: "Giacomo")]

Aqui está o que fazemos aqui: primeiro, defina o protocolo Racer. Isso é tudo o que pode participar de corridas. Em seguida, convertemos todos os nossos tipos criados anteriormente para o protocolo Racer. E, finalmente, criamos uma matriz que contém instâncias de cada um de nosso tipo.

Compile o playground para que tudo esteja em ordem.

Velocidade máxima


Escrevemos uma função para determinar a velocidade máxima dos pilotos. Adicione este código no final do playground:

func topSpeed(of racers: [Racer]) -> Double {
  racers.max(by: { $0.speed < $1.speed })?.speed ?? 0.0
}

topSpeed(of: racers) // 5100

Aqui usamos a função max para encontrar o piloto na velocidade máxima e devolvê-lo. Se a matriz estiver vazia, 0,0 será retornado.

Tornando a função mais geral


Suponha que a matriz Racers seja grande o suficiente, e precisamos encontrar a velocidade máxima não em toda a matriz, mas em alguma parte dela. A solução é alterar o topSpeed ​​(of :) para que ele tome como argumento não especificamente uma matriz, mas tudo o que esteja em conformidade com o protocolo Sequence.

Substitua nossa implementação do topSpeed ​​(of :) da seguinte maneira:

// 1
func topSpeed<RacersType: Sequence>(of racers: RacersType) -> Double
    /*2*/ where RacersType.Iterator.Element == Racer {
  // 3
  racers.max(by: { $0.speed < $1.speed })?.speed ?? 0.0
}

  1. RacersType é o tipo de argumento genérico de nossa função. Pode ser qualquer coisa que esteja em conformidade com o protocolo Sequence.
  2. onde determina que o conteúdo da sequência deve estar em conformidade com o protocolo Racer.
  3. O corpo da função em si permanece inalterado.

Verifique adicionando isso no final do nosso playground:

topSpeed(of: racers[1...3]) // 42

Agora, nossa função funciona com qualquer tipo que atenda ao protocolo Sequence, incluindo ArraySlice.

Tornando a função mais "rápida"


Segredo: você pode fazer ainda melhor. Adicione isso na parte inferior:

extension Sequence where Iterator.Element == Racer {
  func topSpeed() -> Double {
    self.max(by: { $0.speed < $1.speed })?.speed ?? 0.0
  }
}

racers.topSpeed()        // 5100
racers[1...3].topSpeed() // 42

E agora expandimos o próprio protocolo Sequence com topSpeed ​​(). É aplicável apenas quando Sequência contém o tipo Racer.

Comparadores de protocolo


Outro recurso dos protocolos Swift é como você define os operadores de igualdade de objetos ou sua comparação. Escrevemos o seguinte:

protocol Score {
  var value: Int { get }
}

struct RacingScore: Score {
  let value: Int
}

Com o protocolo Score, você pode escrever um código que trata todos os elementos desse tipo de uma maneira. Mas se você obtiver um tipo muito específico, como o RacingScore, não o confundirá com outros derivados do protocolo Score.

Queremos que as pontuações sejam comparadas para ver quem tem a maior pontuação. Antes do Swift 3, os desenvolvedores precisavam escrever funções globais para definir um operador para um protocolo. Agora podemos definir esses métodos estáticos no próprio modelo. Fazemos isso substituindo as definições de Score e RacingScore da seguinte maneira:

protocol Score: Comparable {
  var value: Int { get }
}

struct RacingScore: Score {
  let value: Int
  
  static func <(lhs: RacingScore, rhs: RacingScore) -> Bool {
    lhs.value < rhs.value
  }
}

Temos toda a lógica do RacingScore em um só lugar. O protocolo Comparable requer que você defina uma implementação apenas para a função menor que. Todas as outras funções de comparação serão implementadas automaticamente, com base na implementação da função "menor que" que criamos.

Teste:

RacingScore(value: 150) >= RacingScore(value: 130) // true

Fazendo alterações no objeto


Até agora, cada exemplo demonstrou como adicionar funcionalidade. Mas e se quisermos criar um protocolo que mude algo em um objeto? Isso pode ser feito usando métodos de mutação em nosso protocolo.

Adicione um novo protocolo:

protocol Cheat {
  mutating func boost(_ power: Double)
}

Aqui, definimos um protocolo que nos permite trapacear. Quão? Alteração arbitrária do conteúdo do impulso.

Agora crie uma extensão SwiftBird que esteja em conformidade com o protocolo Cheat:

extension SwiftBird: Cheat {
  mutating func boost(_ power: Double) {
    speedFactor += power
  }
}

Aqui, implementamos a função boost (_ :), aumentando o speedFactor pelo valor transmitido. A palavra-chave mutating faz com que a estrutura entenda que um de seus valores será alterado por essa função.

Confira!
var swiftBird = SwiftBird(version: 5.0)
swiftBird.boost(3.0)
swiftBird.airspeedVelocity // 5015
swiftBird.boost(3.0)
swiftBird.airspeedVelocity // 5030

Conclusão


Aqui você pode baixar o código fonte completo do playground.

Você aprendeu sobre as possibilidades da programação orientada a protocolo criando protocolos simples e aumentando seus recursos com a ajuda de extensões. Usando a implementação padrão, você fornece aos protocolos o "comportamento" apropriado. Quase como nas classes base, mas apenas melhor, pois tudo isso também se aplica a estruturas e enumerações.

Você também viu que a extensão do protocolo se aplica aos protocolos Swift subjacentes.

Aqui você encontrará o guia oficial do protocolo .

Você também pode assistir a uma excelente palestra da WWDC sobre programação orientada a protocolo.

Como em qualquer paradigma de programação, existe o risco de se deixar levar e começar a usar os protocolos à esquerda e à direita. Aqui está uma observação interessante sobre os perigos das decisões no estilo de bala de prata.

Source: https://habr.com/ru/post/undefined/


All Articles