Architectural pattern "Visitor" ("Visitor") in the universe of "iOS" and "Swift"

Published on December 28, 2018

Architectural pattern "Visitor" ("Visitor") in the universe of "iOS" and "Swift"

    “Visitor” is one of the behavioral patterns described in the “Gang of Four”, “GoF” textbook that is “Design Patterns: Elements of Reusable Object-Oriented Software ") .
    In short, the template can be useful when it is necessary to be able to perform any similar actions on a group of unrelated objects of different types. Or, in other words, to extend the functionality of this series of types by some kind of operation, of the same type or having a single source. At the same time, the structure and implementation of extensible types should not be affected.
    The easiest way to explain an idea is by example.

    Immediately I would like to make a reservation that the example is fictional and composed for academic purposes. Those. This material is intended to introduce the reception of the PLO, and not to discuss narrow-specialized problems.

    I would also like to note that the code given in the examples was written in order to study the design technique. I am aware of its (code) deficiencies and the possibilities of its improvement for use in real projects.

    Example


    Suppose there is a subtype UITableViewControllerin which several subtypes are used 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()
        }
    }
    

    Suppose cells of different subtypes should have different heights.

    Of course, the height calculation can be placed directly in the implementation of each cell type. But what if the height of the cell depends not only on its own type, but also on any external conditions? For example, a cell type can be used in different tables with different heights. In this case, we absolutely do not want the subclasses to UITableViewCellbe aware of the needs of their “superview” or “view controller”.

    Then the height calculation can be performed in the methods UITableViewController: either initialize UITableViewCelltogether with the height value, or you can cast the instance UITableViewCellto a specific subtype and return different values ​​in the methodtableView(_:heightForRowAt:). But such an approach can also become inflexible and turn into a long sequence of “if” operators or a cumbersome “switch” structure.

    Solution of the problem using the "Visitor" template


    Of course, not only the template "Visitor" is able to solve this problem, but it is able to do it quite elegantly.

    To do this, first, we will create a type, which will, in fact, be a “visitor” of cell types and an object whose responsibility lies only in calculating the height of the table cell:

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

    The type is aware of each subtype used and returns the desired value for each of them.

    Secondly, each subtype UITableViewCellmust be able to “accept” a given “visitor”. To do this, we will declare a protocol with this “accepting” method, which will be implemented by all the types of cells used:

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

    Inside the subclass, the UITableViewControllerfunctionality can be used as follows:

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

    You can do better!


    Most likely, we do not want to have such a hard-bound to specific functionality code. Perhaps we want to be able to add new functionality to our set of cells, but not only their height, but, say, the background color, the text inside the cell, etc., and not be tied to the return value type. Here protocols with associatedtype( “Protocol with Associated Type”, “PAT” ) will help us :

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

    Its implementation to return the height of the cell:

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

    From the “receiving” side, it is sufficient to have only a general protocol and one single implementation for any “visitor” of this type. Only the “visitor” sides will be aware of the different types of return values.

    The protocol for the “receiving visitor” (in the book “GoF” this side is referred to as “Element”) of the type will look like:

    protocol VisitableСell where Self: UITableViewCell {
        func accept<V: CellVisitor>(_ visitor: V) -> V.T
    }
    

    (There may be no restrictions for the implementing type here. But in this example, it does UITableViewCellnot make sense to implement this protocol in non-subclasses .)

    And its implementations for subtypes UITableViewCell:

    extension FirstCell: VisitableСell {
        func accept<V: CellVisitor>(_ visitor: V) -> V.T  {
            return visitor.visit(self)
        }
    }
    extension SecondCell: VisitableСell {
        func accept<V: CellVisitor>(_ visitor: V) -> V.T  {
            return visitor.visit(self)
        }
    }
    extension ThirdCell: VisitableСell {
        func accept<V: CellVisitor>(_ visitor: V) -> V.T  {
            return visitor.visit(self)
        }
    }
    

    And finally, use:

    override func tableView(_ tableView: UITableView,
                            heightForRowAt indexPath: IndexPath) -> CGFloat {
        let cell = tableView.cellForRow(at: indexPath) as! VisitableСell
        return cell.accept(HeightResultCellVisitor())
    }
    
    Thus, we will be able to create with the help of different implementations of the “visitor”, in general, almost anything, and the “receiving party” will not need anything to support the new functionality. This party will not even be aware of what specifically for the "guest" complained.

    Another example


    Let's try to change the background color of the cell using a similar “visitor”:

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

    An example of using this visitor:

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

    Something in this code should be embarrassing ... At first, it was said that the "visitor" is able to add functionality to the class while being outside. So is it possible to “hide” in it all the functionality of changing the background color of the cell, and not just just get the value from it? Can. Then associatedtypeit will take on the meaning Void(it ()is an empty tuple) :

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

    Using:

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


    Instead of conclusion



    The pattern may be liked almost at a glance, however, it must be applied carefully. Its appearance in code can often be a sign of more general flaws in architecture. Perhaps you are trying to relate things that should not be related. Perhaps the added functionality is in one way or another to bring one level of abstraction above.

    Anyway, almost any pattern has its advantages and disadvantages, and before using it you should always think and make a decision consciously. Patterns are, on the one hand, a way to generalize programming techniques for easier reading and code discussion. On the other hand, there is a way to solve a problem (sometimes, artificially introduced). And, of course, in any case, you should not fanatically bring the code to all known patterns just for the sake of the very fact of their use.


    Perhaps I finished! All beautiful code and fewer "bugs"!

    My other articles about design patterns:
    Architectural pattern "Iterator" ("Iterator") in the universe "Swift"