Swift / SwiftUI

O que torna o código 'Swiftiano'?

O texto explora o conceito de código 'Swifty', destacando que, apesar da sintaxe do Swift ser similar a outras linguagens C-influenciadas, seguir as convenções atuais é crucial para escrever código 'Swifty'. O autor ressalta mudanças nas convenções ao longo dos anos e enfatiza que o que torna um código 'Swifty' está ligado aos objetivos fundamentais do Swift: segurança, velocidade e expressividade. Para exemplificar, utiliza uma estrutura de dados para notas como caso prático, mostrando como a introdução de um enum personalizado pode melhorar a clareza e a segurança do código.

Compartilhar
What makes code “Swifty”? | Swift by Sundell

Ao contrário do que muitos pensam, os idiomas de programação são formalmente definidos por sua sintaxe, mas a maneira como eles são utilizados na prática é determinada em grande parte pelas convenções atuais. Afinal, sintaticamente falando, a maioria dos idiomas 'influenciados pelo C' parecem incrivelmente semelhantes — ao ponto de você poder escrever Swift de maneira que quase faça parecer JavaScript, ou C#, ou até mesmo o próprio C.

Dentro da comunidade Swift, a frase 'código Swiftiano' é frequentemente usada para descrever código que segue as convenções mais populares no momento. No entanto, enquanto a sintaxe básica do Swift não mudou muito desde sua introdução original, suas convenções evoluíram dramaticamente ao longo do tempo.

Por exemplo, muitos desenvolvedores de Swift lembram da transição do Swift 2 para o Swift 3 como uma grande mudança em termos de sintaxe, mas a maioria dessas mudanças não foram realmente mudanças na sintaxe — eram alterações na API da biblioteca padrão baseadas em um novo conjunto de convenções de nomenclatura. Adicione o Swift 4 com sua introdução de caminhos-chave e Codable, o Swift 5.1 com seus construtores de função, wrappers de propriedade e tipos de retorno opacos, e muitas outras APIs e recursos introduzidos ao longo dos anos — e está ficando claro que o que torna um código 'Swiftiano' é uma espécie de alvo em movimento.

Nesta semana, vamos dar uma olhada mais de perto no conjunto básico de convenções do Swift para tentar responder à pergunta sobre o que realmente torna um código 'Swiftiano'?

Genius Scan SDK

O Swift by Sundell é trazido a você pelo Genius Scan SDK — Adicione um poderoso escaneador de documentos a qualquer aplicativo móvel e transforme os scans em PDFs de alta qualidade com uma linha de código. Tente hoje.

Dito isso, uma resposta simples à pergunta acima pode ser "código bem alinhado com o conjunto básico de objetivos do Swift". Afinal, enquanto as várias APIs, convenções e recursos da linguagem do Swift tendem a mudar ao longo do tempo, seus objetivos fundamentais permanecem basicamente os mesmos — então se pudermos escrever nosso próprio código de maneira que corresponda a esses objetivos, teremos uma chance muito melhor de fazer nosso código parecer natural e claro em qualquer contexto Swift.

Então, quais são exatamente esses objetivos? A página sobre o Swift no site oficial lista três palavras-chave: Safe, em termos de minimizar erros do desenvolvedor, Rápido quando se trata da velocidade de execução e Expressivo, na medida em que o Swift visa ser tão claro e fácil de entender quanto possível.

Vamos dar uma olhada em algumas coisas diferentes que podem ser boas para manter em mente para fazer nosso próprio código seguir esses princípios.

Vamos começar com a primeira palavra-chave — segurança. O fato de o Swift colocar muito ênfase na segurança do tipo é difícil de ignorar — com suas verificações estáticas de tipo, um sistema poderoso de gêneros e a necessidade de fazer coisas como esborrachamento de tipos para que o compilador possa verificar a estrutura do nosso código enquanto ele está sendo compilado.

No entanto, é bastante comum encontrar situações em que não fica claro que a segurança do tipo do nosso código poderia ser melhorada — o que também pode fazer o código parecer mais "Swiftiano", e por sua vez, ficar mais fácil de trabalhar.

struct NoteCollection {
var notesByGroup: [String : [Note]]
...
}

No primeiro olhar, o código acima pode parecer perfeitamente adequado. No entanto, um detalhe que não é nada óbvio ao examinar a declaração acima é como estamos lidando com valores sem agrupamento, bem como com um grupo especial contendo todas as notas recentemente abertas do usuário — o que atualmente é feito passando uma string vazia ou "recent" quando subscritamos nosso dicionário:

let groupedNotes = collection.notesByGroup["MyGroup"]
let ungroupedNotes = collection.notesByGroup[""]
let recentNotes = collection.notesByGroup["recent"]

Awhile the above design may have a perfectly valid justification (for example, the structure we’ve used might be how our notes are organized when loading them over the network), it does lead to some of our call sites becoming quite cryptic — which in turn increases the chances of developer mistakes. É fácil esquecer que uma string vazia significa que todas as notas sem agrupamento devem ser recuperadas, e o que acontece se o usuário nomear um de seus grupos personalizados "recent"?

Vamos ver se podemos tornar o código acima mais seguro do tipo e, ao fazê-lo, fazer com que ele também pareça mais "Swifty". Como temos três casos de uso distintos para nosso dicionário notesByGroup, vamos substituir suas chaves baseadas em String por um enum personalizado que modela esses três variantes como casos distintos, assim:

enum Group: Hashable {
case none
case recent
case named(String)
}

struct NoteCollection {
var notesByGroup: [Group : [Note]]
...
}

O acima pode parecer uma mudança sutil, mas torna nossos sites de chamada muito mais claros, já que agora estamos aproveitando o sistema de tipos para distinguir entre nossas três tipos separados de grupos — tudo sem fazer nossa API mais complexa:

let groupedNotes = collection.notesByGroup[.named("MyGroup")]
let ungroupedNotes = collection.notesByGroup[.none]
let recentNotes = collection.notesByGroup[.recent]

Isso talvez seja a essência do que faz o código "Swifty" em termos de segurança de tipo. Enquanto há muitas maneiras de tornar uma API realmente complicada para torná-la mais segura do tipo, a truque é usar as características da linguagem Swift para encontrar um jeito de adicionar essa segurança do tipo sem fazer nosso código mais difícil de entender ou usar.

Enquanto a segurança do tipo é comumente usada para prevenir que um valor do tipo B seja incorretamente passado para uma API que aceita A, o forte tipagem também frequentemente fornece uma maneira de melhorar a semântica e lógica do nosso código também. No exemplo seguinte, nosso código é tecnicamente seguro do tipo — já que estamos usando a característica Swift de gêneros para implementar um LoadingOperation que pode carregar qualquer Resource que atenda ao protocolo Loadable:

class LoadingOperation<Resource: Loadable> {
private let resource: Resource

init(resource: Resource) {
self.resource = resource

if let preloadable = resource as? Preloadable {
preloadable.preload()
}
}

...
}

No entanto, o fato de termos feito um cast condicional em nosso resource para verificar se também segue a conformidade com Preloadable (e se sim, pré-carregamos esse recurso) é algo que pode ser considerado um pouco estranho. Além de tornar difícil entender como fazer um recurso pré-carregar (já que o sistema de tipos não nos dá nenhuma dica para conformarmos com Preloadable para fazermos isso acontecer), é também bastante contraintuitivo ter o pré-carregamento ser um efeito colateral da inicialização de uma operação.

Vamos então tornar a pré-carregamento uma API explícita que só está disponível quando o Resource de uma operação segue conformidade com Preloadable — assim:

extension LoadingOperation where Resource: Preloadable {
func preload() {
resource.preload()
}
}

A alteração acima torna muito mais clara as condições para que um recurso seja pré-carregado, e podemos agora remover nosso cast de tipo efeito colateral do inicializador — grande vitória!

O importante a notar é que escrever código “Swifty” do ponto de vista da segurança não se trata necessariamente sobre usar genéricos o máximo possível. Ao contrário, é sobre utilizar as várias facetas e recursos do sistema de tipos seletivamente, para tornar nosso código mais fácil de entender e usar (e mais difícil de ser mal utilizado).

A segunda das metas principais do Swift, ser rápido, é algo que é um pouco mais complicado de raciocinar em termos gerais. Afinal, uma parte significativa da escrita de código de alta performance vem a partir de medir, afinar e medir novamente. No entanto, uma maneira de tornarmos nosso código mais alinhado com o Swift em termos de desempenho é fazer uso integral do que a biblioteca padrão tem a oferecer — especialmente quando trabalhando com coleções, como strings.

Como vimos em “String parsing in Swift” e “Slicing Swift collections”, a biblioteca padrão do Swift é altamente otimizada para performance, permitindo-nos realizar muitas operações comuns de coleções de maneira altamente eficiente — dado que utilizamos as APIs corretas, é claro.

Por exemplo, uma forma comum de remover um determinado conjunto de caracteres de uma string é usar a antiga API replacingOccurences(of:with:) herdada do Swift String de seu primo Objective-C, NSString. Aqui estamos utilizando uma série de chamadas para essa API para sanitizar uma string removendo um conjunto de caracteres especiais:

let sanitizedString = string
    .replacingOccurrences(of: "@", with: "")
    .replacingOccurrences(of: "#", with: "")
    .replacingOccurrences(of: "<", with: "")
    .replacingOccurrences(of: ">", with: "")

O problema com a implementação acima é que ela causará 4 iterações separadas pela nossa string — o que pode não ser um problema ao trabalhar com strings mais curtas, ou quando fazemos isso dentro de uma rota de código que não é muito utilizada, mas pode se tornar um gargalo em situações onde precisamos do máximo desempenho.

Agradecidamente, o Swift frequentemente não nos obriga a escolher entre código performático e elegante — tudo o que temos que fazer é mudar para uma API mais apropriada, uma que faz apenas um passo pela nossa string para remover cada caractere contido em um Set, como este:

let caracteresParaRemover: Set<Character> = ["@", "#", "<", ">">]
string.removeAll(where: caracteresParaRemover.contains)

Portanto, para tornar nosso código mais "Swifty" do ponto de vista de desempenho, às vezes tudo o que precisamos fazer é explorar o que a biblioteca padrão tem a oferecer ao nos depararmos com uma tarefa específica — e especialmente quando se trata de coleções, há altas chances de existir uma API elegante e simples disponível que também nos dá ótimas características de desempenho.

Finalmente, vamos dar uma olhada na terceira e última palavra-chave: expressiva. Embora seja fácil pensar em expressividade como algo puramente cosmético e que envolve discutir nomes de métodos até que todos leiam como frases perfeitamente gramaticais em inglês, no final das contas é tudo sobre fazer nosso código transmitir claramente seu significado.

Vamos dizer que escrevemos uma função chamada atualmente getContent, que carrega os dados para um modelo de Content embutido e, em seguida, o decodifica:

func getContent(name: String) -> Content? {
    guard let url = Bundle.main.url(
        forResource: name,
        withExtension: "json"
    ) else {
        return nil
    }

    guard let data = try? Data(contentsOf: url) else {
        return nil
    }

    return try? JSONDecoder().decode(Content.self, from: data)
}

Novamente, à primeira vista a função acima pode parecer perfeitamente adequada. Não há erros óbvios e ela faz o trabalho. No entanto, em termos de ser expressiva, poderia definitivamente ser melhorada.

Primeiro, seu nome atual — "get content" — não nos diz realmente como o conteúdo será recuperado. Será simplesmente criado como uma nova instância, será carregado pela rede ou algo diferente? Além disso, o fato de que ele apenas retorna nil em caso de erro pode dificultar a depuração se algo começar a falhar — já que não teremos nenhuma indicação do que realmente aconteceu.

Vamos tentar melhorar isso, renomeando nossa função para loadBundledContent (para deixar claro que estamos carregando o conteúdo do nosso bundle de aplicativo). Também daremos a ela um rótulo de parâmetro externo para torná-la mais legível e finalmente, faremos com que ela relate qualquer erro encontrado lançando-o — assim:

func loadBundledContent(named name: String) throws -> Content {
guard let url = Bundle.main.url(
forResource: name,
withExtension: "json"
) else {
throw Content.Error.missing
}

guard let data = try? Data(contentsOf: url) else {
throw Content.Error.missing
}

do {
return try JSONDecoder().decode(Content.self, from: data)
} catch {
throw Content.Error.decodingFailed(error)
}
}

Para saber mais sobre a maneira acima de projetar APIs lançáveis, confira o primeiro artigo deste site — “Providing a unified Swift error API”.

Aqui está como a chamada do site fica antes e depois da nossa mudança:


let content = getContent(name: "Onboarding")

let content = try loadBundledContent(named: "Onboarding")

Embora seja importante não nos prender demais no que nomeamos nossas funções e tipos (afinal, é muitas vezes uma questão de gosto e preferência), se pudermos encontrar maneiras de transmitir mais claramente o que cada uma das nossas APIs faz, isso é um grande ganho — pois não apenas torna mais fácil para novos desenvolvedores familiarizarem-se com nossa base de código, mas também pode frequentemente tornar nosso código mais agradável de trabalhar no longo prazo.

No meu ponto de vista, escrever “Swifty” código não é sobre usar tantas características da linguagem quanto possível ou fazer nosso código desnecessariamente complexo ao empregar as funcionalidades avançadas do Swift para resolver problemas simples — é sobre alinhar a maneira como projetamos e expressamos nosso código e suas várias APIs com o conjunto de princípios fundamentais do Swift.

Alinhando nosso código tanto para garantir a correção quanto para tornar sua funcionalidade mais clara através do sistema de tipos do Swift, utilizando totalmente a biblioteca padrão e transmitindo a intenção do nosso código por meio de nomes expressivos e design de API — acabamos com código que melhor se alinha ao próprio Swift, não é isso o que escrever “Swifty” código significa?

Tênis perguntas, comentários ou feedback? Eu adoraria ouvir suas opiniões! Encontre-me no Twitter ou envie-me um e-mail.

Obrigado por ler! 🚀

Fonte original

Conteúdo traduzido e adaptado pela redação do Notícias Mobile. Confira também a matéria na fonte original.

Leia a matéria completa