Padrão arquitetural “Visitor” nos universos “iOS” e “Swift”

“Visitor” Ă© um dos padrĂ”es de comportamento descritos no livro “Gang of Four”, “GoF”, “Design Patterns: Elements of ReutilizĂĄvel Software Orientado a Objetos ”) .
Em resumo, o modelo pode ser Ăștil quando vocĂȘ precisar executar qualquer ação do mesmo tipo em um grupo de objetos de tipos diferentes nĂŁo conectados entre si. Ou, em outras palavras, expandir a funcionalidade dessa sĂ©rie de tipos com uma determinada operação do mesmo tipo ou com uma Ășnica fonte. Ao mesmo tempo, a estrutura e a implementação de tipos extensĂ­veis nĂŁo devem ser afetadas.
A maneira mais fĂĄcil de explicar a ideia Ă© com um exemplo.

Gostaria imediatamente de fazer uma reserva de que o exemplo Ă© fictĂ­cio e composto para fins acadĂȘmicos. I.e. este material pretende introduzir a recepção do POO, e nĂŁo discutir problemas altamente especializados.

TambĂ©m gostaria de chamar a atenção para o fato de o cĂłdigo nos exemplos ter sido escrito para estudar a tĂ©cnica de design. Estou ciente de suas deficiĂȘncias (de cĂłdigo) e das possibilidades de aprimorĂĄ-lo para uso em projetos reais.

Exemplo


Suponha que vocĂȘ tenha um subtipo de UITableViewController que use vĂĄrios 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() } } 

Suponha que células de diferentes subtipos tenham diferentes alturas.

Obviamente, o cålculo da altura pode ser colocado diretamente na implementação de cada tipo de célula. Mas e se a altura da célula depender não apenas de seu próprio tipo, mas também de quaisquer condiçÔes externas? Por exemplo, um tipo de célula pode ser usado em diferentes tabelas com diferentes alturas. Nesse caso, absolutamente não queremos que as subclasses UITableViewCell estejam cientes das necessidades de sua "superview" ou "view controller".

Em seguida, o cĂĄlculo da altura pode ser executado nos mĂ©todos UITableViewController : inicialize o UITableViewCell com o valor da altura ou UITableViewCell instĂąncia UITableViewCell em um subtipo especĂ­fico e retorne valores diferentes no mĂ©todo tableView(_:heightForRowAt:) . Mas essa abordagem tambĂ©m pode se tornar inflexĂ­vel e se transformar em uma longa sequĂȘncia de operadores "if" ou em uma construção volumosa de "switch".

Resolvendo o problema usando o modelo "Visitante"


Obviamente, nĂŁo apenas o modelo "Visitante" Ă© capaz de resolver esse problema, mas ele Ă© capaz de fazĂȘ-lo com bastante elegĂąncia.

Para fazer isso, em primeiro lugar, criaremos um tipo que serå, de fato, um "visitante" dos tipos de células e um objeto cuja responsabilidade é apenas calcular a altura da célula da tabela:

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

O tipo estĂĄ ciente de cada subtipo usado e retorna o valor desejado para cada um.

Em segundo lugar, cada subtipo de UITableViewCell deve poder "receber" este "visitante". Para fazer isso, declararemos um protocolo com esse método de "recebimento", que serå implementado por todos os tipos de células usados:

 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 da subclasse UITableViewController , a funcionalidade pode ser usada da seguinte maneira:

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

Poderia ser melhor!


Provavelmente, não queremos que esse código esteja rigidamente anexado a uma funcionalidade específica. Talvez desejemos poder adicionar novas funcionalidades ao nosso conjunto de células, mas não apenas em relação à sua altura, mas, digamos, a cor do plano de fundo, o texto dentro da célula, etc., e não estar vinculado ao tipo do valor de retorno. Os protocolos com o tipo associatedtype ( "Protocolo com tipo associado", "PAT" ) ajudarão aqui:

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

Sua implementação para retornar a altura da célula:

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

No lado “host”, basta ter apenas um protocolo comum e sua Ășnica implementação - para qualquer “visitante” desse tipo. Somente as partes "visitantes" estarĂŁo cientes dos diferentes tipos de valores de retorno.

O protocolo para o "visitante receptor" (no livro "GoF" deste lado Ă© chamado "Elemento") do tipo terĂĄ a seguinte forma:

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

(Pode não haver restriçÔes para o tipo de implementação. Mas neste exemplo, não faz sentido implementar esse protocolo pelas subclasses de UITableViewCell .)

E sua implementação nos 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) } } 

E, finalmente, use:

 override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { let cell = tableView.cellForRow(at: indexPath) as! Visitableell return cell.accept(HeightResultCellVisitor()) } 
Assim, poderemos criar, usando diferentes implementaçÔes do “visitante”, em geral, quase tudo, e nada serĂĄ necessĂĄrio do “lado receptor” para suportar a nova funcionalidade. Essa festa nem mesmo saberĂĄ o que exatamente o "convidado" concedeu.

Outro exemplo


Vamos tentar alterar a cor de fundo da célula usando um "visitante" semelhante:

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

Um exemplo de uso deste visitante:

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

Algo neste cĂłdigo deve ser confuso ... No inĂ­cio, foi dito que o "visitante" Ă© capaz de adicionar funcionalidade Ă  classe de fora. Portanto, Ă© possĂ­vel "ocultar" nele toda a funcionalidade de alterar a cor de fundo da cĂ©lula, e nĂŁo apenas obter o valor dela? VocĂȘ pode. Em seguida, o tipo associatedtype assumirĂĄ o valor Void (aka () - uma tupla vazia) :

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


Em vez de uma conclusĂŁo



VocĂȘ pode gostar do padrĂŁo quase Ă  primeira vista, no entanto, deve usĂĄ-lo com cuidado. Sua aparĂȘncia no cĂłdigo geralmente pode ser um sinal de falhas mais gerais na arquitetura. Talvez vocĂȘ esteja tentando conectar coisas que nĂŁo deveriam estar conectadas. Talvez a funcionalidade adicionada valha a pena elevar um nĂ­vel de abstração de uma maneira ou de outra.

De uma forma ou de outra, quase qualquer padrĂŁo tem suas vantagens e desvantagens e, antes de usĂĄ-lo, vocĂȘ deve sempre pensar e tomar uma decisĂŁo conscientemente. Os padrĂ”es sĂŁo, por um lado, uma maneira de generalizar as tĂ©cnicas de programação para facilitar a leitura e a discussĂŁo do cĂłdigo. E por outro lado - uma maneira de resolver um problema (Ă s vezes introduzido artificialmente). E, Ă© claro, de qualquer forma, nĂŁo traga fanaticamente o cĂłdigo para todos os padrĂ”es conhecidos apenas pelo fato de seu uso.


Acho que terminei! Todo o cĂłdigo bonito e menos "bugs"!

Meus outros artigos sobre padrÔes de design:

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


All Articles