Esta vez me gustaría hablar un poco sobre otro patrón de diseño generativo del arsenal Gang of Four: "Builder" . Resultó que en el proceso de obtener mi experiencia (aunque no demasiado extensa), a menudo vi que el patrón se usaba en el código "Java" en general y en las aplicaciones "Android" en particular. En los proyectos "iOS" , tanto si estaban escritos en "Swift" como en "Objective-C" , el patrón era bastante raro para mí. Sin embargo, con toda su simplicidad, en casos adecuados, puede resultar bastante conveniente y, como está de moda decir, poderoso.

La plantilla se usa para reemplazar el complejo proceso de inicialización construyendo el objeto deseado paso a paso, con el método de finalización llamado al final. Los pasos pueden ser opcionales y no deben tener una secuencia de llamada estricta.

Ejemplo de fundación
En los casos en que la "URL" deseada no es fija, sino que se construye a partir de componentes (por ejemplo, la dirección del host y la ruta relativa al recurso), probablemente haya utilizado el conveniente mecanismo URLComponents
de la biblioteca de Foundation .
URLComponents
es, en su mayor parte, solo una clase que combina muchas variables que almacenan los valores de varios componentes URL, así como la propiedad url
, que devuelve la URL apropiada para el conjunto actual de componentes. Por ejemplo:
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 hecho, el caso de uso anterior es la implementación del patrón Builder. En este caso, URLComponents
actúa como el generador en sí mismo, la asignación de valores a sus diversas propiedades ( scheme
, host
, etc.) es la inicialización del objeto futuro en pasos, y llamar a la propiedad url
es como un método de finalización.
En los comentarios, se desarrollaron batallas acaloradas sobre los documentos "RFC" que describen la "URL" y el "URI" , por lo tanto, para ser más precisos, propongo, por ejemplo, asumir que estamos hablando solo de la "URL" de los recursos remotos, y no tener en cuenta tales Esquemas de "URL", como, por ejemplo, "archivo".
Todo parece estar bien si usa este código con poca frecuencia y conoce todas sus sutilezas. ¿Pero y si olvidas algo? Por ejemplo, ¿algo importante como una dirección de host? ¿Cuál crees que será el resultado de ejecutar el siguiente código?
var urlComponents = URLComponents() urlComponents.scheme = "https" urlComponents.path = "/some/path" _ = urlComponents.url
Trabajamos con propiedades, no con métodos, y no se descartarán errores con seguridad. La propiedad de url
"finalizando" devuelve un valor opcional , así que tal vez obtengamos nil
? No, obtenemos un objeto completo de tipo URL
con un valor sin sentido: "https: / some / path". Por lo tanto, se me ocurrió practicar escribir mi propio "generador" basado en la "API" descrita anteriormente.
(Debería haber una "bicicleta" de emoji, pero "Habr" no la muestra)
A pesar de lo anterior, considero que URLComponents
buena y conveniente "API" para ensamblar "URL" a partir de componentes y, por el contrario, "analizar" los componentes de las bien conocidas "URL". Por lo tanto, en base a ello, ahora escribimos nuestro propio tipo que recopila la "URL" de las partes y posee (supongamos) la "API" que necesitamos en este momento.
En primer lugar, quiero deshacerme de la inicialización dispar asignando nuevos valores a todas las propiedades necesarias. En su lugar, implementamos la posibilidad de crear una instancia del generador y asignar valores a todas las propiedades utilizando métodos llamados por la cadena. La cadena termina con un método de finalización, cuyo resultado será la instancia correspondiente de la URL
. Tal vez haya encontrado algo como " StringBuilder
en Java" en su viaje de vida: ahora nos esforzaremos por lograr una "API" de este tipo.
Para poder llamar a métodos-pasos a lo largo de la cadena, cada uno de ellos debe devolver una instancia del generador actual, dentro del cual se almacenará el cambio correspondiente. Por esta razón, y también para deshacerse de la copia múltiple de objetos y de bailar alrededor de métodos de mutating
, especialmente sin pensar, declararemos a nuestro constructor una clase :
final class URLBuilder { }
Declararemos métodos que especifiquen los parámetros de la futura "URL", teniendo en cuenta los requisitos anteriores:
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 } }
Guardamos los parámetros especificados en las propiedades privadas de la clase para su uso futuro mediante el método de finalización.
Otro tributo a la "API" en la que basamos nuestra clase es la propiedad de path
, que, a diferencia de todas las propiedades vecinas, no es opcional, y si no hay una ruta relativa, almacena una cadena vacía como su valor.
Para escribir este, de hecho, el método de finalización, debe pensar en algunas cosas más. En primer lugar, la "URL" tiene algunas partes, sin las cuales, como se indicó al principio, deja de tener sentido: este es el scheme
y el host
. Hemos "premiado" al primero con el valor predeterminado, por lo tanto, habiéndolo olvidado, aún recibiremos, muy probablemente, el resultado esperado.
Con el segundo, las cosas son un poco más complicadas: no se le puede asignar algún valor predeterminado. En este caso, tenemos dos formas: en ausencia de un valor para esta propiedad, devolver nil
o arrojar un error y dejar que el código del cliente decida por sí mismo qué hacer con él. La segunda opción es más complicada, pero le permitirá indicar explícitamente un error específico del programador. Quizás, por ejemplo, iremos por este camino.
Otro punto interesante está relacionado con las propiedades de user
y password
: solo tienen sentido si se usan simultáneamente. Pero, ¿qué pasa si un programador olvida asignar uno de estos dos valores?
Y, probablemente, lo último a considerar es que, como resultado del método de finalización, queremos tener el valor de la propiedad url
de URLComponents
, y esto, en este caso, no es muy útil opcional. Aunque, para cualquier combinación de valores establecidos de propiedades nil
, no la obtendremos. (Solo una instancia URLComponents
de URLComponents
no tendrá un valor). ¡Para superar esto, puede usar !
- operador "desenvolvimiento forzado". Pero, en general, no quisiera alentar su uso, por lo tanto, en nuestro ejemplo, hacemos un breve resumen del conocimiento de las sutilezas de "Fundación" y consideramos la situación en discusión como un error del sistema, cuya ocurrencia no depende de nuestro código.
Entonces
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
¡Eso es todo, tal vez! Ahora una creación explotada de la "URL" del ejemplo al principio podría verse así:
_ = try URLBuilder() .with(user: "admin") .with(password: "Qwerty") .with(host: "somehost.com") .with(port: 80) .with(path: "/some/path") .with(queryItems: ["page": "0"]) .build()
Por supuesto, ¿usar try
fuera de un catch
do
- catch
o sin un operador ?
Si se produce un error, hará que el programa se bloquee. Pero le brindamos al "cliente" la oportunidad de manejar los errores como lo considere conveniente.
Sí, y otra característica útil de la construcción paso a paso usando esta plantilla es la capacidad de colocar pasos en diferentes partes del código. No es el caso más frecuente, pero no obstante. Gracias akryukov por el recordatorio!
Conclusión
La plantilla es extremadamente fácil de entender, y todo lo simple es, como saben, ingenioso. O viceversa? Bueno, no importa. Lo principal es que yo, sin una sacudida de mi alma, puedo decir que (la plantilla), ya sucedió, me ayudó a resolver problemas de creación de procesos de inicialización grandes y complejos. Por ejemplo, el proceso de preparar una sesión de comunicación con el servidor en la biblioteca, que escribí para un servicio hace casi dos años. Por cierto, el código es de "código abierto" y, si lo desea, es muy posible familiarizarse con él. (Aunque, por supuesto, desde entonces ha fluido mucha agua y otros programadores han aplicado este código).
Mis otras publicaciones sobre patrones de diseño:
Y este es mi "Twitter" para satisfacer un interés hipotético en mi actividad público-profesional.