Patrón arquitectónico "Visitante" en los universos "iOS" y "Swift"

El "visitante" es uno de los patrones de comportamiento descritos en el libro de texto "Gang of Four", "GoF", "Patrones de diseño: elementos de software orientado a objetos reutilizables". ") .
En resumen, la plantilla puede ser útil cuando es necesario poder realizar cualquier acción del mismo tipo en un grupo de objetos de diferentes tipos que no están conectados entre sí. O, en otras palabras, ampliar la funcionalidad de esta serie de tipos con una determinada operación del mismo tipo o con una sola fuente. Al mismo tiempo, la estructura y la implementación de los tipos extensibles no deberían verse afectadas.
La forma más fácil de explicar la idea es con un ejemplo.

Inmediatamente me gustaría hacer una reserva de que el ejemplo es ficticio y está compuesto para fines académicos. Es decir Este material está destinado a introducir la recepción de OOP y no a discutir problemas altamente especializados.

También me gustaría llamar la atención sobre el hecho de que el código en los ejemplos fue escrito para estudiar la técnica de diseño. Soy consciente de sus deficiencias (de código) y de las posibilidades de mejorarlo para su uso en proyectos reales.

Ejemplo


Supongamos que tiene un subtipo de UITableViewController que utiliza varios subtipos 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() } } 

Supongamos que las celdas de diferentes subtipos tienen diferentes alturas.

Por supuesto, el cálculo de la altura se puede colocar directamente en la implementación de cada tipo de celda. Pero, ¿qué pasa si la altura de la celda depende no solo de su propio tipo, sino también de cualquier condición externa? Por ejemplo, un tipo de celda se puede usar en diferentes tablas con diferentes alturas. En este caso, no queremos que las subclases UITableViewCell sean conscientes de las necesidades de su "supervista" o "controlador de vista".

Luego, el cálculo de la altura se puede realizar en los métodos UITableViewController : inicialice UITableViewCell con el valor de altura o UITableViewCell instancia de UITableViewCell en un subtipo específico y devuelva diferentes valores en el método tableView(_:heightForRowAt:) . Pero este enfoque también puede volverse inflexible y convertirse en una larga secuencia de operadores "si" o una construcción voluminosa de "interruptor".

Resolviendo el problema usando la plantilla "Visitante"


Por supuesto, no solo la plantilla "Visitante" puede resolver este problema, sino que puede hacerlo con bastante elegancia.

Para hacer esto, en primer lugar, crearemos un tipo que, de hecho, será un "visitante" de los tipos de celda y un objeto cuya responsabilidad es solo calcular la altura de la celda de la tabla:

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

El tipo conoce cada subtipo utilizado y devuelve el valor deseado para cada uno de ellos.

En segundo lugar, cada subtipo de UITableViewCell debe poder "recibir" a este "visitante". Para hacer esto, declararemos un protocolo con tal método de "recepción", que será implementado por todos los tipos de células usadas:

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

Dentro de la subclase UITableViewController , la funcionalidad se puede utilizar de la siguiente manera:

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

Podría ser mejor!


Lo más probable es que no queramos tener dicho código que esté rígidamente conectado a una funcionalidad específica. Tal vez queremos poder agregar nuevas funcionalidades a nuestro conjunto de celdas, pero no solo con respecto a su altura, sino, por ejemplo, el color de fondo, el texto dentro de la celda, etc., y no estar vinculados al tipo del valor de retorno. Los protocolos con tipo associatedtype ( "Protocolo con tipo asociado", "PAT" ) ayudarán aquí:

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

Su implementación para devolver la altura de la celda:

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

En el lado del "host", es suficiente tener solo un protocolo común y su única implementación, para cualquier "visitante" de este tipo. Solo las partes "visitantes" serán conscientes de los diferentes tipos de valores de retorno.

El protocolo para el "visitante receptor" (en el libro "GoF" de este lado se llama "Elemento") del tipo tomará la forma:

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

(Puede que no haya restricciones para el tipo de implementación. Pero en este ejemplo no tiene sentido implementar este protocolo por subclases de UITableViewCell ).

Y su implementación en subtipos 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) } } 

Y finalmente, use:

 override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { let cell = tableView.cellForRow(at: indexPath) as! Visitableell return cell.accept(HeightResultCellVisitor()) } 
Por lo tanto, podremos crear, usando diferentes implementaciones del "visitante", en general, casi cualquier cosa, y no se requerirá nada del "lado receptor" para soportar la nueva funcionalidad. Esta fiesta ni siquiera sabrá qué es exactamente lo que el "invitado" ha otorgado.

Otro ejemplo


Intentemos cambiar el color de fondo de la celda usando un "visitante" similar:

 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 ejemplo de uso de este visitante:

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

Algo en este código debería ser confuso ... Al principio, se decía que el "visitante" puede agregar funcionalidad a la clase desde afuera. Entonces, ¿es posible "ocultar" en él toda la funcionalidad de cambiar el color de fondo de la celda, y no solo obtener el valor de ella? Usted puede Entonces el tipo associatedtype tomará el valor Void (aka () - una tupla vacía) :

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

Uso:

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


En lugar de una conclusión



Puede que le guste el patrón casi a primera vista, sin embargo, debe usarlo con cuidado. Su aparición en el código a menudo puede ser un signo de fallas más generales en la arquitectura. Quizás esté intentando conectar cosas que no deberían estar conectadas. Quizás valga la pena agregar una funcionalidad adicional a un nivel de abstracción de una forma u otra.

De una forma u otra, casi cualquier patrón tiene sus ventajas y desventajas, y antes de usarlo siempre debe pensar y tomar una decisión conscientemente. Los patrones son, por un lado, una forma de generalizar las técnicas de programación para facilitar la lectura y discusión del código. Y por otro lado, una forma de resolver un problema (a veces presentado artificialmente). Y, por supuesto, en cualquier caso, no traiga fanáticamente el código a todos los patrones conocidos solo por el solo hecho de su uso.


¡Supongo que ya terminé! ¡Todo un código hermoso y menos "errores"!

Mis otros artículos sobre patrones de diseño:

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


All Articles