Modèle architectural «Visitor» dans les univers «iOS» et «Swift»

«Visiteur» est l'un des modèles de comportement décrits dans le manuel «Gang of Four», «GoF», «Design Patterns: Elements of Reusable Object-Oriented Software ”) .
En bref, le modèle peut être utile lorsqu'il est nécessaire de pouvoir effectuer des actions du même type sur un groupe d'objets de types différents non connectés les uns aux autres. Ou, en d'autres termes, pour étendre les fonctionnalités de cette série de types avec une certaine opération du même type ou ayant une seule source. Dans le même temps, la structure et la mise en œuvre des types extensibles ne devraient pas être affectées.
La façon la plus simple d'expliquer l'idée est avec un exemple.

Je voudrais immédiatement faire une réserve que l'exemple est fictif et composé à des fins académiques. C'est-à-dire ce matériel est destiné à présenter la réception de la POO et non à discuter de problèmes hautement spécialisés.

Je voudrais également attirer l'attention sur le fait que le code dans les exemples a été écrit afin d'étudier la technique de conception. Je connais ses défauts (de code) et les possibilités de l'améliorer pour une utilisation dans des projets réels.

Exemple


Supposons que vous ayez un sous-type de UITableViewController qui utilise plusieurs sous-types de UITableViewCell :

 class FirstCell: UITableViewCell { /**/ } class SecondCell: UITableViewCell { /**/ } class ThirdCell: UITableViewCell { /**/ } class TableVC: UITableViewController { override func viewDidLoad() { super.viewDidLoad() tableView.register(FirstCell.self, forCellReuseIdentifier: "FirstCell") tableView.register(SecondCell.self, forCellReuseIdentifier: "SecondCell") tableView.register(ThirdCell.self, forCellReuseIdentifier: "ThirdCell") } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { /**/ return FirstCell() /**/ return SecondCell() /**/ return ThirdCell() } } 

Supposons que les cellules de différents sous-types aient des hauteurs différentes.

Bien entendu, le calcul de la hauteur peut être placé directement dans l'implémentation de chaque type de cellule. Mais que se passe-t-il si la hauteur de la cellule dépend non seulement de son propre type, mais aussi de toute condition extérieure? Par exemple, un type de cellule peut être utilisé dans différents tableaux avec différentes hauteurs. Dans ce cas, nous ne voulons absolument pas que les sous-classes UITableViewCell soient conscientes des besoins de leur «superview» ou «view controller».

Ensuite, le calcul de la hauteur peut être effectué dans les méthodes UITableViewController : soit initialiser UITableViewCell avec la valeur de hauteur, soit UITableViewCell instance UITableViewCell en un sous-type spécifique et renvoyer différentes valeurs dans la méthode tableView(_:heightForRowAt:) . Mais cette approche peut aussi devenir inflexible et se transformer en une longue séquence d'opérateurs «si» ou en une construction encombrante de «commutateur».

Résolution du problème à l'aide du modèle "Visiteur"


Bien sûr, non seulement le modèle «Visiteur» est capable de résoudre ce problème, mais il est capable de le faire avec élégance.

Pour ce faire, dans un premier temps, nous allons créer un type qui sera, en fait, un «visiteur» de types de cellules et un objet dont la responsabilité est uniquement de calculer la hauteur de la cellule du tableau:

 struct HeightResultVisitor { func visit(_ ell: FirstCell) -> CGFloat { return 10.0 } func visit(_ ell: SecondCell) -> CGFloat { return 20.0 } func visit(_ ell: ThirdCell) -> CGFloat { return 30.0 } } 

Le type connaît chaque sous-type utilisé et renvoie la valeur souhaitée pour chacun d'eux.

Deuxièmement, chaque sous-type de UITableViewCell doit pouvoir "recevoir" ce "visiteur". Pour ce faire, nous allons déclarer un protocole avec une telle méthode de "réception", qui sera implémentée par tous les types de cellules utilisés:

 protocol HeightResultVisitable { func accept(_ visitor: HeightResultVisitor) -> CGFloat } extension FirstCell: HeightResultVisitable { func accept(_ visitor: HeightResultVisitor) -> CGFloat { return visitor.visit(self) } } extension SecondCell: HeightResultVisitable { func accept(_ visitor: HeightResultVisitor) -> CGFloat { return visitor.visit(self) } } extension ThirdCell: HeightResultVisitable { func accept(_ visitor: HeightResultVisitor) -> CGFloat { return visitor.visit(self) } } 

À l'intérieur de la sous-classe UITableViewController , la fonctionnalité peut être utilisée comme suit:

 override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { let cell = tableView.cellForRow(at: indexPath) as! HeightResultVisitable return cell.accept(HeightResultVisitor()) } 

Ça pourrait être mieux!


Très probablement, nous ne voulons pas qu'un tel code soit attaché de manière rigide à une fonctionnalité spécifique. Peut-être que nous voulons pouvoir ajouter de nouvelles fonctionnalités à notre ensemble de cellules, mais pas seulement en ce qui concerne leur hauteur, mais, par exemple, la couleur d'arrière-plan, le texte à l'intérieur de la cellule, etc., et ne pas être liés au type de la valeur de retour. Les protocoles avec type associatedtype ( «Protocole avec type associé», «PAT» ) vous aideront ici:

 protocol CellVisitor { associatedtype T func visit(_ cell: FirstCell) -> T func visit(_ cell: SecondCell) -> T func visit(_ cell: ThirdCell) -> T } 

Son implémentation pour le retour de la hauteur de cellule:

 struct HeightResultCellVisitor: CellVisitor { func visit(_ cell: FirstCell) -> CGFloat { return 10.0 } func visit(_ cell: SecondCell) -> CGFloat { return 20.0 } func visit(_ cell: ThirdCell) -> CGFloat { return 30.0 } } 

Du côté «hôte», il suffit de n'avoir qu'un protocole commun et sa seule implémentation - pour tout «visiteur» de ce type. Seules les parties «visiteurs» seront conscientes des différents types de valeurs de retour.

Le protocole pour le "visiteur récepteur" (dans le livre "GoF" ce côté est appelé "Element") prendra la forme:

 protocol Visitableell where Self: UITableViewCell { func accept<V: CellVisitor>(_ visitor: V) -> VT } 

(Il peut n'y avoir aucune restriction pour le type d'implémentation. Mais dans cet exemple, il n'est pas logique d'implémenter ce protocole par des sous-classes de UITableViewCell .)

Et son implémentation dans les sous-types de UITableViewCell :

 extension FirstCell: Visitableell { func accept<V: CellVisitor>(_ visitor: V) -> VT { return visitor.visit(self) } } extension SecondCell: Visitableell { func accept<V: CellVisitor>(_ visitor: V) -> VT { return visitor.visit(self) } } extension ThirdCell: Visitableell { func accept<V: CellVisitor>(_ visitor: V) -> VT { return visitor.visit(self) } } 

Et enfin, utilisez:

 override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { let cell = tableView.cellForRow(at: indexPath) as! Visitableell return cell.accept(HeightResultCellVisitor()) } 
Ainsi, nous pourrons créer, en utilisant différentes implémentations du «visiteur», en général, presque n'importe quoi, et rien ne sera exigé du «côté récepteur» pour supporter la nouvelle fonctionnalité. Cette fête ne saura même pas exactement ce que "l'invité" a accordé.

Un autre exemple


Essayons de changer la couleur d'arrière-plan de la cellule en utilisant un "visiteur" similaire:

 struct ColorResultCellVisitor: CellVisitor { func visit(_ cell: FirstCell) -> UIColor { return .black } func visit(_ cell: SecondCell) -> UIColor { return .white } func visit(_ cell: ThirdCell) -> UIColor { return .red } } 

Un exemple d'utilisation de ce visiteur:

 override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { cell.contentView.backgroundColor = (cell as! Visitableell).accept(ColorResultCellVisitor()) } 

Quelque chose dans ce code devrait être déroutant ... Au début, il était dit que le "visiteur" était capable d'ajouter des fonctionnalités à la classe de l'extérieur. Est-il donc possible de "cacher" en elle toutes les fonctionnalités de changement de la couleur d'arrière-plan de la cellule, et pas seulement d'en tirer la valeur? Tu peux. Le type associatedtype prendra alors la valeur Void (aka () - un tuple vide) :

 struct BackgroundColorSetter: CellVisitor{ func visit(_ cell: FirstCell) { cell.contentView.backgroundColor = .black } func visit(_ cell: SecondCell) { cell.contentView.backgroundColor = .white } func visit(_ cell: ThirdCell) { cell.contentView.backgroundColor = .red } } 

Utilisation:

 override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { (cell as! Visitableell).accept(BackgroundColorSetter()) } 


Au lieu d'une conclusion



Vous pouvez aimer le motif presque à première vue, cependant, vous devez l'utiliser avec soin. Son apparition dans le code peut souvent être le signe de défauts plus généraux de l'architecture. Peut-être que vous essayez de connecter des choses qui ne devraient pas l'être. Peut-être que la fonctionnalité ajoutée vaut la peine de mettre un niveau d'abstraction plus haut d'une manière ou d'une autre.

D'une manière ou d'une autre, presque n'importe quel modèle a ses avantages et ses inconvénients, et avant de l'utiliser, vous devez toujours réfléchir et prendre une décision consciemment. Les modèles sont, d'une part, un moyen de généraliser les techniques de programmation pour faciliter la lecture et la discussion du code. Et de l'autre - un moyen de résoudre un problème (parfois introduit artificiellement). Et, bien sûr, dans tous les cas, n'apportez pas fanatiquement le code à tous les modèles connus juste pour le fait même de leur utilisation.


Je suppose que j'ai fini! Tout beau code et moins de "bugs"!

Mes autres articles sur les modèles de conception:

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


All Articles