Паттерн посетитель: Посетитель на C++
Посетитель
Также известен как: Visitor
Суть паттерна
Посетитель — это поведенческий паттерн проектирования, который позволяет добавлять в программу новые операции, не изменяя классы объектов, над которыми эти операции могут выполняться.
Проблема
Ваша команда разрабатывает приложение, работающее с геоданными в виде графа. Узлами графа являются городские локации: памятники, театры, рестораны, важные предприятия и прочее. Каждый узел имеет ссылки на другие, ближайшие к нему узлы. Каждому типу узлов соответствует свой класс, а каждый узел представлен отдельным объектом.
Экспорт геоузлов в XML.
Ваша задача — сделать экспорт этого графа в XML. Дело было бы плёвым, если бы вы могли редактировать классы узлов. Достаточно было бы добавить метод экспорта в каждый тип узла, а затем, перебирая узлы графа, вызывать этот метод для каждого узла. Благодаря полиморфизму, решение получилось бы изящным, так как вам не пришлось бы привязываться к конкретным классам узлов.
Но, к сожалению, классы узлов вам изменить не удалось. Системный архитектор сослался на то, что код классов узлов сейчас очень стабилен, и от него многое зависит, поэтому он не хочет рисковать и позволять кому-либо его трогать.
Код XML-экспорта придётся добавить во все классы узлов, а это слишком накладно.
К тому же он сомневался в том, что экспорт в XML вообще уместен в рамках этих классов. Их основная задача была связана с геоданными, а экспорт выглядит в рамках этих классов чужеродно.
Была и ещё одна причина запрета. Если на следующей неделе вам бы понадобился экспорт в какой-то другой формат данных, то эти классы снова пришлось бы менять.
Решение
Паттерн Посетитель предлагает разместить новое поведение в отдельном классе, вместо того чтобы множить его сразу в нескольких классах. Объекты, с которыми должно было быть связано поведение, не будут выполнять его самостоятельно. Вместо этого вы будете передавать эти объекты в методы посетителя.
Код поведения, скорее всего, должен отличаться для объектов разных классов, поэтому и методов у посетителя должно быть несколько. Названия и принцип действия этих методов будет схож, но основное отличие будет в типе принимаемого в параметрах объекта, например:
class ExportVisitor implements Visitor is
method doForCity(City c) { ... }
method doForIndustry(Industry f) { ... }
method doForSightSeeing(SightSeeing ss) { ... }
// ...
Здесь возникает вопрос: как подавать узлы в объект-посетитель? Так как все методы имеют отличающуюся сигнатуру, использовать полиморфизм при переборе узлов не получится. Придётся проверять тип узлов для того, чтобы выбрать соответствующий метод посетителя.
foreach (Node node in graph)
if (node instanceof City)
exportVisitor.doForCity((City) node)
if (node instanceof Industry)
exportVisitor.doForIndustry((Industry) node)
// ...
Тут не поможет даже механизм перегрузки методов (доступный в Java и C#). Если назвать все методы одинаково, то неопределённость реального типа узла всё равно не даст вызвать правильный метод. Механизм перегрузки всё время будет вызывать метод посетителя, соответствующий типу Node
, а не реального класса поданного узла.
Но паттерн Посетитель решает и эту проблему, используя механизм двойной диспетчеризации. Вместо того, чтобы самим искать нужный метод, мы можем поручить это объектам, которые передаём в параметрах посетителю. А они уже вызовут правильный метод посетителя.
// Client code
foreach (Node node in graph)
node.accept(exportVisitor)
// City
class City is
method accept(Visitor v) is
v.doForCity(this)
// ...
// Industry
class Industry is
method accept(Visitor v) is
v.doForIndustry(this)
// ...
Как видите, изменить классы узлов всё-таки придётся. Но это простое изменение позволит применять к объектам узлов и другие поведения, ведь классы узлов будут привязаны не к конкретному классу посетителей, а к их общему интерфейсу. Поэтому если придётся добавить в программу новое поведение, вы создадите новый класс посетителей и будете передавать его в методы узлов.
Аналогия из жизни
У страхового агента приготовлены полисы для разных видов организаций.
Представьте начинающего страхового агента, жаждущего получить новых клиентов. Он беспорядочно посещает все дома в округе, предлагая свои услуги. Но для каждого из посещаемых типов домов у него имеется особое предложение.
- Придя в дом к обычной семье, он предлагает оформить медицинскую страховку.
- Придя в банк, он предлагает страховку от грабежа.
- Придя на фабрику, он предлагает страховку предприятия от пожара и наводнения.
Структура
Посетитель описывает общий интерфейс для всех типов посетителей. Он объявляет набор методов, отличающихся типом входящего параметра, которые нужны для запуска операции для всех типов конкретных элементов. В языках, поддерживающих перегрузку методов, эти методы могут иметь одинаковые имена, но типы их параметров должны отличаться.
Конкретные посетители реализуют какое-то особенное поведение для всех типов элементов, которые можно подать через методы интерфейса посетителя.
Элемент описывает метод принятия посетителя. Этот метод должен иметь единственный параметр, объявленный с типом интерфейса посетителя.
Конкретные элементы реализуют методы принятия посетителя. Цель этого метода — вызвать тот метод посещения, который соответствует типу этого элемента. Так посетитель узнает, с каким именно элементом он работает.
Клиентом зачастую выступает коллекция или сложный составной объект, например, дерево Компоновщика. Зачастую клиент не привязан к конкретным классам элементов, работая с ними через общий интерфейс элементов.
Псевдокод
В этом примере Посетитель добавляет в существующую иерархию классов геометрических фигур возможность экспорта в XML.
Пример организации экспорта объектов в XML через отдельный класс-посетитель.
// Сложная иерархия элементов.
interface Shape is
method move(x, y)
method draw()
method accept(v: Visitor)
// Метод принятия посетителя должен быть реализован в каждом
// элементе, а не только в базовом классе. Это поможет программе
// определить, какой метод посетителя нужно вызвать, если вы не
// знаете тип элемента.
class Dot implements Shape is
// ...
method accept(v: Visitor) is
v.visitDot(this)
class Circle implements Shape is
// ...
method accept(v: Visitor) is
v.visitCircle(this)
class Rectangle implements Shape is
// ...
method accept(v: Visitor) is
v.visitRectangle(this)
class CompoundShape implements Shape is
// ...
method accept(v: Visitor) is
v.visitCompoundShape(this)
// Интерфейс посетителей должен содержать методы посещения
// каждого элемента. Важно, чтобы иерархия элементов менялась
// редко, так как при добавлении нового элемента придётся менять
// всех существующих посетителей.
interface Visitor is
method visitDot(d: Dot)
method visitCircle(c: Circle)
method visitRectangle(r: Rectangle)
method visitCompoundShape(cs: CompoundShape)
// Конкретный посетитель реализует одну операцию для всей
// иерархии элементов. Новая операция = новый посетитель.
// Посетитель выгодно применять, когда новые элементы
// добавляются очень редко, а новые операции — часто.
class XMLExportVisitor implements Visitor is
method visitDot(d: Dot) is
// Экспорт id и координат центра точки.
method visitCircle(c: Circle) is
// Экспорт id, кординат центра и радиуса окружности.
method visitRectangle(r: Rectangle) is
// Экспорт id, кординат левого-верхнего угла, ширины и
// высоты прямоугольника.
method visitCompoundShape(cs: CompoundShape) is
// Экспорт id составной фигуры, а также списка id
// подфигур, из которых она состоит.
// Приложение может применять посетителя к любому набору
// объектов элементов, даже не уточняя их типы. Нужный метод
// посетителя будет выбран благодаря проходу через метод accept.
class Application is
field allShapes: array of Shapes
method export() is
exportVisitor = new XMLExportVisitor()
foreach (shape in allShapes) do
shape.accept(exportVisitor)
Вам не кажется, что вызов метода accept
— это лишнее звено? Если так, то ещё раз рекомендую вам ознакомиться с проблемой раннего и позднего связывания в статье Посетитель и Double Dispatch.
Применимость
Когда вам нужно выполнить какую-то операцию над всеми элементами сложной структуры объектов, например, деревом.
Посетитель позволяет применять одну и ту же операцию к объектам различных классов.
Когда над объектами сложной структуры объектов надо выполнять некоторые не связанные между собой операции, но вы не хотите «засорять» классы такими операциями.
Посетитель позволяет извлечь родственные операции из классов, составляющих структуру объектов, поместив их в один класс-посетитель. Если структура объектов является общей для нескольких приложений, то паттерн позволит в каждое приложение включить только нужные операции.
Когда новое поведение имеет смысл только для некоторых классов из существующей иерархии.
Посетитель позволяет определить поведение только для этих классов, оставив его пустым для всех остальных.
Шаги реализации
Создайте интерфейс посетителя и объявите в нём методы «посещения» для каждого класса элемента, который существует в программе.
Опишите интерфейс элементов. Если вы работаете с уже существующими классами, то объявите абстрактный метод принятия посетителей в базовом классе иерархии элементов.
Реализуйте методы принятия во всех конкретных элементах. Они должны переадресовывать вызовы тому методу посетителя, в котором тип параметра совпадает с текущим классом элемента.
Иерархия элементов должна знать только о базовом интерфейсе посетителей. С другой стороны, посетители будут знать обо всех классах элементов.
Для каждого нового поведения создайте конкретный класс посетителя. Приспособьте это поведение для работы со всеми типами элементов, реализовав все методы интерфейса посетителей.
Вы можете столкнуться с ситуацией, когда посетителю нужен будет доступ к приватным полям элементов. В этом случае вы можете либо раскрыть доступ к этим полям, нарушив инкапсуляцию элементов, либо сделать класс посетителя вложенным в класс элемента, если вам повезло писать на языке, который поддерживает вложенность классов.
Клиент будет создавать объекты посетителей, а затем передавать их элементам, используя метод принятия.
Преимущества и недостатки
- Упрощает добавление операций, работающих со сложными структурами объектов.
- Объединяет родственные операции в одном классе.
- Посетитель может накапливать состояние при обходе структуры элементов.
- Паттерн не оправдан, если иерархия элементов часто меняется.
- Может привести к нарушению инкапсуляции элементов.
Отношения с другими паттернами
Посетитель можно рассматривать как расширенный аналог Команды, который способен работать сразу с несколькими видами получателей.
Вы можете выполнить какое-то действие над всем деревом Компоновщика при помощи Посетителя.
Посетитель можно использовать совместно с Итератором. Итератор будет отвечать за обход структуры данных, а Посетитель — за выполнение действий над каждым её компонентом.
Посетитель на Python
Посетитель — это поведенческий паттерн, который позволяет добавить новую операцию для целой иерархии классов, не изменяя код этих классов.
Подробней о том, почему Посетитель нельзя заменить простой перегрузкой методов читайте в статье Посетитель и Double Dispatch.
Особенности паттерна на Python
Сложность:
Популярность:
Применимость: Посетитель нечасто встречается в Python-коде из-за своей сложности и нюансов реализазации.
Концептуальный пример
Этот пример показывает структуру паттерна Посетитель, а именно — из каких классов он состоит, какие роли эти классы выполняют и как они взаимодействуют друг с другом.
main.py: Пример структуры паттерна
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import List
class Component(ABC):
"""
Интерфейс Компонента объявляет метод accept, который в качестве аргумента
может получать любой объект, реализующий интерфейс посетителя.
"""
@abstractmethod
def accept(self, visitor: Visitor) -> None:
pass
class ConcreteComponentA(Component):
"""
Каждый Конкретный Компонент должен реализовать метод accept таким образом,
чтобы он вызывал метод посетителя, соответствующий классу компонента.
"""
def accept(self, visitor: Visitor) -> None:
"""
Обратите внимание, мы вызываем visitConcreteComponentA, что
соответствует названию текущего класса. Таким образом мы позволяем
посетителю узнать, с каким классом компонента он работает.
"""
visitor.visit_concrete_component_a(self)
def exclusive_method_of_concrete_component_a(self) -> str:
"""
Конкретные Компоненты могут иметь особые методы, не объявленные в их
базовом классе или интерфейсе. Посетитель всё же может использовать эти
методы, поскольку он знает о конкретном классе компонента.
"""
return "A"
class ConcreteComponentB(Component):
"""
То же самое здесь: visitConcreteComponentB => ConcreteComponentB
"""
def accept(self, visitor: Visitor):
visitor. visit_concrete_component_b(self)
def special_method_of_concrete_component_b(self) -> str:
return "B"
class Visitor(ABC):
"""
Интерфейс Посетителя объявляет набор методов посещения, соответствующих
классам компонентов. Сигнатура метода посещения позволяет посетителю
определить конкретный класс компонента, с которым он имеет дело.
"""
@abstractmethod
def visit_concrete_component_a(self, element: ConcreteComponentA) -> None:
pass
@abstractmethod
def visit_concrete_component_b(self, element: ConcreteComponentB) -> None:
pass
"""
Конкретные Посетители реализуют несколько версий одного и того же алгоритма,
которые могут работать со всеми классами конкретных компонентов.
Максимальную выгоду от паттерна Посетитель вы почувствуете, используя его со
сложной структурой объектов, такой как дерево Компоновщика. В этом случае было
бы полезно хранить некоторое промежуточное состояние алгоритма при выполнении
методов посетителя над различными объектами структуры.
"""
class ConcreteVisitor1(Visitor):
def visit_concrete_component_a(self, element) -> None:
print(f"{element.exclusive_method_of_concrete_component_a()} + ConcreteVisitor1")
def visit_concrete_component_b(self, element) -> None:
print(f"{element.special_method_of_concrete_component_b()} + ConcreteVisitor1")
class ConcreteVisitor2(Visitor):
def visit_concrete_component_a(self, element) -> None:
print(f"{element.exclusive_method_of_concrete_component_a()} + ConcreteVisitor2")
def visit_concrete_component_b(self, element) -> None:
print(f"{element.special_method_of_concrete_component_b()} + ConcreteVisitor2")
def client_code(components: List[Component], visitor: Visitor) -> None:
"""
Клиентский код может выполнять операции посетителя над любым набором
элементов, не выясняя их конкретных классов. Операция принятия направляет
вызов к соответствующей операции в объекте посетителя.
"""
# ...
for component in components:
component. accept(visitor)
# ...
if __name__ == "__main__":
components = [ConcreteComponentA(), ConcreteComponentB()]
print("The client code works with all visitors via the base Visitor interface:")
visitor1 = ConcreteVisitor1()
client_code(components, visitor1)
print("It allows the same client code to work with different types of visitors:")
visitor2 = ConcreteVisitor2()
client_code(components, visitor2)
Output.txt: Результат выполнения
The client code works with all visitors via the base Visitor interface:
A + ConcreteVisitor1
B + ConcreteVisitor1
It allows the same client code to work with different types of visitors:
A + ConcreteVisitor2
B + ConcreteVisitor2
Посетитель на других языках программирования
Посетитель (шаблон проектирования)
Пользователи также искали:
паттерн посетитель c#,
паттерн visitor c,
шаблон посетитель php,
visitor pattern java,
паттерны,
паттерн,
Посетитель,
посетитель,
шаблон,
visitor,
java,
pattern,
проектирования,
команда,
паттерн visitor c,
мост,
фасад,
паттерн мост,
паттерн команда,
паттерн фасад,
visitor pattern java,
паттерны проектирования,
шаблон посетитель php,
Посетитель шаблон проектирования,
паттерн посетитель c,
посетитель (шаблон проектирования),
/** | |
* | |
* ПАТТЕРН ПОСЕТИТЕЛЬ (visitor) | |
* | |
* ПОСЕТИТЕЛЬ — используется для расширения возможностей комбинации объектов, т. е. паттерн Посетитель позволяет | |
* добавлять объектам дополнительные операции, не изменяя их исходный код. | |
* | |
* Когда вам нужно выполнить какую-то операцию над всеми элементами сложной структуры объектов, например, деревом. | |
* Посетитель позволяет применять одну и ту же операцию к объектам различных классов. ИЛИ когда новое поведение имеет | |
* смысл только для некоторых классов из существующей иерархии. | |
* | |
* Шаблон следует использовать, если: | |
* 1. имеются различные объекты разных классов с разными интерфейсами, но над ними нужно совершать операции, зависящие | |
* от конкретных классов; | |
* 2. необходимо над структурой выполнить различные, усложняющие структуру операции; | |
* 3. часто добавляются новые операции над структурой. | |
* Реализация: | |
* 1. Добавьте метод accept(Visitor) в иерархию «элемент». | |
* 2. Создайте базовый класс Visitor и определите методы visit() для каждого типа элемента. | |
* 3. Создайте производные классы Visitor для каждой операции, исполняемой над элементами. | |
* 4. Клиент создаёт объект Visitor и передаёт его в вызываемый метод accept(). | |
* | |
* Посетителя часто используют своместно с итератором и компоновщиком. | |
*/ | |
// Элементы в которые будет приходить посетитель | |
class Monkey { | |
shout() { | |
console.log(‘Ooh oo aa aa!’) | |
} | |
accept(operation) { | |
operation.visitMonkey(this) | |
} | |
} | |
class Lion { | |
roar() { | |
console. log(‘Roaaar!’) | |
} | |
accept(operation) { | |
operation.visitLion(this) | |
} | |
} | |
class Dolphin { | |
speak() { | |
console.log(‘Tuut tuttu tuutt!’) | |
} | |
accept(operation) { | |
operation. visitDolphin(this) | |
} | |
} | |
// Посетители | |
class Visitor { | |
visitMonkey(monkey) { | |
throw new Error(`В ${this.constructor.name} не описан метод visitMonkey()`) | |
} | |
visitLion(lion) { | |
throw new Error(`В ${this.constructor.name} не описан метод visitLion()`); | |
} | |
visitDolphin(dolphin) { | |
throw new Error(`В ${this. constructor.name} не описан метод visitDolphin()`); | |
} | |
} | |
class voiceVisitor extends Visitor { | |
visitMonkey(monkey){ | |
monkey.shout() | |
} | |
visitLion(lion){ | |
lion.roar() | |
} | |
visitDolphin(dolphin){ | |
dolphin.speak() | |
} | |
} | |
class jumpVisitor extends Visitor { | |
visitMonkey(monkey) { | |
console. log(‘Jumped 20 feet high! on to the tree!’) | |
} | |
visitLion(lion) { | |
console.log(‘Jumped 7 feet! Back on the ground!’) | |
} | |
visitDolphin(dolphin) { | |
console.log(‘Walked on water a little and disappeared’) | |
} | |
} | |
const monkey = new Monkey() | |
const lion = new Lion() | |
const dolphin = new Dolphin() | |
const voicer = new voiceVisitor(); | |
const jumper = new jumpVisitor(); | |
// Пробуем первого посетителя | |
monkey. accept(voicer) // Ooh oo aa aa! | |
lion.accept(voicer) // Roaaar! | |
dolphin.accept(voicer) // Tuut tutt tuutt! | |
// Пробуем комбинацию из двух посетителей | |
// Второй посетитель добавляет новое поведение в классы элементов без изменения самих элементов. | |
monkey.accept(voicer) // Ooh oo aa aa! | |
monkey.accept(jumper) // Jumped 20 feet high! on to the tree! | |
lion.accept(voicer) // Roaaar! | |
lion. accept(jumper) // Jumped 7 feet! Back on the ground! | |
dolphin.accept(voicer) // Tuut tutt tuutt! | |
dolphin.accept(jumper) // Walked on water a little and disappeared |
Шаблон проектирования Посетитель (Visitor)
Epiphany.
In Python, with iterators, the
Visitor
design pattern is useless. And a strongly-ingrained habit. Which I’m trying to break.
Here’s a common
Visitor
approach:
class Visitor:
def __init__( self ): ...
def visit( self, some_target_thing ): ...
def all_done( self ): ...
v = Visitor()
for thing in some_iterator():
v.visit(thing)
v.all_done()
If we refactor the
for
statement into the
Visitor
, then it’s just a
Command
or something.
Here’s the refactored
Iterating Visitor
:
class Command:
def __init__( self ): . ..
def process_all( self, iterable ):
for thing in iterable:
self.visit( thing )
def visit( self, thing ): ...
def all_done( self ): ...
c=Command()
c.process_all( some_iterator() )
c.all_done()
Possible Objection
The one possible objection is this: «What if our data structure is so hellishly complex that we can’t reduce it to a simple iterator?»
That’s perfectly silly. Any hyper-complex algorithm to walk any hyper-complex data structure, no matter how hyper complex, can always be recast into a generator function which uses
yield
to iterate over the objects.
Better Design
Once we start down this road, we can generally simplify processing into a kind of
Command
that looks something like this.
class Command:
def __init__( self ): ...
def run( self ):
for thing in self.iterable:
....
c= Command()
c.iterable= some_iterator()
c.run()
I find that this interface is somewhat easier to deal with when composing large commands from individual small commands. It follows a
Create-Configure-Run
pattern that seems to work out well. I just wish I would start with this rather than start with a
Visitor
, refactor, and end up with this.
Шаблоны программирования на Swift: Visitor
В этой статье мы разберем шаблон программирования Визитер (Visitor) на языке программирования Swift в среде разработки Xcode.
Вот у каждого дома вероятнее всего есть холодильник. В ВАШЕМ доме, ВАШ холодильник. Что будет если холодильник сломается? Некоторые пойдут почитают в интернете как чинить холодильник, какая модель, попробуют поколдовать над ним, и разочаровавшись вызовут мастера по ремонту холодильников. Заметьте – холодильник ваш, но функцию “Чинить Холодильник” выполняет совершенно другой человек, про которого вы ничего не знаете, а попросту – обычный визитер
Паттерн визитер – позволяет вынести из наших объектов логику, которая относится к этим объектам, в отдельный клас, что позволяет нам легко изменять / добавлять алгоритмы, при этом не меняя логику самого класа.
Когда мы захотим использовать этот паттерн:
- Когда у нас есть сложный объект, в котором содержится большое количество различных элементов, и вы хотите выполнять различные операции в зависимости от типа этих элиментов.
- Вам необходимо выполнять различные операции над классами, и при этом Вы не хотите писать вагон кода внутри реализации этих классов.
- В конце–концов, вам нужно добавлять различные операции над элементами, и вы не хотите постоянно обновлять классы этих элементов.
Рассмотрим пример: у нас есть несколько складов, в каждом складе можнт хранится товар. Один визитер будет смотреть склады, другой визитер будет называть цену товара в складе.
Итак, для начала сам товар:
// // proSwift.ru // class WarehouseItem { var name:String var isBroken: Bool var price: Int init(aName:String, isBrokenState: Bool, aPrice: Int) { self.name = aName self.isBroken = isBrokenState self. price = aPrice } }
И естественно сам склад:
// // proSwift.ru // class Warehouse { lazy var itemsArray: [WarehouseItem] = [] func addItem(anItem: WarehouseItem) { itemsArray.append(anItem) } func accept(visitor: BasicVisitor) { visitor.visit(self) for item in itemsArray { visitor.visit(item) } } }
Как видим, наш склад умеет хранить и добавлять товар, но также обладает таинственным методом accept который принимает в себя визитор и вызвает его метод visit. Чтобы картинка сложилась, давайте создадим протокол BasicVisitor и различных визиторов:
// // proSwift.ru // protocol BasicVisitor { func visit(anObject: AnyObject) }
Как видим, протокол требует реализацию только одного метода. Теперь давайте перейдем к самим визитерам:
// // proSwift. ru // class QualityCheckerVisitor: BasicVisitor { func visit(anObject: AnyObject) { if let obj = anObject as? WarehouseItem { if obj.isBroken { print("Товар \(obj.name) сломан") } else { print("Товар \(obj.name) весьма не плох") } } if let _ = anObject as? Warehouse { print("Отличный склад!") } } }
Если почитать код, то сразу видно что визитер при вызове своего метода visit определяет тип объекта который ему передался, и выполняет определнные функции в зависимоти от этого типа. Данный объект просто говорит или вещи на складе поломаны, а так же что ему нравится склад:)
// // proSwift.ru // class PriceCheckerVisitor:BasicVisitor { func visit(anObject: AnyObject) { if let obj = anObject as? WarehouseItem { print("Товар \(obj. name) имеет цену: \(obj.price)") } if let _ = anObject as? Warehouse { print("Я не знаю сколько стоит склад!") } } }
В принципе этот визитер делает тоже самое, только в случае склада он признается что растерян, а в случае товара говорит цену товара!
Теперь давайте запустим то что у нас получилось! Код генерации тестовых даных:
// // proSwift.ru // let warehouse = Warehouse() warehouse.addItem(WarehouseItem(aName: "Товар1", isBrokenState: true, aPrice: 37)) warehouse.addItem(WarehouseItem(aName: "Товар2", isBrokenState: true, aPrice: 45)) warehouse.addItem(WarehouseItem(aName: "Товар3", isBrokenState: false, aPrice: 74)) warehouse.addItem(WarehouseItem(aName: "Товар4", isBrokenState: false, aPrice: 34)) warehouse.addItem(WarehouseItem(aName: "Товар5", isBrokenState: true, aPrice: 15)) warehouse.addItem(WarehouseItem(aName: "Товар6", isBrokenState: true, aPrice: 84)) warehouse. addItem(WarehouseItem(aName: "Товар7", isBrokenState: false, aPrice: 91)) warehouse.addItem(WarehouseItem(aName: "Товар8", isBrokenState: false, aPrice: 50)) warehouse.addItem(WarehouseItem(aName: "Товар9", isBrokenState: true, aPrice: 11)) let priceVisitor = PriceCheckerVisitor() let qualityVisitor = QualityCheckerVisitor() warehouse.accept(priceVisitor) print("- - - - - - - - - - - - - - - - - - - - ") warehouse.accept(qualityVisitor)
Вывод в консоль:
Я не знаю сколько стоит склад! Товар Товар1 имеет цену: 37 Товар Товар2 имеет цену: 45 Товар Товар3 имеет цену: 74 Товар Товар4 имеет цену: 34 Товар Товар5 имеет цену: 15 Товар Товар6 имеет цену: 84 Товар Товар7 имеет цену: 91 Товар Товар8 имеет цену: 50 Товар Товар9 имеет цену: 11 - - - - - - - - - - - - - - - - - - - - Отличный склад! Товар Товар1 сломан Товар Товар2 сломан Товар Товар3 весьма не плох Товар Товар4 весьма не плох Товар Товар5 сломан Товар Товар6 сломан Товар Товар7 весьма не плох Товар Товар8 весьма не плох Товар Товар9 сломан
Пример с GitHub
Шаблон посетитель используется для разделения относительно сложного набора методов класса, которые могут быть выполнены на данных, которые этот класс содержит.
// // proSwift.ru // // https://github.com/ochococo/Design-Patterns-In-Swift/blob/master/source/behavioral/visitor.swift protocol Planet { func accept(visitor: PlanetVisitor) } class PlanetAlderaan: Planet { func accept(visitor: PlanetVisitor) { visitor.visit(self) } } class PlanetCoruscant: Planet { func accept(visitor: PlanetVisitor) { visitor.visit(self) } } class PlanetTatooine: Planet { func accept(visitor: PlanetVisitor) { visitor.visit(self) } } protocol PlanetVisitor { func visit(planet: PlanetAlderaan) func visit(planet: PlanetCoruscant) func visit(planet: PlanetTatooine) } class NameVisitor: PlanetVisitor { var name = "" func visit(planet: PlanetAlderaan) { name = "Alderaan" } func visit(planet: PlanetCoruscant) { name = "Coruscant" } func visit(planet: PlanetTatooine) { name = "Tatooine" } } class VolumeVisitor: PlanetVisitor { var volume = "" func visit(planet: PlanetAlderaan) { volume = "355 * 10^134 км3" } func visit(planet: PlanetTatooine) { volume = "132 * 10^536 км3" } func visit(planet: PlanetCoruscant) { volume = "834 * 10^420 км3" } } let planets: [Planet] = [PlanetAlderaan(), PlanetCoruscant(), PlanetTatooine()] let names = planets. map { (planet: Planet) -> String in let visitor = NameVisitor() planet.accept(visitor) return visitor.name } print(names) let volumes = planets.map { (planet: Planet) -> String in let visitor = VolumeVisitor() planet.accept(visitor) return visitor.volume } print(volumes)
Тут все то же самое, что и в примере из книги, за исключением выбора функции для определенного класса. В первом примере мы проверяли принадлежность к тому или иному классу и строили модель поведения исходя из результата этой проверки. А во втором примере вступает в дело особенность языка Swift. Т.к. протокол PlanetVisitor определяет три метода с одним и тем же именем, но разными типами параметров, нам не нужно делать проверку на тип параметра в методе visit — Swift сам выберет нужную функцию. Т.е. при выполнении метода map над элементами массива планет каждая планета передается в замыкание, которые мы пишем после. Для определения имени планеты мы использовали NameVisitor, а для определения ее размера — VolumeVisitor. Но делаем одно и тоже — создаем экземпляр визитера, вызываем метод accept и возвращаем нужное значение.
—
Вконтакте
Google+
Одноклассники
Что такое F-паттерн?
Чтение в онлайн-режиме совершенно непохоже на традиционное чтение бумажных источников. В интернете пользователи находят нужную информацию и в первую очередь пробегают по ней глазами. То есть в виртуальной среде люди быстро просматривают материал, выхватывая лишь важные для себя моменты. Так что давайте разберемся, что это за особенность движения человеческого глаза и важна ли она для ведения интернет-ресурса.
F-паттерн: что это и как работает?
F-паттерн – это своеобразный шаблон, по которому человеческий глаз скользит по наполнению сайта. Происходит термин от английского слова «fast» — быстро. Именно так посетители просматривают контент. Всего за пару секунд глаза пробегают по странице, выхватывая лишь ключевые, важные моменты.
Шаблон был выведен после продолжительных исследований NNGroup, которая отслеживала и изучала траекторию движения человеческого глаза по странице в интернете. В эксперименте участвовало более двухсот человек, которые просмотрели в общей сложности около 1000 сайтов. В итоге, несмотря на различные цели и задачи, у всех пользователей поведение чтения было приблизительно одинаковым.
Шаблон скольжения взгляда имеет сходство с буквой «F» и характеризуется такими особенностями:
- В первую очередь посетители просматривают контент по горизонтали, уделяя особое внимание информации сверху. Это похоже на верхнюю перекладину буквы «F».
- Далее происходит движение глаза вниз по левой стороне экрана. В это время посетитель ищет ключевые моменты в начале каждого абзаца или строки, которые бы заинтересовали его. Если что-то привлекает внимание, то чтение продолжается уже осознанно и внимательно, но занимает меньшую область по сравнению с предыдущим направлением. Это похоже на нижнюю перекладину буквы «F».
- Затем посетители пробегают взглядом контент по вертикали с левой стороны, но, как правило, не задерживаются.
Тем не менее такой шаблон читательского поведения в интернете не является единственным. Структура из трех шагов может дополняться, если пользователь находит что-то интересное в вертикальном ряду и продолжает внимательное чтение в горизонтальном направлении.
Чем объясняется F-паттерн?
Обычно люди просматривают интернет-контент по F-образному принципу в таких случаях:
- На странице или ее разделе размещен текстовый контент, который не соответствует читательским привычкам в интернет-среде. Например, под такой критерий подпадают «простыни текста» без подзаголовков, абзацев, выделений шрифтом и списков.
- Посетитель ресурса старается максимально быстро и эффективно просмотреть контент, чтобы не затрачивать много времени и не забивать голову лишней информацией.
- Пользователя не удалось заинтересовать контентом, так что он не готов читать полностью все.
Последние 2 нюанса в большинстве своем присущи многим интернет-пользователям. Практически все посетители хотят отделаться малой кровью, чтобы решить задачу в кратчайшие сроки и без особых усилий. Они посещают ресурс, чтобы найти ответ на свой вопрос в считанные секунды, а не перечитывать километры текстов в поисках нужного.
Если веб-дизайнеры под контролем автора/создателя сайта не помогли пользователю найти главное, не выделили основную информацию, не разбили текст на читабельные фрагменты, то посетитель пойдет собственным путем. Без специальных сигнальных указателей он будет просматривать контент по упрощенному маршруту, концентрируясь в основном на тех местах, где началось чтение. Как можно догадаться, это левый верхний угол.
F-паттерн – это бессознательное поведение. Человек следует определенным маршрутом при чтении источников в интернете, потому что привык видеть важную информацию выше той, что менее значима. Так что сам пользователь не понимает, что знакомиться с контентом по шаблону буквы «F», но такое поведение является объяснимым и характерным для многих.
Использование F-паттерна
F-шаблон помогает сделать дизайн с грамотным разделением контента, который легко воспринимается и привлекает внимание посетителей. Для большинства жителей Европы такое построение очень удобно, поскольку чтение сверху вниз и слева направо им привычно.
Если учитывать F-паттерн при разработке и наполнении новостных блогов и сайтов, где большую часть контента составляет текст, то читать такой ресурс будет в несколько раз легче. Так вы сможете расположить главный посыл статей в местах наибольшей концентрации читательского внимания.
Вот несколько советов, которые помогут использовать F-образный шаблон в своих целях:
1. Расставьте приоритеты
Перед тем, как наполнять свой сайт, определите самую важную информацию, основные детали. Чтобы они первыми бросались в глаза аудитории, нужно расположить их в тех точках, где концентрация внимания посетителей выше всего.
2. Подогрейте интерес аудитории
Самый главный контент рекомендуется размещать в самом верху страницы, прямо под названием. Именно в этом месте люди задерживаются для внимательного чтения и ознакомления с темой статьи или ресурса в целом. Буквально в течение нескольких секунд пользователь может составить свое мнение: интересен ли ему предложенный контент и стоит ли продолжать его читать.
3. Имейте в виду поверхностный просмотр, а не тщательное чтение
Чтобы создать отличный дизайн, продумайте расположение контента:
- Начало абзаца должно быть интересным и интригующим, чтобы привлечь внимание посетителя, и он продолжил чтение.
- Люди воспринимают в первую очередь те части контента, которые визуально выделяются на фоне остальных. Это может быть жирный шрифт, курсив, контрастный цвет или размер букв. Так что делайте акцент на весомой информации с помощью инфографики, картинок или шрифта.
- Каждый абзац – это отдельная тема. Так текст воспринимается проще и понятнее. Обязательно используйте маркированные списки.
- Располагайте важную информацию, в том числе и кнопки призыва к действию, справа или слева. Именно там пользователь начинает и заканчивает чтение, так что концентрирует свое внимание больше всего. Эффективность контента, расположенного в таких местах, выше.
4. Применяйте боковую панель
Боковая панель используется для привлечения внимания к более серьезным и глубоким вопросам. Несколько полезных рекомендаций:
- Если вам нужно дополнить основной контент информацией, которая не подходит к основной статье, смело размещайте ее на боковой панели. Это может быть реклама, дополнительные статьи по теме или ссылки на социальные сети.
- Сделайте этот инструмент своеобразным поисковиком, который поможет пользователям добраться до чего-то нужного или интересного. Например, список самых популярных статей, разделы сайта и прочее.
5. Откажитесь от повторения одного и того же макета
F-паттерн довольно скучен и монотонен. Если использовать этот принцип просмотра контента постоянно, он может быстро надоесть пользователям, они будут знать, чего от вас ожидать. Поэкспериментируйте и попробуйте поставить какой-то важный элемент в нетипичном месте, чтобы заинтересовать, привлечь внимание и заинтриговать.
Такое разрушение ожиданий пойдет вам на руку, если тексты слишком длинные и вертикальные переходы с неинтересным и малополезным контентом чересчур растянуты. Разбавьте их яркой деталью, чтобы не дать читателю заскучать.
Как F-паттерн влияет на бизнес?
Веб-дизайн сайта, его лаконичное и простое оформление помогают увеличивать конверсию. Если при разработке, создании и наполнении ресурса руководствоваться проверенным F-образным шаблоном, то можно привлечь к себе большое количество новых посетителей и клиентов, потому что вы будете знать, как грамотно выстроить контент для удержания внимания. Так, традиционный сайт, основанный на F-паттерне, имеет следующие основные компоненты:
- В верхнем левом углу обычно находится логотип компании, который делает акцент на бренде и его названии.
- В правом верхнем углу чаще всего размещают контакты с номером телефона и адресом, чтобы посетитель мог быстро связаться с представителем компании.
- Контент, опубликованный в соответствии с «горячими» точками F-паттерна.
Зная точно, каким образом посетитель воспринимает информацию в интернете, вы можете этим умело пользоваться и продвигать свой бренд. Достаточно просто располагать ключевые моменты контента в тех местах, где глаз человека удерживается дольше обычного. Вот увидите, количество просмотров и целевых действий будет в разы больше.
> Создать сайт
Посетитель
Задача
Представьте, что ваша команда разрабатывает приложение, которое работает с географической информацией, структурированной в виде одного колоссального графика. Каждый узел графа может представлять сложную сущность, такую как город, но также и более детализированные объекты, такие как отрасли, достопримечательности и т. Д. Узлы связаны с другими, если между реальными объектами, которые они представляют, есть дорога. Под капотом каждый тип узла представлен своим собственным классом, а каждый конкретный узел является объектом.
Экспорт графика в XML.
В какой-то момент у вас появилась задача реализовать экспорт графика в формат XML. Сначала работа казалась довольно простой. Вы планировали добавить метод экспорта к каждому классу узла, а затем использовать рекурсию, чтобы пройти по каждому узлу графа, выполняя метод экспорта. Решение было простым и элегантным: благодаря полиморфизму вы не связали код, который вызывал метод экспорта, с конкретными классами узлов.
К сожалению, системный архитектор отказал вам в изменении существующих классов узлов.Он сказал, что код уже находится в разработке и не хочет рисковать, взломав его из-за потенциальной ошибки в ваших изменениях.
Метод экспорта XML должен был быть добавлен во все классы узлов, что несло риск поломки всего приложения, если какие-либо ошибки проскользнули вместе с изменением.
Кроме того, он спросил, имеет ли смысл иметь код экспорта XML в классах узлов. Основная задача этих классов заключалась в работе с геоданными. Здесь поведение экспорта XML выглядело бы чуждым.
Была и другая причина отказа. Весьма вероятно, что после того, как эта функция будет реализована, кто-то из отдела маркетинга попросит вас предоставить возможность экспорта в другой формат или запросит другие странные вещи. Это заставит вас снова изменить эти драгоценные и хрупкие классы.
Решение
Паттерн «Посетитель» предполагает, что вы поместите новое поведение в отдельный класс с именем посетитель , вместо того, чтобы пытаться интегрировать его в существующие классы.Исходный объект, который должен был выполнять такое поведение, теперь передается одному из методов посетителя в качестве аргумента, обеспечивая доступ метода ко всем необходимым данным, содержащимся в объекте.
А что, если это поведение может выполняться над объектами разных классов? Например, в нашем случае с экспортом XML фактическая реализация, вероятно, будет немного отличаться для разных классов узлов. Таким образом, класс посетителя может определять не один, а набор методов, каждый из которых может принимать аргументы разных типов, например:
класс ExportVisitor реализует Visitor
метод doForCity (City c) {...}
метод doForIndustry (Industry f) {...}
метод doForSightSeeing (SightSeeing ss) {...}
// . ..
Но как именно мы будем называть эти методы, особенно когда имеем дело со всем графом? У этих методов разные сигнатуры, поэтому мы не можем использовать полиморфизм. Чтобы выбрать подходящий метод посетителя, способный обрабатывать данный объект, нам нужно проверить его класс. Разве это не похоже на кошмар?
foreach (узел узла в графе)
if (экземпляр узла City)
exportVisitor.doForCity (узел (Город))
if (экземпляр узла отрасли)
exportVisitor.doForIndustry (узел (Промышленность))
// ...
}
Вы можете спросить, почему мы не используем перегрузку методов? Это когда вы даете всем методам одно и то же имя, даже если они поддерживают разные наборы параметров. К сожалению, даже если предположить, что наш язык программирования вообще поддерживает это (как это делают Java и C #), это нам не поможет. Поскольку точный класс объекта узла заранее неизвестен, механизм перегрузки не сможет определить правильный метод для выполнения. По умолчанию используется метод, который принимает объект базового класса Node
.
Однако шаблон Visitor решает эту проблему. Он использует метод под названием Double Dispatch, который помогает выполнить правильный метод для объекта без громоздких условных выражений. Вместо того, чтобы позволить клиенту выбрать подходящую версию метода для вызова, как насчет того, чтобы делегировать этот выбор объектам, которые мы передаем посетителю в качестве аргумента? Поскольку объекты знают свои собственные классы, они смогут легче выбрать подходящий метод для посетителя.Они «принимают» посетителя и говорят ему, какой способ посещения нужно выполнить.
// Код клиента
foreach (узел узла на графике)
node.accept (exportVisitor)
// Город
класс Сити
метод accept (Посетитель v)
v.doForCity (это)
// ...
// Промышленность
класс Промышленность
метод accept (Посетитель v)
v.doForIndustry (это)
// ...
Каюсь. В конце концов, нам пришлось изменить классы узлов. Но, по крайней мере, это изменение тривиально, и оно позволяет нам добавлять новые варианты поведения без повторного изменения кода.
Теперь, если мы извлечем общий интерфейс для всех посетителей, все существующие узлы смогут работать с любым посетителем, которого вы вводите в приложение. Если вы обнаружите, что вводите новое поведение, связанное с узлами, все, что вам нужно сделать, это реализовать новый класс посетителей.
Псевдокод
В этом примере шаблон Visitor добавляет поддержку экспорта XML в иерархию классов геометрических фигур.
Экспорт различных типов объектов в формат XML через объект посетителя.
// Интерфейс элемента объявляет метод accept, который принимает
// базовый интерфейс посетителя в качестве аргумента.
форма интерфейса
метод move (x, y)
метод draw ()
метод accept (v: Посетитель)
// Каждый конкретный класс элемента должен реализовывать `accept`
// метод таким образом, что он вызывает метод посетителя, который
// соответствует классу элемента.
класс Dot реализует форму
// ...
// Обратите внимание, что мы вызываем `visitDot`, который соответствует
// имя текущего класса.Таким образом мы сообщаем посетителю
// класс элемента, с которым он работает.
метод accept (v: Посетитель) равен
v.visitDot (это)
класс Circle реализует Shape is
// ...
метод accept (v: Посетитель) равен
v.visitCircle (это)
класс Rectangle реализует форму
// ...
метод accept (v: Посетитель) равен
v.visitRectangle (это)
класс CompoundShape реализует форму
// ...
метод accept (v: Посетитель) равен
v.visitCompoundShape (это)
// Интерфейс посетителя объявляет набор методов посещения, которые
// соответствуют классам элементов.Подпись посещения
// метод позволяет посетителю идентифицировать точный класс
// элемент, с которым он имеет дело.
интерфейс Посетитель
метод visitDot (d: Dot)
метод visitCircle (c: Circle)
метод visitRectangle (r: прямоугольник)
метод visitCompoundShape (cs: CompoundShape)
// Конкретные посетители реализуют несколько версий одного и того же
// алгоритм, который может работать со всеми конкретными классами элементов.
//
// Вы можете ощутить наибольшее преимущество паттерна Посетитель
// при использовании со сложной структурой объекта, такой как
// Составное дерево.В этом случае может быть полезно хранить
// некоторое промежуточное состояние алгоритма при выполнении
// методы посетителя над различными объектами структуры.
класс XMLExportVisitor реализует Visitor
метод visitDot (d: Dot) равен
// Экспорт идентификатора точки и координат центра.
метод visitCircle (c: Circle) равен
// Экспортируем ID круга, координаты центра и
// радиус.
метод visitRectangle (r: Rectangle) равен
// Экспорт идентификатора прямоугольника, левых верхних координат,
// ширина и высота.метод visitCompoundShape (cs: CompoundShape) является
// Экспортируем идентификатор фигуры, а также список ее
// детские идентификаторы.
// Клиентский код может запускать операции посетителя над любым набором
// элементы без определения их конкретных классов. В
// операция приема направляет вызов соответствующей операции
// в объекте посетителя.
класс Application - это
field allShapes: массив фигур
метод export () есть
exportVisitor = новый XMLExportVisitor ()
foreach (форма во всех формах) do
форма.принять (экспорт посетитель)
Если вам интересно, зачем нам нужен метод accept
в этом примере, моя статья Visitor and Double Dispatch рассматривает этот вопрос подробно.
Расшифровка шаблонов дизайна посетителей
| Джонни Фокс | Factory Mind
Сегодня мы рассмотрим один из менее известных паттернов, который часто не используется и поэтому более интересен при правильном использовании.
Все примеры написаны на Typescript … Даже если вы этого не знаете, обратите внимание, что я выбрал этот язык, потому что он добавляет дополнительную статическую типизацию в Javascript, предоставляя набор примеров, легко переводимых на нетипизированные языки (с соответствующим расположением 😄 )
Это один из шаблонов Банды четырех (GoF — см. Здесь), и, в частности, это «шаблон поведения », который позволяет отделить алгоритм от структуры объекта, на которой он работает. .Это означает, что мы можем добавлять новые операции в эту структуру объекта (даже список разнородных классов) , не изменяя ее .
Давайте попробуем описать простой пример из реальной жизни (вы можете поиграть с ним — см. Это репозиторий Github, написанный с использованием Typescript), где у нас есть список объектов Employee
и Clerk
, над которыми нам нужно выполнить два действия ( и, возможно, другие в будущем):
- увеличить доход : увеличить доход
сотрудника / клерка
на 30% и 10% соответственно - увеличить количество дней отпуска : увеличить отпуск на 3 дня для сотрудника
Clerk
Сначала мы объявляем интерфейс IVisitor
, который предоставляет метод visit : Этот метод будет соответствующим образом определен двумя реализациями посетителя относительно действий выше, т. Е. IncomeVisitor
и VacationVisitor
.
В этом случае мы отличаем тип Clerk
от типа Employee
с помощью простого if (элемент instance of MyType)
, выполняющего операции в соответствии с указанными выше спецификациями: обратите внимание, что это упрощенный вариант посетителя шаблон, который может (или должен 😏) предоставлять отдельные методы для разных типов, см. вторую реализацию здесь.
Давайте продолжим, добавив вторую часть шаблона:
Мы объявляем новый интерфейс IVisitable
, который предоставляет метод accept : этот метод позволяет нам «получать» экземпляр посетителя и соответствующим образом выполнять действие с заинтересованным лицом. элемент.
Это позволяет нам указать различное поведение для различных типов объектов / структур: в данном случае мы указываем, что в случае списка сотрудников метод accept должен выполняться для каждого компонент списка .
Вот простой пример, включающий весь код, который мы видели до сих пор:
Образец посетителя можно резюмировать следующим образом:
Цель
- Отделите алгоритмы от структуры объекта, на которой они работают.Это позволяет нам добавлять новые операции в эту структуру объекта, не изменяя ее.
Elements
- Интерфейс IVisitor , предоставляющий метод visit (для каждого посещаемого класса)
- Интерфейс IVisitable , обеспечивающий метод accept , реализуется каждым доступным классом
Pros
- Добавление функций в библиотеки классов, для которых у вас либо нет источника, либо вы не можете его изменить
- Соберите данные из разрозненной коллекции несвязанных классов и используйте их для представления результатов глобальный расчет в пользовательской программе
- Соберите связанные операции в один класс, а не заставляйте вас изменять или извлекать классы для добавления этих операций
- Сотрудничать с шаблоном Composite
Cons
- Для добавления или удаления посещаемых объектов требуется вам обновить всех посетителей
- «Посещенные классы »Должно быть« стабильным »
UML
Отсюда
Мы можем резюмировать шаблон посетителя следующим предложением:
Реализуйте столько классов посетителей, сколько действий, которые должны быть выполнены для набора объектов, указав Метод принимает каждый экземпляр посетителя для каждого типа объекта.
Развлекайтесь и не забудьте порекомендовать статью, если она вам понравилась 👏!
Шаблоны проектирования C #: шаблон посетителя
В моей последней статье были представлены выражения на C # и немного объяснялось, почему они были полезны. Сегодня я собираюсь развить эту статью и познакомить вас с шаблоном посетителя, который часто используется при работе с выражениями. Если вы не знакомы с выражениями или немного устали, я бы рекомендовал сначала прочитать предыдущую статью.
Образец посетителя
Википедия определяет шаблон посетителя как «шаблон дизайна посетителя — это способ отделения алгоритма от структуры объекта, на которой он работает».
Что это значит? Когда я обычно определяю классы на C #, они выглядят примерно так.
Внутри своего класса я определяю свои данные и свои методы или алгоритмы, которые работают с моим классом.
Но если вы используете шаблон посетителя, вы разделяете класс и методы на что-то вроде этого.
Теперь данные и методы определены отдельно, их разделение имеет преимущества, которые мы вскоре увидим.
В качестве примечания, разделение данных и методов в этом примере на самом деле ничего для вас не дает, к тому же я не использую терминологию шаблонов посетителей, но мы рассмотрим это позже.
Печать выражений
Следующий код основан на примере, приведенном в этой ссылке на статью в Википедии.
Давайте продемонстрируем этот шаблон, определив выражения для представления простых математических уравнений и посетитель, чтобы распечатать наши уравнения.
Чтобы начать с нашего шаблона, я собираюсь определить два интерфейса. IExpressionVisitor
определяет, какие типы выражений поддерживает наш посетитель. Все, что наследуется от IExpression
, является объектами, которые будут «посещены». Accept
метод — это стандартное имя метода, позволяющего принять посетителя в класс IExpression
.
Теперь давайте определим наше первое выражение под названием Literal. Ответственность за сохранение числовых значений в наших уравнениях.
Вы заметите, что он наследуется от IExpression
и реализует метод Accept
, все что он делает, он передает себя посетителю с помощью ключевого слова this
.
Далее мне понадобится выражение Addition
, определенное следующим образом.
Я определяю два выражения IExpression
, Left и Right, как два выражения, которые будут складываться вместе.
Теперь, когда у меня есть два выражения, мне понадобится посетитель, который сможет посетить эти выражения и распечатать уравнение.
Позвоним нашему посетителю InfixExpressionPrinter
и дадим ему следующую реализацию.
У этого есть два метода обработки обоих моих типов выражений. Если это Literal
, все, что мне нужно, — это распечатать значение.Если это Addition
, оно генерирует значения выражений Left и Right и складывает их вместе.
Теперь давайте сложим все это вместе, чтобы распечатать наше выражение.
Я создаю простое уравнение сложения, вызывая метод Accept.
просматривает все выражение и распечатывает уравнение, представленное нашим деревом выражений. Выполнив это, вы напечатаете (5 + 6)
, что отражает уравнение, которое мы построили с помощью выражений.
Мы также можем распечатать более сложные выражения, например следующие.
Выполнение этого вывода (((5 + 6) +20) + (1 + 3))
, также математическое представление нашего выражения.
Проницательный читатель заметит, что наш принтер выражений печатает уравнения в инфиксной нотации. Что, если бы мы хотели напечатать префикс? Или Postfix?
Это действительно сила паттерна посетителей. Поскольку логика печати не содержится в наших классах выражений, мы можем добавить еще один принтер, если мы наследуем от метода IExpressionVisitor
.
Позволяет определить принтер для записи префикса.
Когда второй пример запускается с использованием второго уравнения, результат будет + + + 5 6 20 + 1 3
. Создание нового посетителя позволяет нам преобразовать одно и то же выражение в другой результат.
Преимущества шаблона посетителя
Как вы можете видеть в наших примерах, для печати различных форм уравнений не требуется никаких изменений в наших классах Literal
или Addition
. Шаблон посетителя позволяет другому классу определять, как печатать выражения, и передает эту ответственность тем, кто реализует интерфейс IExpressionVisitor
.
Это довольно простой пример шаблона посетителя. Если вы хотите увидеть корпоративный пример этого шаблона, просмотрите базу кода Entity Framework Core. Он использует этот шаблон (и многое другое), чтобы превратить оператор Linq в специальный запрос к базе данных.
Вместо простых классов, таких как Literal
, Addition
, InfixExpressionPrinter
и PrefixExpressionPrinter
, у них есть сложные классы, такие как SqlServerQuerySqlGenerator, который содержит этот метод:
Этот метод очень похож на примеры, которые мы создали в этой статье.
Целые проекты SQL посвящены преобразованию выражений в каждое конкретное хранилище данных. Будь то SQL Server, Sqlite, Oracle, Postgres или другие.
Entity Framework Core, очевидно, имеет много сложностей, но, как и большинство другого программного обеспечения, оно построено на принципах или шаблонах, с которыми большинство из нас может ознакомиться. Шаблон посетителя является одним из таких шаблонов.
Заключительные мысли
Я слышал, что шаблоны проектирования — это «повторное использование опыта».Как отрасль, мы сталкиваемся с очень распространенными проблемами в различных технологиях и сферах бизнеса. Существуют шаблоны для решения тех проблем, которые были порождены как болью, так и успехами в прошлом.
Знание шаблонов проектирования помогает нам научиться избегать повторения тех же ошибок. Шаблоны дизайна — отличные инструменты в наборе инструментов для разработчиков, и я рад сегодня рассказать об одном из них.
Библиотека Github
объектно-ориентированный — шаблон посетителя: в чем смысл метода accept?
Но я действительно не вижу смысла в этом приеме звонка. Если у вас есть посетитель и объекты, которые нужно посетить, почему бы просто не передать эти объекты непосредственно посетителю и избежать косвенного обращения?
Кристоф отвечает по существу, я просто хочу остановиться на этом подробнее. Незнание типа среды выполнения объекта на самом деле является предположением о шаблоне посетителя.
Вы можете понять эту закономерность двумя способами. Во-первых, это уловка для выполнения множественной отправки на языке единой отправки. Во-вторых, это способ создания абстрактных типов данных на языках ООП.Позволь мне объяснить.
Видите ли, есть два основных подхода к абстракции данных 1 . ООП достигает этого, абстрагируя вызовы процедур. Например, вы фактически указываете абстрактную операцию, когда делаете вызов (вы указываете «сообщение»), а фактическая функция, которую вы вызываете, разрешается каким-то базовым механизмом. Этот базовый механизм позволяет объектам реагировать на определенный интерфейс (набор общедоступных методов / сообщений), что упрощает добавление новых представлений (путем создания подклассов), но затрудняет добавление новых операций. Обратите внимание, что при использовании такого рода полиморфизма, в то время как код, который создает объекты, знает конкретные типы, другой клиентский код написан в терминах абстрактного типа (и в случае ООП, это конкретно означает в терминах интерфейса, определенного этим абстрактный тип).
Другой подход — это абстрактные типы данных (ADT), где конечный набор представлений (конкретных типов данных) абстрагируется и обрабатывается как один тип данных. В отличие от ООП, вы сейчас вызываете конкретные функции, но передаете абстракцию данных.То есть тип параметра никогда не бывает конкретным, и клиентский код никогда не работает с конкретными представлениями и не знает их (кроме строительных площадок, но то же самое верно и для ООП). Существует базовый механизм, который позволяет функциям идентифицировать (или сопоставлять) конкретный тип, и каждая операция должна поддерживать все представления (или, с точки зрения шаблона посетителя, каждый конкретный посетитель должен обрабатывать все типы элементов). В простейшей форме это что-то вроде оператора switch, на функциональных языках он проявляется как сопоставление с образцом, а в шаблоне посетителя он закодирован в абстрактном интерфейсе посетителя (абстрактный метод посещения для каждого возможного типа элемента), который каждая производная должна поддерживать в осмысленно.Компромиссы для такого типа абстракции данных — наоборот: легко добавлять новые операции, но сложно добавлять новые представления (новые типы элементов).
Итак, имея это в виду, шаблон Visitor хорош для сценариев, в которых можно ожидать, что операции будут меняться чаще по сравнению с представлениями, т. Е. Сценарии, в которых ожидается, что количество различных типов элементов будет конечным и относительно стабильным.
Я заметил, что вы связались со страницей «Ремесленные интерпретаторы: шаблон посетителя».Пример использования демонстрирует эту идею — основная структура данных представляет собой дерево выражений, которое состоит из узлов, которые могут быть представлены по-разному (имеют разные типы данных). Существует конечное число представлений (определяемых правилами языка), но все они свернуты в абстрактный тип данных, представляющий дерево выражений ( Expr
). Затем вы можете определить количество конкретных посетителей, представляющих различные обобщенные операции, которые можно применить к этому дереву.Внешний (обращенный к клиенту) интерфейс каждого посетителя использует только абстрактный тип Expr
, который затем позволяет писать клиентский код только в терминах этой абстракции (т. Е. Клиентскому коду не обязательно знать конкретные типы каждого посетителя. node, просто это дерево выражений и что к нему можно применить ряд операций). Я знаю, что приведенные здесь примеры создают дерево прямо перед его использованием, но более реалистичный сценарий — это чтение некоторого кода из файла и возврат абстрактного синтаксического дерева.
Интересно, что в этой статье шаблон Visitor реализован в обратном порядке; их пример кода клиента:
новый AstPrinter (). Print (выражение)
, тогда как должно быть:
expression.accept (новый AstPrinter ())
, поскольку AstPrinter является «посещающей» операцией (но тогда метод извлечения результата из AstPrinter будет другим).
Если вы считаете, что название accept / visit сбивает с толку, вы можете мысленно переименовать эти методы:
элемент.принять (посетитель)
// можно увидеть как:
abstractType.do (операция)
и
visitor.visit (это)
// можно увидеть как:
operation.applyTo (конкретный тип)
Важно понимать, что интерфейс Visitor (различные перегрузки visit
) предназначен для обработки как внутренних для абстракции типа (другими словами, они предназначены для (1) внутреннего вызова конкретными элементами и (2) должны быть реализованы производными от посетителей; они , а не , предназначенные для использования клиентским кодом).
1 Эти два подхода предполагают разные компромиссы; в сообществе CS это известно как «проблема выражения».
Шаблон посетителя (представляют операцию со структурой объекта)
Шаблон посетителей
Шаблон Visitor обеспечивает удобный и простой способ представления операции, выполняемой над элементами структуры объекта.
Шаблон «Посетитель» позволяет определить новую операцию, не изменяя классы элементов, с которыми она работает.На рисунке ниже показан шаблон «Посетитель».
Шаблон
Посетитель позволяет определить новую операцию, не изменяя классы элементов, с которыми она работает.
Преимущества:
Ниже перечислены преимущества использования шаблона «Посетитель»:
- Облегчает добавление новых операций
- Собирает связанные операции и отделяет несвязанные
Когда использовать:
Вам следует использовать шаблон Visitor, когда:
- Структура объекта содержит множество классов объектов с разными интерфейсами, и вы хотите выполнять операции с этими объектами, которые зависят от их конкретных классов.
- Классы, определяющие структуру объекта, редко меняются, но вы часто хотите определить новые операции над структурой.
Теория паттерна посетителей
Шаблон «Посетитель» полезен при проектировании работы с гетерогенным набором объектов иерархии классов.
Шаблон Visitor позволяет определять операцию без изменения класса любого из объектов в коллекции. Для этого шаблон Посетитель предлагает определить операцию в
отдельный класс, называемый классом посетителей.Это отделяет операцию от коллекции объектов, с которой она работает. Для каждой новой операции, которая должна быть определена, создается новый класс посетителей.
Поскольку операция должна выполняться над набором объектов, посетителю необходим способ доступа к открытым членам этих объектов.
Это требование можно удовлетворить, реализовав следующие две дизайнерские идеи.
Идея дизайна 1
Каждый класс посетителя, который работает с объектами одного и того же набора классов, может быть разработан для реализации соответствующего интерфейса VisitorInterface. Типичный VisitorInterface объявляет набор методов посещения (ObjectType), по одному для каждого типа объекта из коллекции объектов. Каждый из этих методов предназначен для обработки экземпляров определенного класса.
Например, если коллекция объектов состоит из объектов ClassA и ClassB, то интерфейс VisitorInterface объявляет следующие два метода:
посещение (ClassA objClassA)
для обработки объектов ClassA.
посещение (ClassB objClassB)
для обработки объектов ClassB.
Каждый объект из коллекции объектов вызывает метод соответствующего посещения (ObjectType), передавая себя в качестве аргумента.
Типичный исполнитель (посетитель) VisitorInterface может получить доступ к информации, необходимой для операции, для которой он разработан, путем доступа к открытым членам (методам и атрибутам) экземпляра объекта, переданного ему через вызов метода посещения.
Идея дизайна 2
Классы объектов из коллекции объектов должны определять метод:
accept (посетитель)
Клиент, заинтересованный в выполнении операции посетителя, должен:
- Создайте экземпляр разработчика (посетителя) интерфейса VisitorInterface, который предназначен для выполнения требуемой операции.
- Создайте коллекцию объектов и вызовите метод accept (посетителя) для каждого члена коллекции объектов, передав экземпляр посетителя, созданный выше.
В рамках реализации метода accept (visitor) каждый объект в коллекции объектов вызывает метод visit (ObjectType) для экземпляра посетителя.
Внутри метода visit (ObjectType) посетитель собирает необходимые данные из коллекции объектов для выполнения операции, для которой он предназначен.
Теорию паттернов посетителей и примеры кода можно найти по следующей ссылке. Код шаблона посетителя
Объяснение шаблона посетителя | Блог Мански
На последнем собеседовании я получил (довольно расплывчатый) вопрос об обходе дерева и работе с узлами дерева. Я думаю, что у меня большой опыт программирования, но я не мог найти ответ самостоятельно. Ответ, который хотел услышать парень, был: шаблон посетителя .
Я никогда не слышал об этом раньше. Итак, готовясь к следующему собеседованию, я подумал, что взгляну на него.
Пытаясь понять это, я наткнулся на эту цитату:
Шаблон Visitor, возможно, самый сложный шаблон дизайна, с которым вы столкнетесь. (Источник)
Полностью согласен. (И, вероятно, поэтому я никогда раньше не слышал и не использовал его.)
Но так как я не лодырь, я пошел и приручил его. Итак, в этой статье я собираюсь пролить свет на этот загадочный паттерн дизайна.
Определение ∞
Основная проблема (на мой взгляд) шаблона посетителя в том, что часто не совсем понятно, что он делает.Итак, начнем со следующего определения (на основе Википедии):
Шаблон проектирования посетителя — это способ отделения операции от структуры объекта, с которой она работает. […] По сути, этот шаблон позволяет добавлять новые виртуальные функции к семейству классов без изменения самих классов;
Мотивация ∞
Давайте разберемся, что это значит. Я приведу несколько базовых примеров с возрастающей сложностью, чтобы проиллюстрировать мотивацию шаблона посетителя.
Примечание: Все примеры написаны на C #.
Структура объекта ∞
В приведенном выше определении говорится, что шаблон посетителя отделяет «операцию от структуры объекта ». Итак, давайте определим эту структуру объекта.
Предположим, у нас есть какой-то парсер викикода. Викикод позволяет нам писать документы в виде обычного текста, но обогащать их некоторым форматированием (жирным шрифтом) и гиперссылками.
Итак, у нас есть классы для обычного текста, полужирного текста и гиперссылок:
открытый абстрактный класс DocumentPart { общедоступная строка Text {получить; частный набор; } } открытый класс PlainText: DocumentPart {} открытый класс BoldText: DocumentPart {} открытый класс Hyperlink: DocumentPart { общедоступная строка Url {получить; частный набор; } }
И у нас есть класс документа:
Public class Document { частный списокm_parts; }
Сценарий 1.
Преобразование в HTML ∞
Теперь предположим, что мы хотим преобразовать этот документ в HTML.
Самый простой способ — добавить виртуальный метод
с именем ToHTML ()
в DocumentPart
, например:
открытый абстрактный класс DocumentPart { общедоступная строка Text {получить; частный набор; } публичная абстрактная строка ToHTML (); } открытый класс PlainText: DocumentPart { общедоступная строка переопределения ToHTML () { вернуть this.Text; } } public class BoldText: DocumentPart { общедоступная строка переопределения ToHTML () { вернуть "" + это.Текст + ""; } } открытый класс Hyperlink: DocumentPart { общедоступная строка Url {получить; частный набор; } общедоступная строка переопределения ToHTML () { вернуть "" + this.Text + ""; } }
И класс Document
также получит метод ToHTML ()
:
Public class Document { частный списокm_parts; публичная строка ToHTML () { строка output = ""; foreach (часть DocumentPart в этом. m_parts) { вывод + = part.ToHTML (); } возвратный вывод; } }
Затем, вызвав Document.ToHTML ()
, можно было преобразовать весь документ в HTML.
Сценарий 2: разные форматы вывода ∞
Давайте добавим сложности. В дополнение к предыдущему сценарию мы теперь также хотим разрешить преобразование в обычный текст и LaTeX.
Наивным способом было бы предоставить реализации для каждого формата вывода:
открытый абстрактный класс DocumentPart { общедоступная строка Text {получить; частный набор; } публичная абстрактная строка ToHTML (); публичная абстрактная строка ToPlainText (); публичная абстрактная строка ToLatex (); } открытый класс PlainText: DocumentPart { общедоступная строка переопределения ToHTML () { верни это.Текст; } публичная переопределенная строка ToPlainText () { вернуть this.Text; } общедоступная строка переопределения ToLatex () { вернуть this.Text; } } public class BoldText: DocumentPart { общедоступная строка переопределения ToHTML () { return "" + this. Text + ""; } публичная переопределенная строка ToPlainText () { вернуть «**» + this.Text + «**»; } общедоступная строка переопределения ToLatex () { вернуть "\\ textbf {" + this.Text + "}"; } } открытый класс Hyperlink: DocumentPart { общедоступная строка Url {получить; частный набор; } общедоступная строка переопределения ToHTML () { вернуть " "+ this.Text +" "; } публичная переопределенная строка ToPlainText () { вернуть this.Text + "[" + this.Url + "]"; } общедоступная строка переопределения ToLatex () { return "\\ href {" + this.Url + "} {" + this.Text + "}"; } } public class Document { частный списокm_parts; публичная строка ToHTML () { строка output = ""; foreach (часть DocumentPart в this.m_parts) { вывод + = part.ToHTML (); } возвратный вывод; } публичная строка ToPlainText () { строка output = ""; foreach (часть DocumentPart в этом.m_parts) { вывод + = part.ToPlainText (); } возвратный вывод; } публичная строка ToLatex () { строка output = ""; foreach (часть DocumentPart в this. m_parts) { output + = part.ToLatex (); } возвратный вывод; } }
Эта реализация имеет две основные проблемы:
Эти проблемы можно решить с помощью шаблона посетителя .
Образец посетителя ∞
Шаблон посетителя состоит из двух частей:
Посетитель: преобразовать в HTML ∞
Начнем с посетителя .В качестве примера мы собираемся реализовать операцию «преобразовать в HTML».
Для этого нам нужно определить интерфейс с именем IVisitor
:
public interface IVisitor { недействительный визит (PlainText docPart); void Visit (BoldText docPart); недействительный визит (Hyperlink docPart); }
Затем реализуем преобразование в HTML:
public class HtmlVisitor: IVisitor { public string Output { получить {вернуть this.m_output; } } частная строка m_output = ""; public void Visit (PlainText docPart) { это.Вывод + = docPart.Text; } public void Visit (BoldText docPart) { this. m_output + = "" + docPart.Text + ""; } public void Visit (Hyperlink docPart) { this.m_output + = "" + docPart.Text + ""; } }
Доступно для просмотра: структура документа ∞
Применяя шаблон посетителя к нашим классам документов, они меняются на:
открытый абстрактный класс DocumentPart { общедоступная строка Text {получить; частный набор; } public abstract void Accept (посетитель IVisitor); } открытый класс PlainText: DocumentPart { public override void Accept (посетитель IVisitor) { посетитель.Посетите (это); } } public class BoldText: DocumentPart { public override void Accept (посетитель IVisitor) { visitor.Visit (это); } } открытый класс Hyperlink: DocumentPart { общедоступная строка Url {получить; частный набор; } public override void Accept (посетитель IVisitor) { visitor.Visit (это); } } public class Document { частный списокm_parts; public void Accept (посетитель IVisitor) { foreach (часть DocumentPart в this. m_parts) { part.Accept (посетитель); } } }
Примечание: Реализации Accept ()
кажутся идентичными для всех дочерних классов DocumentPart
.Однако мы не можем переместить код в базовый класс, потому что IVisitor
не имеет метода Visit (DocumentPart)
, а только для конкретных реализаций. (Мы могли бы решить эту проблему с помощью отражения, но потеряем проверку во время компиляции.)
Собираем все вместе ∞
Теперь, чтобы преобразовать документ в HTML, мы можем использовать этот код:
Документ doc = ...; HtmlVisitor посетитель = новый HtmlVisitor (); doc.Accept (посетитель); Console.WriteLine ("Html: \ n" + посетитель.Выход);
Чтобы преобразовать документ в LaTeX, нам нужно реализовать LatexVisitor
:
public class LatexVisitor: IVisitor { public string Output { получить {вернуть this.m_output; } } частная строка m_output = ""; public void Visit (PlainText docPart) { this. m_output + = docPart.Text; } public void Visit (BoldText docPart) { this.m_output + = "\\ textbf {" + docPart.Text + "}"; } public void Visit (Hyperlink docPart) { this.m_output + = "\\ href {" + docPart.URL + "} {" + docPart.Text + "}"; } }
Реализация фактических классов документов остается без изменений.
Боковое примечание: Если вам интересно, хорошее ли имя Accept
или нужно ли переименовать метод (например, в Convert
): проверьте, возможны ли другие операции, кроме преобразования. Например, можно реализовать BoldTextCountVisitor
или UrlExtractorVisitor
. Если такие операции возможны, вам следует придерживаться имени Accept
— это означает, что здесь используется шаблон посетителя.
Реальная проблема решается ∞
Если не в теории, можете пропустить эту часть. Это объясняет формальную проблему, которую решает шаблон посетителя: нечто, называемое двойной отправкой .
Что это?
Единая отправка ∞
Большинство (все?) Языков программирования ООП поддерживают однократную отправку , более известную как виртуальные методы . Например, рассмотрим следующий код:
космический корабль общественного класса { общедоступная виртуальная строка GetShipType () { return "SpaceShip"; } } открытый класс ApolloSpacecraft: SpaceShip { общедоступная строка переопределения GetShipType () { return "ApolloSpacecraft"; } }
Теперь выполните этот код:
SpaceShip ship = новый ApolloSpacecraft (); Консоль.WriteLine (ship.GetShipType ());
Это напечатает «ApolloSpacecraft». Фактическая реализация метода для вызова выбирается во время выполнения исключительно на основе фактического типа корабля
. Таким образом, для выбора метода используется только тип single объекта, отсюда и название single dispatch.
Примечание: «Однократная отправка» — это одна из форм «динамической отправки», т. е. метод выбирается во время выполнения. Если метод выбирается во время компиляции (верно для всех невиртуальных методов), он называется «статической отправкой».
Двойная отправка ∞
Добавим астероидов:
public class Asteroid { public virtual void CollideWith (корабль SpaceShip) { Console.WriteLine («Астероид врезался в космический корабль»); } public virtual void CollideWith (корабль ApolloSpacecraft) { Console.WriteLine («Астероид врезался в космический корабль Аполлона»); } }; public class ExplodingAsteroid: Asteroid { public override void CollideWith (корабль SpaceShip) { Console.WriteLine («Взрывающийся астероид попадает в космический корабль»); } public override void CollideWith (корабль ApolloSpacecraft) { Консоль.WriteLine («Взрывающийся астероид попал в космический корабль« Аполлон »); } };
Теперь давайте выполним еще немного кода. С:
Астероид theAsteroid = новый астероид (); ExplodingAsteroid theExplodingAsteroid = новый ExplodingAsteroid (); SpaceShip theSpaceShip = новый SpaceShip (); ApolloSpacecraft theApolloSpacecraft = новый ApolloSpacecraft ();
это код:
theAsteroid. CollideWith (космический корабль); theAsteroid.CollideWith (космический корабль Аполлона); Взрывающийся астероид.CollideWith (космический корабль); theExplodingAsteroid.CollideWith (theApolloSpacecraft);
напечатает:
Астероид врезался в космический корабль Астероид врезался в космический корабль Аполлона Взрывающийся астероид попал в космический корабль Взрывающийся астероид попал в космический корабль Аполлона
Все как ожидалось. Теперь рассмотрим этот код:
// Обратите внимание на разные типы данных! Астероид theExplodingAsteroidRef = new ExplodingAsteroid (); Космический корабль theApolloSpacecraftRef = новый ApolloSpacecraft (); theExplodingAsteroidRef.CollideWith (theApolloSpacecraftRef);
Желаемый результат здесь был бы «Взрывающийся астероид попал в космический корабль Apollo », но вместо этого мы получим «Взрывающийся астероид попал в космический корабль ».
Проблема в том, что C # (и Java, C ++,…) поддерживает только одиночную отправку, но не поддерживает двойную отправку. Выбран метод , только на основе theExplodingAsteroidRef
, но не theExplodingAsteroidRef
, и theApolloSpacecraftRef
(что было бы двойной отправкой).
Проблема, не решенная на самом деле: итераторы ∞
Многие страницы в Интернете связывают шаблон посетителя с обходом некоторой структуры данных, обычно дерева или иерархии.
Это меня полностью сбило с толку, потому что сначала я не мог понять , в чем разница между шаблоном посетителя и шаблоном итератора .
Дело в том, что основная цель шаблона посетителя — решить проблему двойной отправки. Решение шаблона итератора — это только побочный продукт.Если вы просто ищете способ итерации структуры данных, шаблон итератора может быть лучшей альтернативой.
Предположим, у вас есть этот класс:
// Список целых открытый класс MyList: IVisitable, IEnumerable { частный списокm_list; public Enumerator GetEnumerator () {. ..} // шаблон итератора public void Accept (IVisitor visitor) {...} // шаблон посетителя }
Теперь вы хотите вычислить сумму всех целых чисел в списке.
Вы можете сделать это либо с помощью шаблона итератора :
IEnumerable myList = новый MyList (...); int sum = 0; foreach (значение int в myList) { сумма + = значение; } Console.WriteLine ("Сумма:" + сумма);
Или вы можете сделать это с помощью шаблона посетителя :
IVisitable myList = новый MyList (...); Посетитель IVisitor = новый SumVisitor (); myList.Accept (посетитель); Console.WriteLine ("Сумма:" + Visitor.Sum);
Выпуски ∞
Есть некоторые проблемы (или проблемы) с шаблоном посетителя.
Порядок итераций ∞
Одна проблема — это порядок итераций.
Например, если вы определяете шаблон посетителя на дереве, итерация может быть в глубину или в ширину.
Итак, если у вас есть операция (посетитель), которая требует определенного порядка итераций, у вас может быть проблема с .
Например, вы можете реализовать шаблон посетителя для сохранения структуры данных на диск, где каждый посетитель представляет другой формат файла. Это позволит вам позже легко добавлять новые форматы файлов. Однако это работает только до тех пор, пока все форматы файлов хранят данные в том же порядке .Если для двух форматов требуются разные порядки, этот шаблон больше не работает.
Новые возможности для посещения ∞
Если вы добавляете новый объект для посещения, вам необходимо обновить каждого уже реализованного посетителя.
Давайте рассмотрим наши классы документов сверху. У нас были классы PlainText
, BoldText
и Hyperlink
.
Теперь предположим, что мы хотим добавить класс для подчеркнутого текста. Таким образом, интерфейс IVisitor
изменится на:
.
public interface IVisitor { недействительный визит (PlainText docPart); void Visit (BoldText docPart); недействительный визит (Hyperlink docPart); недействительный визит (подчеркнутый текст docPart); // добавлен }
Из-за этого изменения нам также потребуется обновить всех посетителей, которых мы уже внедрили. Мы могли бы использовать отражение, чтобы решить (или просто скрыть) проблему, но в конечном итоге шаблон посетителя лучше всего работает со структурами данных, которые не изменяют .
Доступ к частным членам ∞
Есть еще одна проблема, когда посетителям нужен доступ к личным данным посещаемых.
Чтобы придерживаться шаблона посетителя, вас могут заставить сделать эти данные общедоступными, даже если они не должны быть общедоступными, тем самым нарушив принцип сокрытия информации.
Резюме ∞
Шаблон посетителя — относительно сложный узор.
Цель проекта: Отделить операции от структур данных, над которыми они работают. В качестве приятного побочного эффекта это позволяет вам добавлять операции к структурам данных, которые вы не можете изменить (возможно, из-за того, что вы потеряли для них исходный код).
Вам нужно:
Выпущено:
Примеры из реального мира:
CodeProject
Scala и шаблон посетителя
Scala предоставляет несколько языковых функций, которые призваны упростить
для пользователей, чтобы определить и работать с
алгебраические типы данных (ADT). Вы пишете несколько классов кейсов, которые расширяют запечатанный трейт, вы пишете некоторые функции
совпадение с образцом для этих классов случаев,
и все готово: у вас есть красивый связанный список, розовое дерево или что-то еще.
Однако иногда вы не можете использовать классы case для реализации своих вариантов,
или вы не хотите помещать свои классы case в свой общедоступный API, а в этих
сопоставление с образцом ситуаций обычно гораздо менее полезно. Это сообщение в блоге о
шаблон посетителя, который
является альтернативой сопоставлению с образцом, которая обеспечивает многие из его преимуществ, а также около
использование посетителей, которые мы планируем для Circe 1.0.
Сопоставление с образцом
Для начала с относительно простого примера (без универсальных шаблонов, только два варианта), следующее было бы разумным
реализация неизменяемого связанного списка целых чисел в Scala:
запечатанный трейт IntList
case объект IntNil расширяет IntList
case class IntCons (h: Int, t: IntList) расширяет IntList
Запечатанная черта здесь — это наш тип суммы, а объект и класс дела — это наши типы продукта (или варианта).
Мы можем использовать сопоставление с образцом Scala для записи операций в этом ADT:
def sum (xs: IntList): Int = xs match {
case IntNil => 0
case IntCons (h, t) => h + sum (t)
}
Одна из приятных особенностей сопоставления с образцом для типов суммы в Scala заключается в том, что компилятор проверяет
что наши дела являются исчерпывающими.предупреждение: совпадение не может быть исчерпывающим.
Это не сработает на следующем входе: IntNil
сумма: (xs: IntList) Int
Мы можем использовать тот же подход для моделирования более сложных типов данных, таких как абстрактное синтаксическое дерево, представляющее документы JSON:
запечатанный трейт Json
case объект JsonNull расширяет Json
case class JsonBoolean (значение: Boolean) расширяет Json
case class JsonNumber (значение: BigDecimal) расширяет Json
case class JsonString (value: String) расширяет Json
case class JsonArray (value: Vector [Json]) расширяет Json
case class JsonObject (value: Vector [(String, Json)]) расширяет Json
Мы можем реализовать операции с этим ADT, используя сопоставление с образцом, как мы это сделали для суммы выше:
def countValues (json: Json): Int = json match {
case JsonNull => 0
case JsonBoolean (_) => 1
case JsonNumber (_) => 1
case JsonString (_) => 1
case JsonArray (js) => js. карта (countValues) .sum
case JsonObject (fs) => fs.map (field => countValues (field._2)). sum
}
И снова, если мы забудем один из этих случаев, компилятор нам сообщит.
Оптимизация нашего представительства🔗
К сожалению, если мы заботимся о производительности, мы быстро столкнемся с некоторыми проблемами с
это представление. Мы могли бы добавить числовой вариант для целых чисел, которые подходят
в Long
, например, поскольку BigDecimal
- дорогой способ их представления:
case class JsonLong (значение: Long) расширяет Json
Теперь мы стоим перед дизайнерским решением.Мы могли бы предложить этот вариант нашим пользователям,
или мы могли бы сделать это приватным. Тот факт, что у нас есть два способа представления JSON
числа кажутся деталью реализации, о которой нашим пользователям, вероятно, не стоит беспокоиться
about, так что есть аргумент в пользу того, чтобы сделать эти классы дел частными.
На самом деле эти решения часто даже проще. В Цирце,
например, в настоящее время у нас есть два представления объекта JSON, которые можно преобразовать в наш упрощенный
пример вроде этого:
импорт java.util.LinkedHashMap
case class LHMJsonObject (значение: LinkedHashMap [String, Json]) расширяет Json
case class MVJsonObject (ключи: Vector [String], значение: Map [String, Json]) расширяет Json
Мы можем значительно улучшить производительность, создав изменяемую связанную карту во время синтаксического анализа,
и поиск значений в LinkedHashMap
также значительно быстрее, чем неизменяемый Scala
Карта
. Однако мы хотим, чтобы наши значения Json
были неизменными, поэтому мы определенно не можем дать нашим пользователям
доступ к базовому представлению в этом случае.
Чтобы вернуться к нашей оптимизации чисел, даже если мы сделаем классы числового регистра закрытыми, мы можем
по-прежнему поддерживает сопоставление с образцом благодаря экстракторам Scala:
запечатанный трейт Json
case объект JsonNull расширяет Json
case class JsonBoolean (значение: Boolean) расширяет Json
класс частного случая JsonBigDecimalNumber (значение: BigDecimal) расширяет Json
класс частного случая JsonLongNumber (значение: Long) расширяет Json
case class JsonString (value: String) расширяет Json
case class JsonArray (value: Vector [Json]) расширяет Json
case class JsonObject (value: Vector [(String, Json)]) расширяет Json
object JsonNumber {
def unapply (json: Json): Option [BigDecimal] = json match {
case JsonBigDecimalNumber (значение) => Some (значение)
case JsonLongNumber (значение) => Некоторые (BigDecimal (значение))
case _ => Нет
}
}
Несмотря на то, что у нас больше нет класса case JsonNumber
, Scala
позволяет нам использовать JsonNumber
как случай сопоставления с образцом, потому что мы
определил метод unapply
с соответствующим типом в объекте JsonNumber
. Это означает, что мы можем записать наши countValues
метод точно так же, как и раньше, когда у нас действительно
класс корпуса JsonNumber
.
Проблема с этим подходом в том, что как только мы начинаем использовать специальные экстракторы,
мы теряем проверку на полноту, что, на мой взгляд, делает ее не запускаемой. Синтаксис сопоставления с образцом
хорошо, но в итоге возникают ошибки времени выполнения, потому что вы забыли регистр, а компилятор не сказал вам,
нет.
Стоит отметить, что Circe предоставляет экстракторы для Json
в отдельной Circe-optics.
модуль:
импорт io.circe.Json, io.circe.optics.all._
def countValues (json: Json): Int = json match {
case jsonBoolean (_) => 1
case jsonNumber (_) => 1
case jsonString (_) => 1
case jsonArray (js) => js.map (countValues) .sum
case jsonObject (fs) => fs.values.map (countValues) .sum
case _ => 0
}
Все эти экстракторы являются призмами из
Monocle, библиотека линз Scala, и пока они
не обеспечивают исчерпывающую проверку сопоставления с образцом, у них есть много других полезных функций
(которые, к сожалению, выходят за рамки этого сообщения в блоге).
«Складной» 🔗
Если вы когда-либо использовали Argonaut или Circe, вы могли знать, что, хотя они не поддерживают сопоставление с образцом в своих классах кейсов JSON AST,
они оба предоставляют метод кратного
в качестве альтернативы сопоставлению с образцом:
импорт io.circe.Json
def countValues (json: Json): Int = json.fold (
0,
_ => 1,
_ => 1,
_ => 1,
js => js.map (countValues) .sum,
fs => fs.values.map (countValues) .sum
)
Мы можем сделать сходство с сопоставлением с образцом более ясным, используя именованные аргументы:
def countValues (json: Json): Int = json.складывать(
jsonNull = 0,
jsonBoolean = _ => 1,
jsonNumber = _ => 1,
jsonString = _ => 1,
jsonArray = js => js.map (countValues) .sum,
jsonObject = fs => fs.values.map (countValues) .sum
)
Обе версии на самом деле немного более краткие, чем наша реализация сопоставления с образцом, хотя кратное
менее гибкое в некоторых
способами (мы не можем использовать охранников и т. д.), и версия с позиционными аргументами, в частности, возможно, менее читабельна.Но кратное
по-прежнему дает нам моральный эквивалент исчерпания,
в том смысле, что компилятор не позволит нам пропустить случай, а также оставляет нам большую свободу для оптимизации
наше представление JSON без навязывания подробностей нашим пользователям.
Одним из недостатков подхода кратного
является то, что каждый вызов требует от нас предоставления шести функций и
Использование идиоматического сопоставления с образцом означает создание экземпляров шести объектов. Это может реально повлиять на производительность,
и Circe предоставляет альтернативу для пользователей, чувствительных к производительности:
импорт io.circe. {Json, JsonNumber, JsonObject}
импорт io.circe.Json.Folder
val countValues: Folder [Int] = new Folder [Int] {
def onNull: Int = 0
def onBoolean (значение: Boolean): Int = 1
def onNumber (значение: JsonNumber): Int = 1
def onString (значение: String): Int = 1
def onArray (value: Vector [Json]): Int = value. map (_. foldWith (this)). sum
def onObject (значение: JsonObject): Int = value.values.map (_. foldWith (this)). sum
}
… который мы можем использовать так:
scala> импортировать io.circe.literal._
импорт io.circe.literal._
scala> json "[1,2, true, [4,5,6], null, [[[7]]]]". foldWith (countValues)
res0: Int = 7
Все, что делает этот тип Folder
, это позволяет нам инкапсулировать шесть функций в одном объекте.
У нас есть несколько тестов
в Цирце, которые пытаются смоделировать реалистичное использование,
и они показывают раз, при этом
имеет пропускную способность более чем на 70% больше, чем раз
. Фактически, раза с
также превосходит
сопоставление с образцом в этих тестах (показано здесь для Scala 2.13):
Benchmark Mode Cnt Score Единицы ошибки
FoldingBenchmark.withFold thpt 20 6491,030 ± 13,383 операций / с
Складной тест. Складывание со скоростью 20 11353,992 ± 98,429 оп / с
FoldingBenchmark. withPatternMatch thpt 20 8307,922 ± 27,285 операций / с
Для большинства пользователей эти различия, скорее всего, не имеют значения, и кратное
будет работать нормально.
Лично я предпочитаю Folder
, даже если я не сильно беспокоюсь о минимизации выделения.
Если вы читали «Банду четырех» (которой в этом году исполняется 25 лет),
Вы можете распознать Папку
как экземпляр шаблона посетителя. Это неудивительно, поскольку шаблон посетителя - это попытка решить те же проблемы, что и ADT,
обычно на языках, в которых нет сопоставления с образцом или функций высшего порядка, и то, что мы делаем, пытаемся придумать хороший способ работы
с ADT без сопоставления с образцом (потому что мы хотим, чтобы реализация была скрыта, но также была проверена полнота) или функций высшего порядка (по соображениям производительности).
Оригинальное обрамление рисунка посетителя «Банды четырех» довольно сильно приправлено императивом, но есть статья 2005 года.
Питера Бухловского и Хайо Тилеке, который дает теоретико-типовое представление о шаблоне и подробно рассматривает его взаимосвязь с алгебраическими типами данных.
Для меня самая полезная часть статьи - это различие между «внутренними» и «внешними» посетителями:
Еще один аспект паттерна «Посетитель» - выбор стратегии обхода для составных объектов.Мы могли бы поместить код обхода в тип данных. Для этого мы гарантируем, что метод accept вызывается рекурсивно для любых объектов-компонентов и передает результаты посетителю в вызове to visit. В качестве альтернативы мы могли бы поместить код обхода в самого посетителя. Мы будем называть их «внутренними» и «внешними» посетителями соответственно по аналогии с внутренними и внешними итераторами.
Используя эту терминологию, складки
и foldWith
в Argonaut и Circe являются примерами внешних посетителей, поскольку они требуют от пользователя явной рекурсии в случаях массивов и объектов. Внешний посетитель для нашего IntList
ADT будет выглядеть так:
запечатанный трейт IntList {
def accept [A] (посетитель: (A, (Int, IntList) => A)): A
}
case объект IntNil расширяет IntList {
def accept [A] (посетитель: (A, (Int, IntList) => A)): A = посетитель._1
}
case class IntCons (h: Int, t: IntList) расширяет IntList {
def accept [A] (посетитель: (A, (Int, IntList) => A)): A = посетитель._2 (h, t)
}
Что мы могли бы использовать так:
def sum (xs: IntList): Int = xs.accept [Int] ((0, _ + сумма (_)))
Или, чтобы подчеркнуть сходство с нашей исходной реализацией сопоставления с образцом:
def sum (xs: IntList): Int = xs.accept [Int] ((
0,
(h, t) => h + сумма (t)
))
Внутренний посетитель для IntList
будет примерно таким. Обратите внимание, что рекурсия происходит внутри реализации accept
, а не в самом посетителе:
запечатанный трейт IntList {
def accept [A] (посетитель: (A, (Int, A) => A)): A
}
case объект IntNil расширяет IntList {
def accept [A] (посетитель: (A, (Int, A) => A)): A = посетитель. _1
}
case class IntCons (h: Int, t: IntList) расширяет IntList {
def accept [A] (посетитель: (A, (Int, A) => A)): A =
visitor._2 (h, t.accept (посетитель))
}
Теперь мы можем переписать наш метод суммы
следующим образом:
def sum (xs: IntList): Int = xs.accept [Int] ((0, _ + _))
Это может показаться знакомым, потому что стандартная библиотека List
имеет почти идентичный метод:
def sum (xs: List [Int]): Int = xs.свернуть [Int] (0) (_ + _)
Опять же, это сходство не случайно: внутренние посетители, как говорится в другой газете, «в основном сбиты с толку».
Подводя итог: внешние посетители похожи на сопоставление с образцом, предоставляя пользователю доступ к одному слою структуры за раз,
и позволяя им рекурсивно переходить на следующий уровень по мере необходимости, в то время как внутренние посетители похожи на складки, где структура данных
сам управляет рекурсией.
Крепление Circe🔗
В течение многих лет меня не устраивал тот факт, что Argonaut (а позже Circe) имеет метод сгиба
, который на самом деле не похож на складку.Различие между внутренними и внешними посетителями помогает прояснить проблему: складки - это внутренние посетители, а то, что в настоящее время предоставляют Аргонавт и Цирцея, - это внешние посетители.
Это различие также помогает выявить пробел в API. Для таких операций, как наш метод countValues
выше, было бы намного лучше, если бы нам не приходилось выполнять рекурсию вручную. Используя подход внутреннего посетителя, мы могли бы написать следующее:
def countValues (json: Json): Int = json.acceptInternalVisitor (
0,
_ => 1,
_ => 1,
_ => 1,
_.sum,
_.foldMap (_._ 2)
)
В то время как подход внешнего посетителя является более гибким и обрабатывает больше вариантов использования, предоставление внутренних посетителей также упростило бы выполнение многих общих операций (например, вычисление статистики документа).
Еще одним преимуществом подхода внутреннего посетителя является то, что реализация accept
может взять на себя ответственность за безопасность стека (см., Например, эту суть), так что независимо от того, насколько глубоко вложен наш документ JSON, нам не о чем беспокоиться о рекурсии, переполняющей стек.Конечно, можно безопасно рекурсировать с Folder
прямо сейчас, но вам придется делать все прыжки вручную.
Вероятно, что Circe 1.0 переименует текущую папку
в Visitor
, как нашего внешнего посетителя, и представит нового безопасного для стека внутреннего посетителя как Folder
. Я также работаю над пакетом io.circe.internal
, который предоставит API внешнего посетителя, который будет предоставлять детали реализации (такие как представление числа, конкретная реализация карты и т. Д.). В настоящее время мы завершаем разработку названий и других деталей для этих типов и методов в Circe 1.