Motif architectural "Builder" dans l'univers de "Swift" et "iOS" / "macOS"

Cette fois, je voudrais parler un peu d'un autre modèle de conception générative de l'arsenal Gang of Four - «Builder» . Il s'est avéré qu'au cours de mon expérience (bien que pas trop étendue), j'ai souvent vu que le modèle était utilisé dans le code Java en général et dans les applications Android en particulier. Dans les projets "iOS" , qu'ils soient écrits en "Swift" ou "Objective-C" , le pattern était assez rare pour moi. Néanmoins, avec toute sa simplicité, dans des cas appropriés, il peut s'avérer assez pratique et, comme c'est à la mode pour le dire, puissant.


image


Le modèle est utilisé pour remplacer le processus d'initialisation complexe en construisant l'objet souhaité étape par étape, avec la méthode de finalisation appelée à la fin. Les étapes peuvent être facultatives et ne doivent pas avoir une séquence d'appel stricte.


image


Exemple de fondation


Dans les cas où l ' «URL» souhaitée n'est pas fixe, mais est construite à partir de composants (par exemple, l'adresse d'hôte et le chemin relatif vers la ressource), vous avez probablement utilisé le mécanisme URLComponents pratique de la bibliothèque Foundation .


URLComponents est, pour la plupart, juste une classe qui combine de nombreuses variables qui stockent les valeurs de divers composants URL, ainsi que la propriété url , qui renvoie l'URL appropriée pour l'ensemble actuel de composants. Par exemple:


 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 // https://admin:qwerty@somehost.com:80/some/path?page=0 

En fait, le cas d'utilisation ci-dessus est l'implémentation du modèle Builder. Dans ce cas, URLComponents agit comme le constructeur lui-même, attribuer des valeurs à ses différentes propriétés ( scheme , host , etc.) est l'initialisation du futur objet par étapes, et appeler la propriété url est comme une méthode de finalisation.


Dans les commentaires, des batailles houleuses se sont déroulées au sujet des documents «RFC» décrivant «l'URL» et «l'URI» , donc, pour être plus précis, je propose par exemple de supposer que nous ne parlons que de «l'URL» des ressources distantes, et ne tenons pas compte de tels Les schémas "URL", comme, par exemple, "fichier".


Tout semble bien se passer si vous utilisez rarement ce code et connaissez toutes ses subtilités. Et si vous oubliez quelque chose? Par exemple, quelque chose d'important comme une adresse d'hôte? Selon vous, quel sera le résultat de l'exécution du code suivant?


 var urlComponents = URLComponents() urlComponents.scheme = "https" urlComponents.path = "/some/path" _ = urlComponents.url 

Nous travaillons avec des propriétés, pas avec des méthodes, et aucune erreur ne sera "rejetée" à coup sûr. La propriété url "finalizing" renvoie une valeur facultative , alors peut-être que nous obtenons nil ? Non, nous obtenons un objet à part entière de type URL avec une valeur vide de sens - "https: / some / path". Par conséquent, il m'est venu à l'esprit de m'exercer à écrire mon propre «constructeur» basé sur l ' «API» décrite ci-dessus.


(Il aurait dû y avoir un "emoji" "vélo", mais "Habr" ne l'affiche pas)


Malgré ce qui précède, je considère URLComponents comme URLComponents «API» bonne et pratique pour assembler des «URL» à partir de composants et, au contraire, «analyser» les composants des «URL» bien connues. Par conséquent, sur la base de cela, nous écrivons maintenant notre propre type qui recueille l '"URL" des parties et possède (supposons) l' "API" dont nous avons actuellement besoin.


Tout d'abord, je veux me débarrasser de l'initialisation disparate en attribuant de nouvelles valeurs à toutes les propriétés nécessaires. Au lieu de cela, nous implémentons la possibilité de créer une instance du générateur et d'attribuer des valeurs à toutes les propriétés à l'aide de méthodes appelées par la chaîne. La chaîne se termine par une méthode de finalisation, dont le résultat de l'appel sera l'instance correspondante de l' URL . Peut-être avez-vous rencontré quelque chose comme « StringBuilder en Java» au cours de votre parcours de vie - nous allons nous efforcer pour une telle «API» maintenant.


Pour pouvoir appeler des méthodes-étapes le long de la chaîne, chacune d'elles doit renvoyer une instance du générateur courant, à l'intérieur de laquelle la modification correspondante sera stockée. Pour cette raison, et aussi pour se débarrasser de la copie multiple d'objets et de la danse autour des méthodes de mutating , surtout sans réfléchir, nous allons déclarer notre constructeur une classe :


 final class URLBuilder { } 

Nous déclarerons des méthodes qui spécifient les paramètres de la future "URL", en tenant compte des exigences ci-dessus:


 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 } } 

Nous enregistrons les paramètres spécifiés dans les propriétés privées de la classe pour une utilisation future par la méthode de finalisation.


Un autre hommage à l '«API» sur laquelle nous basons notre classe est la propriété path , qui, contrairement à toutes les propriétés voisines, n'est pas facultative, et s'il n'y a pas de chemin relatif, elle stocke une chaîne vide comme valeur.


Pour écrire cela, en fait, la méthode de finalisation, vous devez penser à quelques autres choses. Premièrement, l'URL comporte certaines parties sans lesquelles, comme cela a été indiqué au début, elle n'a plus de sens - c'est le scheme et l' host . Nous avons «attribué» le premier avec la valeur par défaut, donc, l'ayant oublié, nous recevrons toujours, très probablement, le résultat attendu.


Avec le second, les choses sont un peu plus compliquées: on ne peut pas lui attribuer de valeur par défaut. Dans ce cas, nous avons deux façons: en l'absence de valeur pour cette propriété, renvoyer nil ou renvoyer une erreur et laisser le code client décider lui-même quoi faire avec. La deuxième option est plus compliquée, mais elle vous permettra d'indiquer explicitement une erreur de programmation spécifique. Peut-être, par exemple, nous emprunterons cette voie.


Un autre point intéressant concerne les propriétés de l' user et du password : elles n'ont de sens que si elles sont utilisées simultanément. Mais que se passe-t-il si un programmeur oublie d'affecter l'une de ces deux valeurs?


Et, probablement, la dernière chose à considérer est qu'à la suite de la méthode de finalisation, nous voulons avoir la valeur de la propriété url de URLComponents , et cela, dans ce cas, n'est pas très utile en option. Bien que, pour toute combinaison de valeurs définies de propriétés nil , nous ne l'obtiendrons pas. (Seule une instance vide, juste créée, d' URLComponents n'aura pas de valeur.) Pour surmonter cela, vous pouvez utiliser ! - opérateur "déballage forcé". Mais en général, je ne voudrais pas encourager son utilisation, par conséquent, dans notre exemple, nous résumons brièvement la connaissance des subtilités de «Foundation» et considérons la situation en discussion comme une erreur système, dont l'occurrence ne dépend pas de notre code.


Donc:


 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 // Impossible? } return url } enum URLBuilderError: Error { case emptyHost case inconsistentCredentials case systemError } } 

C'est tout, peut-être! Maintenant, une création éclatée de l '«URL» de l'exemple au début pourrait ressembler à ceci:


 _ = try URLBuilder() .with(user: "admin") .with(password: "Qwerty") .with(host: "somehost.com") .with(port: 80) .with(path: "/some/path") .with(queryItems: ["page": "0"]) .build() // https://admin:Qwerty@somehost.com:80/some/path?page=0 

Bien sûr, utiliser try dehors d'un catch do - catch ou sans opérateur ? si une erreur se produit, le programme se bloquera. Mais nous avons donné au «client» l'occasion de traiter les erreurs comme bon lui semble.


Oui, et une autre fonctionnalité utile de la construction étape par étape à l'aide de ce modèle est la possibilité de placer des étapes dans différentes parties du code. Pas le cas le plus fréquent, mais néanmoins. Merci akryukov pour le rappel!


Conclusion


Le modèle est extrêmement facile à comprendre et tout ce qui est simple est, comme vous le savez, ingénieux. Ou vice versa? Eh bien, tant pis. L'essentiel est que moi, sans un mouvement de l'âme, je peux dire que cela (le modèle), déjà arrivé, m'a aidé à résoudre les problèmes de création de processus d'initialisation grands et complexes. Par exemple, le processus de préparation d'une session de communication avec le serveur de la bibliothèque, que j'ai écrit pour un service il y a presque deux ans. Soit dit en passant, le code est «open source» et, si vous le souhaitez, il est tout à fait possible de vous familiariser avec lui. (Bien que, depuis lors, beaucoup d'eau ait coulé, et d'autres programmeurs se sont appliqués à ce code.)


Mes autres articles sur les modèles de conception:



Et c'est mon «Twitter» pour satisfaire un intérêt hypothétique dans mon activité public-professionnel.

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


All Articles