Архитектурный шаблон «Посетитель» (“Visitor”) во вселенной «iOS» и «Swift»

    «Посетитель» (“Visitor”) – это один из поведенческих шаблонов, описанных в хрестоматийной книге «банды четырех» (“Gang of Four”, “GoF”) “Шаблоны проектирования” (“Design Patterns: Elements of Reusable Object-Oriented Software”).
    Если вкратце, то шаблон может быть полезен, когда необходимо иметь возможность совершить какие-либо однотипные действия над группой не связанных друг с другом объектов разных типов. Или, другими словами, расширить функциональность данного ряда типов некоей операцией, однотипной или имеющей единый источник. При этом структура и реализация расширяемых типов затронута быть не должна.
    Проще всего объяснить идею на примере.

    Сразу хотелось бы оговориться, что пример вымышленный и сочинен в академических целях. Т.е. данный материал призван познакомить с приемом ООП, а не обсуждать узко-специализированные проблемы.

    Также хотелось бы обратить внимание, что приведенный в примерах код написан в целях исследования приема проектирования. Мне известно о его (кода) недостатках и возможностях его улучшения для использования в реальных проектах.

    Пример


    Предположим, имеется подтип UITableViewController, в котором используются несколько подтипов 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()
        }
    
    }
    

    Предположим, ячейки разных подтипов должны иметь разную высоту.

    Конечно, расчет высоты можно поместить непосредственно в реализацию каждого типа ячейки. Но что если высота ячейки зависит не только от собственного типа, но и от каких-либо внешних условий? Например, тип ячейки может использоваться в разных таблицах с разной высотой. В этом случае мы совершенно не хотим, чтобы подклассы UITableViewCell были осведомлены о потребностях своих “superview” или “view controller”.

    Тогда расчет высоты можно производить в методах UITableViewController: либо инициализировать UITableViewCell вместе со значением высоты, либо приводить экземпляр UITableViewCell к конкретному подтипу и возвращать разные значения в методе tableView(_:heightForRowAt:). Но такой подход также может стать негибким и обернуться длинной последовательностью «if»-операторов или громоздкой «switch»-конструкцией.

    Решение задачи с помощью шаблона «Посетитель»


    Конечно, не только шаблон «Посетитель» способен решить эту задачу, но он способен это сделать вполне элегантно.

    Для этого, во-первых, создадим тип, который и будет, собственно, являться «посетителем» типов ячеек и объектом, ответственность которого заключается лишь в расчете высоты ячейки таблицы:

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

    Тип осведомлен о каждом используемом подтипе и возвращает нужное значение для каждого из них.

    Во-вторых, каждый подтип UITableViewCell должен уметь «принимать» данного «посетителя». Для этого объявим протокол с таким «принимающим» методом, который будет реализован всеми используемыми типами ячеек:

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

    Внутри подкласса UITableViewController функционал может быть использован следующим образом:

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

    Можно лучше!


    Скорее всего, мы не хотим иметь такой жестко-привязанный к конкретной функциональности код. Возможно, мы хотим иметь возможность добавить нашему набору ячеек новые функциональности, но не только касающиеся их высоты, а, скажем, цвета фона, текста внутри ячейки и т.д., и не привязываться к типу возвращаемого значения. Здесь нам помогут протоколы с associatedtype (“Protocol with Associated Type”, “PAT”):

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

    Его реализация для возврата высоты ячейки:

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

    С «принимающей» стороны достаточно иметь лишь общий протокол и одну-единственную его реализацию – для любого «посетителя» данного типа. Осведомлены о различных типах возвращаемых значений будут только стороны «посетителя».

    Протокол для «принимающего посетителя» (в книге «GoF» эта сторона именуется «Element») типа примет вид:

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

    (Ограничения для реализующего типа здесь может и не быть. Но в данном примере реализовывать этот протокол не подклассами UITableViewCell не имеет смысла.)

    И его реализации у подтипов 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)
        }
    }
    

    И, наконец, использование:

    override func tableView(_ tableView: UITableView,
                            heightForRowAt indexPath: IndexPath) -> CGFloat {
        let cell = tableView.cellForRow(at: indexPath) as! VisitableСell
        return cell.accept(HeightResultCellVisitor())
    }
    
    Таким образом, мы сможем создавать с помощью разных реализаций «посетителя», в общем-то, почти что угодно, а от «принимающей стороны» при этом не потребуется ничего для поддержки нового функционала. Эта сторона даже не будет осведомлена, что конкретно за «гость» пожаловал.

    Еще пример


    Попробуем изменить цвет фона ячейки с помощью аналогичного «посетителя»:

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

    Пример использования этого посетителя:

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

    Что-то в этом коде должно смущать… Вначале шла речь о том, что «посетитель» способен добавить функционал классу, находясь снаружи. Так нельзя ли «спрятать» в него весь функционал смены цвета фона ячейки, а не просто лишь получать от него значение? Можно. Тогда associatedtype здесь примет значение Void (он же () – пустой кортеж):

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

    Использование:

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


    Вместо заключения



    Паттерн может понравиться практически с первого взгляда, тем не менее применять его необходимо осторожно. Его появление в коде зачастую может стать признаком более общих недостатков в архитектуре. Возможно, вы пытаетесь связать вещи, которые не должны быть связаны. Возможно, добавляемый функционал стоит тем или иным образом вынести на один уровень абстракции выше.

    Так или иначе, практический любой паттерн имеет свои достоинства и недостатки, и перед его использованием всегда стоит задуматься и принять решение осознанно. Паттерны – это, с одной стороны, способ обощить техники программирования для удобства чтения и обсуждения кода. А с другой – способ решения какой-либо проблемы (порой, привнесенной искуственно). И, конечно, в любом случае, не стоит фанатично приводить код ко всем известным паттернам просто ради самого факта их использования.


    Пожалуй, я закончил! Всем красивого кода и поменьше «багов»!

    Другие мои статьи о шаблонах проектирования:

    Поделиться публикацией

    Похожие публикации

    Комментарии 1

      +2
      Примеры хорошие, но как по мне, лучше сразу объявлять вью-модели ячеек, с мапперами моделей уровня сервисов во вьюмодели. Вью-модели ячеек будут как раз таки содержать все необходимые свойства (высота, цвет, тип ячейки и т.д.), а таблица только подписывать на датасорс таких ячеек.

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое