Méthode d'usine et usine abstraite dans l'univers Swift et iOS

Le mot «usine» est de loin l'un des plus fréquemment utilisé par les programmeurs lorsqu'ils discutent de leurs programmes (ou d'autres). Mais le sens qui y est intégré peut être très différent: il peut s'agir d'une classe qui génère des objets (polymorphes ou non); et une méthode qui crée des instances de tout type (statiques ou non); cela arrive, et même n'importe quelle méthode de génération (y compris les constructeurs ).

Bien sûr, tout ce qui génère des instances de quoi que ce soit ne peut pas être appelé le mot usine. De plus, sous ce mot, deux modèles génératifs différents de l'arsenal Gang of Four peuvent être cachés - la "méthode d'usine" et l ' "usine abstraite" , dont je voudrais approfondir un peu les détails, en accordant une attention particulière à leur compréhension et à leur mise en œuvre classiques.

Et j'ai été inspiré pour écrire cet essai de Joshua Kerivsky (responsable de la «Logique industrielle» ), ou plutôt, son livre «Refactoring to Patterns» , qui a été publié au début du siècle dans le cadre d'une série de livres fondée par Martin Fowler (un célèbre auteur du classique de la programmation moderne - le livre « Refactoring " ). Si quelqu'un n'a pas lu ou même entendu parler du premier (et j'en connais beaucoup), assurez-vous de l'ajouter à votre liste de lecture. Il s'agit d'une suite digne à la fois de Refactoring et d'un livre encore plus classique, Objective Design Techniques. Design Patterns . "

Le livre, entre autres, contient des dizaines de recettes pour se débarrasser des diverses "odeurs" dans le code en utilisant des modèles de conception . Y compris trois (au moins) «recettes» sur le sujet en discussion.

Usine abstraite


Kerivsky dans son livre donne deux cas où l'utilisation de ce modèle sera utile.

Le premier est l' encapsulation des connaissances sur des classes spécifiques connectées par une interface commune. Dans ce cas, seul le type qui est l'usine aura cette connaissance. L'API publique de l'usine consistera en un ensemble de méthodes (statiques ou non) qui renvoient des instances d'un type d'interface commun et ont des noms «parlants» (afin de comprendre quelle méthode doit être appelée dans un but particulier).

Le deuxième exemple est très similaire au premier (et, en général, tous les scénarios d'utilisation du modèle sont plus ou moins similaires les uns aux autres). C'est le cas lorsque des instances d'un ou plusieurs types du même groupe sont créées à différents endroits du programme. Dans ce cas, l'usine encapsule à nouveau les connaissances sur le code qui crée les instances, mais avec une motivation légèrement différente. Par exemple, cela est particulièrement vrai si le processus de création d'instances de ces types est complexe et ne se limite pas à appeler le constructeur.

Pour être plus proche du sujet du développement sous "iOS" , il est pratique de pratiquer les sous-classes de UIViewController . En effet, c'est certainement l'un des types les plus courants dans le développement "iOS", il est presque toujours "hérité" avant utilisation, et une sous-classe particulière n'est souvent même pas importante pour le code client.
Je vais essayer de garder les exemples de code aussi proches que possible de l'implémentation classique du livre Gang of Four, mais dans la vraie vie, le code est souvent simplifié d'une manière ou d'une autre. Et seule une compréhension suffisante du modèle ouvre la porte à son utilisation plus libre.

Exemple détaillé


Supposons que nous échangeons des véhicules dans une application et que la cartographie dépend du type de véhicule spécifique: nous utiliserons différentes sous-classes de l' UIViewController pour différents véhicules. De plus, tous les véhicules diffèrent en état (neufs et d'occasion):

 enum VehicleCondition{ case new case used } final class BicycleViewController: UIViewController { private let condition: VehicleCondition init(condition: VehicleCondition) { self.condition = condition super.init(nibName: nil, bundle: nil) } required init?(coder aDecoder: NSCoder) { fatalError("BicycleViewController: init(coder:) has not been implemented.") } } final class ScooterViewController: UIViewController { private let condition: VehicleCondition init(condition: VehicleCondition) { self.condition = condition super.init(nibName: nil, bundle: nil) } required init?(coder aDecoder: NSCoder) { fatalError("ScooterViewController: init(coder:) has not been implemented.") } } 

Ainsi, nous avons une famille d'objets du même groupe, dont des exemples de types sont créés aux mêmes endroits en fonction de certaines conditions (par exemple, un utilisateur a cliqué sur un produit dans la liste, et selon qu'il s'agit d'un scooter ou d'un vélo, nous créer le contrôleur approprié). Les constructeurs de contrôleurs ont certains paramètres qui doivent également être définis à chaque fois. Ces deux arguments sont-ils en faveur de la création d'une "usine" qui seule aura connaissance de la logique de création du bon contrôleur?

Bien sûr, l'exemple est assez simple, et dans un projet réel dans un cas similaire, l'introduction d'une «usine» sera une «ingénierie» explicite. Néanmoins, si nous imaginons que nous n'avons pas deux types de véhicules et que les concepteurs ont plus d'un paramètre, les avantages de «l'usine» deviendront plus évidents.

Déclarons donc une interface qui jouera le rôle d'une "fabrique abstraite":

 protocol VehicleViewControllerFactory { func makeBicycleViewController() -> UIViewController func makeScooterViewController() -> UIViewController } 

(Une «ligne directrice» assez courte pour la conception d'une «API» dans Swift recommande d'appeler les méthodes d'usine commençant par le mot make.)

(Un exemple dans le livre des gangs de quatre est donné en «C ++» et est basé sur l' héritage et les fonctions «virtuelles» . En utilisant «Swift», bien sûr, le paradigme de programmation orienté protocole est plus proche de nous.)

L'interface d'usine abstraite ne contient que deux méthodes: créer des contrôleurs pour vendre des vélos et des scooters. Les méthodes renvoient des instances non pas de sous-classes spécifiques, mais d'une classe de base commune. Ainsi, l'étendue des connaissances sur des types spécifiques est limitée au domaine dans lequel elles sont vraiment nécessaires.

En tant que «usines concrètes», nous utiliserons deux implémentations de l'interface d'usine abstraite:

 struct NewVehicleViewControllerFactory: VehicleViewControllerFactory { func makeBicycleViewController() -> UIViewController { return BicycleViewController(condition: .new) } func makeScooterViewController() -> UIViewController { return ScooterViewController(condition: .new) } } struct UsedVehicleViewControllerFactory: VehicleViewControllerFactory { func makeBicycleViewController() -> UIViewController { return BicycleViewController(condition: .used) } func makeScooterViewController() -> UIViewController { return ScooterViewController(condition: .used) } } 

Dans ce cas, comme le montre le code, des usines spécifiques sont responsables des véhicules de différentes conditions (neufs et d'occasion).

La création du bon contrôleur ressemblera maintenant à ceci:

 let factory: VehicleViewControllerFactory = NewVehicleViewControllerFactory() let vc = factory.makeBicycleViewController() 

Classes d'encapsulation en usine


Passons maintenant brièvement en revue les cas d'utilisation que Kerivsky propose dans son livre.

Le premier cas est lié à l' encapsulation de classes spécifiques . Par exemple, prenez les mêmes contrôleurs pour afficher les données sur les véhicules:

 final class BicycleViewController: UIViewController { } final class ScooterViewController: UIViewController { } 

Supposons que nous ayons affaire à un module séparé, par exemple, une bibliothèque de plug-ins. Dans ce cas, les classes déclarées ci-dessus restent (par défaut) internal , et la fabrique internal comme l '«API» publique de la bibliothèque, qui dans ses méthodes renvoie les classes de base des contrôleurs, laissant ainsi la connaissance de sous-classes spécifiques à l'intérieur de la bibliothèque:

 public struct VehicleViewControllerFactory { func makeBicycleViewController() -> UIViewController { return BicycleViewController() } func makeScooterViewController() -> UIViewController { return ScooterViewController() } } 

Déplacement des connaissances sur la création d'un objet à l'intérieur d'une usine


Le deuxième «cas» décrit l' initialisation complexe de l'objet , et Kerivsky, comme l'un des moyens de simplifier le code et de protéger les principes d'encapsulation, suggère de restreindre la diffusion des connaissances sur le processus d'initialisation en dehors de l'usine.

Supposons que nous voulions vendre des voitures en même temps. Et c'est sans doute une technique plus complexe, avec un plus grand nombre de caractéristiques. Par exemple, nous nous limitons au type de carburant utilisé, au type de transmission et à la taille de la jante:

 enum Condition { case new case used } enum EngineType { case diesel case gas } struct Engine { let type: EngineType } enum TransmissionType { case automatic case manual } final class CarViewController: UIViewController { private let condition: Condition private let engine: Engine private let transmission: TransmissionType private let wheelDiameter: Int init(engine: Engine, transmission: TransmissionType, wheelDiameter: Int = 16, condition: Condition = .new) { self.engine = engine self.transmission = transmission self.wheelDiameter = wheelDiameter self.condition = condition super.init(nibName: nil, bundle: nil) } required init?(coder aDecoder: NSCoder) { fatalError("CarViewController: init(coder:) has not been implemented.") } } 

Un exemple d'initialisation du contrôleur correspondant:

 let engineType = EngineType.diesel let engine = Engine(type: engineType) let transmission = TransmissionType.automatic let wheelDiameter = 18 let vc = CarViewController(engine: engine, transmission: transmission, wheelDiameter: wheelDiameter) 

Nous pouvons mettre la responsabilité de toutes ces "petites choses" sur les "épaules" d'une usine spécialisée:

 struct UsedCarViewControllerFactory { let engineType: EngineType let transmissionType: TransmissionType let wheelDiameter: Int func makeCarViewController() -> UIViewController { let engine = Engine(type: engineType) return CarViewController(engine: engine, transmission: transmissionType, wheelDiameter: wheelDiameter, condition: .used) } } 

Et créez le contrôleur de cette manière:

 let factory = UsedCarViewControllerFactory(engineType: .gas, transmissionType: .manual, wheelDiameter: 17) let vc = factory.makeCarViewController() 

Méthode d'usine


Le deuxième modèle «racine unique» encapsule également les connaissances sur des types générés spécifiques, mais pas en cachant ces connaissances au sein d'une classe spécialisée, mais par polymorphisme. Kerivsky dans son livre donne des exemples en Java et suggère d'utiliser des classes abstraites , mais les habitants de l'univers Swift ne connaissent pas ce concept. Nous avons notre propre atmosphère ici ... et nos protocoles.
Le livre "Gangs of Four" rapporte que le modèle est également connu comme le "concepteur virtuel", et ce n'est pas en vain. En "C ++", virtuel est une fonction redéfinie dans les classes dérivées. Le langage ne donne pas au concepteur la possibilité de se déclarer virtuel, et il est possible que ce soit une tentative d'imiter le comportement souhaité qui ait conduit à l'invention de ce motif.

Création d'objets polymorphes


Comme exemple classique de l'utilité du modèle, nous considérons le cas où dans la hiérarchie différents types ont l'implémentation identique d'une méthode à l'exception de l'objet qui est créé et utilisé dans cette méthode . En guise de solution, il est proposé de créer cet objet dans une méthode distincte et de l'implémenter séparément, et d'élever la méthode générale plus haut dans la hiérarchie. Ainsi, différents types utiliseront l'implémentation générale de la méthode, et l'objet nécessaire pour cette méthode sera créé de manière polymorphe.

Par exemple, revenons à nos contrôleurs pour afficher les véhicules:

 final class BicycleViewController: UIViewController { } final class ScooterViewController: UIViewController { } 

Et supposons qu'une certaine entité soit utilisée pour les afficher, par exemple, un coordinateur , qui représente ces contrôleurs de façon modale à partir d'un autre contrôleur:

 protocol Coordinator { var presentingViewController: UIViewController? { get set } func start() } 

La méthode start() est toujours utilisée de la même manière, sauf qu'elle crée différents contrôleurs:

 final class BicycleCoordinator: Coordinator { weak var presentingViewController: UIViewController? func start() { let vc = BicycleViewController() presentingViewController?.present(vc, animated: true) } } final class ScooterCoordinator: Coordinator { weak var presentingViewController: UIViewController? func start() { let vc = ScooterViewController() presentingViewController?.present(vc, animated: true) } } 

La solution proposée est de faire la création de l'objet utilisé dans une méthode distincte:

 protocol Coordinator { var presentingViewController: UIViewController? { get set } func start() func makeViewController() -> UIViewController } 

Et la méthode principale consiste à fournir l'implémentation de base:

 extension Coordinator { func start() { let vc = makeViewController() presentingViewController?.present(vc, animated: true) } } 

Les types spécifiques dans ce cas prendront la forme:

 final class BicycleCoordinator: Coordinator { weak var presentingViewController: UIViewController? func makeViewController() -> UIViewController { return BicycleViewController() } } final class ScooterCoordinator: Coordinator { weak var presentingViewController: UIViewController? func makeViewController() -> UIViewController { return ScooterViewController() } } 

Conclusion


J'ai essayé de couvrir ce sujet simple en combinant trois approches:

  • la déclaration classique de l'existence de la réception, inspirée du livre «Gangs of Four»;
  • motivation d'utilisation, ouvertement inspirée par le livre de Kerivsky;
  • application appliquée comme exemple d'une industrie de programmation proche de moi.

En même temps, j'ai essayé d'être aussi proche que possible de la structure des manuels des modèles, dans la mesure du possible, sans détruire les principes de l'approche moderne du développement pour le système iOS et en utilisant les capacités du langage Swift (au lieu des plus courants C ++ et Java).

En fin de compte, il est assez difficile de trouver des documents détaillés sur le sujet contenant des exemples appliqués. La plupart des articles et manuels existants ne contiennent que des revues superficielles et des exemples abrégés, qui sont déjà assez tronqués par rapport aux versions de manuels des implémentations.

J'espère qu'au moins partiellement j'ai pu atteindre mes objectifs, et le lecteur - au moins partiellement était intéressé ou au moins curieux d'apprendre ou de rafraîchir mes connaissances sur ce sujet.

Mes autres matériaux sur les modèles de conception:


Et ceci est un lien vers mon «Twitter», où je publie des liens vers mes essais et un peu plus que cela.

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


All Articles