Desta vez, gostaria de falar um pouco sobre outro padrão de design generativo do arsenal do Gang of Four - "Builder" . Verificou-se que, no processo de obtenção da minha experiência (embora não muito extensa), vi com freqüência que o padrão era usado no código Java em geral e nos aplicativos Android em particular. Nos projetos "iOS" , sejam eles escritos em "Swift" ou "Objective-C", o padrão era bastante raro para mim. No entanto, com toda a sua simplicidade, em casos adequados, pode ser bastante conveniente e, como está na moda dizer, poderoso.

O modelo é usado para substituir o processo de inicialização complexo, construindo o objeto desejado passo a passo, com o método de finalização chamado no final. As etapas podem ser opcionais e não devem ter uma sequência de chamadas estrita.

Exemplo de fundação
Nos casos em que o “URL” desejado não é fixo, mas é construído a partir de componentes (por exemplo, o endereço do host e o caminho relativo ao recurso), você provavelmente utilizou o conveniente mecanismo URLComponents
da biblioteca Foundation .
URLComponents
é, na maioria das vezes, apenas uma classe que combina muitas variáveis que armazenam os valores de vários componentes da URL, bem como a propriedade url
, que retorna a URL apropriada para o conjunto atual de componentes. Por exemplo:
var urlComponents = URLComponents() urlComponents.scheme = "https" urlComponents.user = "admin" urlComponents.password = "qwerty" urlComponents.host = "somehost.com" urlComponents.port = 80 urlComponents.path = "/some/path" urlComponents.queryItems = [URLQueryItem(name: "page", value: "0")] _ = urlComponents.url
De fato, o caso de uso acima é a implementação do padrão Builder. Nesse caso, URLComponents
atua como o próprio construtor, atribuindo valores a suas várias propriedades ( scheme
, host
etc.) é a inicialização do objeto futuro em etapas, e chamar a propriedade url
é como um método de finalização.
Nos comentários, ocorreram batalhas acirradas sobre os documentos “RFC” que descrevem o “URL” e o “URI” , portanto, para ser mais preciso, sugiro, por exemplo, supor que estamos falando apenas do “URL” dos recursos remotos e não consideramos tais Esquemas de "URL", como, por exemplo, "arquivo".
Tudo parece estar bem se você usar esse código com pouca frequência e conhecer todas as suas sutilezas. Mas e se você esquecer alguma coisa? Por exemplo, algo importante como um endereço de host? O que você acha que será o resultado da execução do código a seguir?
var urlComponents = URLComponents() urlComponents.scheme = "https" urlComponents.path = "/some/path" _ = urlComponents.url
Trabalhamos com propriedades, não métodos, e nenhum erro será "jogado fora" com certeza. A propriedade url
"finalizing" retorna um valor opcional , então talvez não tenhamos nil
? Não, obtemos um objeto completo do tipo URL
com um valor sem sentido - "https: / some / path". Portanto, me ocorreu praticar praticar meu próprio "construtor" com base na "API" descrita acima.
(Deveria haver um "emoji" "bicicleta", mas "Habr" não o exibe)
Apesar do exposto, considero os URLComponents
“API” boa e conveniente para montar “URLs” de componentes e, pelo contrário, “analisar” os componentes dos conhecidos “URLs”. Portanto, com base nisso, agora escrevemos nosso próprio tipo que coleta a "URL" das partes e possui (suponha) a "API" de que precisamos no momento.
Em primeiro lugar, quero me livrar de uma inicialização diferente, atribuindo novos valores a todas as propriedades necessárias. Em vez disso, implementamos a possibilidade de criar uma instância do construtor e atribuir valores a todas as propriedades usando métodos chamados pela cadeia. A cadeia termina com um método de finalização, cujo resultado da chamada será a instância correspondente da URL
. Talvez você tenha encontrado algo como " StringBuilder
em Java" em sua jornada de vida - vamos nos esforçar para obter uma "API" agora.
Para poder chamar métodos-etapas ao longo da cadeia, cada um deles deve retornar uma instância do construtor atual, dentro da qual a alteração correspondente será armazenada. Por esse motivo, e também para se livrar de várias cópias de objetos e de dançar com métodos mutating
, especialmente sem pensar, declararemos ao construtor uma classe :
final class URLBuilder { }
Declararemos métodos que especificam os parâmetros do futuro "URL", levando em consideração os requisitos acima:
final class URLBuilder { private var scheme = "https" private var user: String? private var password: String? private var host: String? private var port: Int? private var path = "" private var queryItems: [String : String]? func with(scheme: String) -> URLBuilder { self.scheme = scheme return self } func with(user: String) -> URLBuilder { self.user = user return self } func with(password: String) -> URLBuilder { self.password = password return self } func with(host: String) -> URLBuilder { self.host = host return self } func with(port: Int) -> URLBuilder { self.port = port return self } func with(path: String) -> URLBuilder { self.path = path return self } func with(queryItems: [String : String]) -> URLBuilder { self.queryItems = queryItems return self } }
Nós salvamos os parâmetros especificados nas propriedades particulares da classe para uso futuro pelo método de finalização.
Outro tributo à “API” na qual baseamos nossa classe é a propriedade path
, que, diferentemente de todas as propriedades vizinhas, não é opcional e, se não houver um caminho relativo, ele armazena uma string vazia como valor.
Para escrever esse método, de fato, finalizando, você precisa pensar em mais algumas coisas. Primeiro, a “URL” possui algumas partes, sem as quais, como foi indicado no início, deixa de fazer sentido - esse é o scheme
e o host
. Nós “premiamos” o primeiro com o valor padrão, portanto, tendo esquecido, ainda receberemos, provavelmente, o resultado esperado.
Com o segundo, as coisas ficam um pouco mais complicadas: não é possível atribuir algum valor padrão. Nesse caso, temos duas maneiras: na ausência de um valor para essa propriedade, retorne nil
ou gere um erro e deixe o código do cliente decidir por si mesmo o que fazer com ela. A segunda opção é mais complicada, mas permitirá que você indique explicitamente um erro específico do programador. Talvez, por exemplo, possamos seguir esse caminho.
Outro ponto interessante está relacionado às propriedades do user
e da password
: elas só fazem sentido se forem usadas simultaneamente. Mas e se um programador esquecer de atribuir um desses dois valores?
E, provavelmente, a última coisa a considerar é que, como resultado do método de finalização, queremos ter o valor da propriedade url
de URLComponents
, e isso, nesse caso, não é muito útil opcional. Embora, para qualquer combinação de valores definidos de propriedades nil
, não a obtenhamos. (Apenas uma instância URLComponents
de URLComponents
recém-criada não terá valor.) Para superar isso, você pode usar !
- operador "desembrulhar forçado". Mas, em geral, eu não gostaria de incentivar seu uso; portanto, em nosso exemplo, abstraimos brevemente o conhecimento das sutilezas de "Foundation" e consideramos a situação em discussão como um erro de sistema, cuja ocorrência não depende do nosso código.
Então:
extension URLBuilder { func build() throws -> URL { guard let host = host else { throw URLBuilderError.emptyHost } if user != nil { guard password != nil else { throw URLBuilderError.inconsistentCredentials } } if password != nil { guard user != nil else { throw URLBuilderError.inconsistentCredentials } } var urlComponents = URLComponents() urlComponents.scheme = scheme urlComponents.user = user urlComponents.password = password urlComponents.host = host urlComponents.port = port urlComponents.path = path urlComponents.queryItems = queryItems?.map { URLQueryItem(name: $0, value: $1) } guard let url = urlComponents.url else { throw URLBuilderError.systemError
Isso é tudo, talvez! Agora, uma criação explodida da "URL" a partir do exemplo no início pode ser assim:
_ = try URLBuilder() .with(user: "admin") .with(password: "Qwerty") .with(host: "somehost.com") .with(port: 80) .with(path: "/some/path") .with(queryItems: ["page": "0"]) .build()
Obviamente, usando try
fora de um catch
do
- catch
ou sem um operador ?
se ocorrer um erro, o programa falhará. Mas nós fornecemos ao “cliente” a oportunidade de lidar com os erros como entender.
Sim, e outro recurso útil da construção passo a passo usando esse modelo é a capacidade de colocar etapas em diferentes partes do código. Não é o caso mais frequente, mas mesmo assim. Obrigado akryukov pelo lembrete!
Conclusão
O modelo é extremamente fácil de entender e tudo o que é simples é, como você sabe, engenhoso. Ou vice-versa? Bem, não importa. O principal é que eu, sem um tremor na minha alma, posso dizer que ele (o modelo) já aconteceu, me ajudou a resolver problemas de criação de processos de inicialização grandes e complexos. Por exemplo, o processo de preparar uma sessão de comunicação com o servidor na biblioteca, que escrevi para um serviço há quase dois anos. A propósito, o código é "código aberto" e, se desejado, é bem possível se familiarizar com ele. (Embora, é claro, desde então, muita água tenha fluído e outros programadores tenham se aplicado a esse código.)
Meus outros posts sobre padrões de design:
E este é o meu "Twitter" para satisfazer um interesse hipotético em minha atividade profissional público.