Опубликован: 04.04.2012 | Доступ: свободный | Студентов: 1988 / 60 | Оценка: 4.60 / 4.40 | Длительность: 13:49:00
Лекция 5:

Универсальность плюс наследование

< Лекция 4 || Лекция 5: 1234 || Лекция 6 >

Обращение структуры: посетители и агенты

Дисциплина проектирования, рассматриваемая в этой лекции, связана с комбинацией наследования, отложенных классов, полиморфизма, переопределения, динамического связывания, поддерживаемая контрактами, она вырабатывает в результате элегантную и гибкую архитектуру приложения. Но есть и темная сторона, которую нельзя оставить без исследования.

Грязный маленький секрет

"Грязный" секрет (не совсем уж секрет, поскольку коротко о нем упоминалось в начале обсуждения в этой лекции) состоит в том, что наш рецепт по сохранению стабильности архитектуры ПО годится только для одного из двух принципиальных вариантов внесения изменений. Применяемые до сих пор приемы программирования обеспечивают гладкую эволюцию программной системы, когда операции известны, а изменения связаны с появлением новых типов. Но ничего не было сказано о противоположном варианте.

Мне никак не хотелось бы, чтобы вы рассматривали прочитанные десятки страниц этой лекции как увертюру к опере, которую предстоит теперь услышать. Эксперименты показывают, что сценарий, глубоко нами изученный — расширение старых операций на новые типы, — наиболее частый в эволюции системы, он требует наиболее тонкого обращения. Здесь проявляется мощь полиморфизма и динамического связывания, демонстрируются успехи объектной технологии.

Но встречается и противоположный сценарий, игнорировать его нельзя.

Предположим, например, что в интересах ряда приложений в систему Traffic следует ввести изменения, позволяющие показ объектов разных видов сопровождать вспышками — повторяющимся несколько раз миганием. Такие вспышки можно разрешить всем объектам или только избранным видам объектов списка. Можно ли добавить эти свойства в ПО, сохраняя ту же степень скрытия информации, в частности, без проверки специфического типа объектов?

Позвольте нам называть целевыми классами те классы, в которые добавляются новые свойства, и клиентскими классами — классы приложения, которые будут применять новые операции к целевым объектам. В системе Traffic класс TAXI может быть примером целевого класса.

В благоприятных ситуациях уже изученные приемы все еще могут успешно применяться.

  • Если все целевые классы наследуются от общего предка, то можно добавить новую операцию на верхнем уровне и переопределить ее у потомков нужным образом.
  • Если нет общего предка, то можно его создать. Можно ввести, например, класс FLASHABLE, а затем, благодаря множественному наследованию, сделать все классы, объекты которых могут мигать, потомками класса FLASHABLE.

Эти приемы работают, но они плохо масштабируются, если необходимость в новых операциях возникает довольно часто. Наряду с "миганием" может возникнуть необходимость "вращения", а позже их "подъема" — еще многое, что может придумать человек. Множественное наследование позволяет вводить все новые маленькие классы — ROTATABLE, RAISABLE и другие — но все это не выглядит впечатляющим.

В качестве еще одного важного для практики примера рассмотрим среду разработки, такую как EiffelStudio или Eclipse. В качестве фундаментальной структуры используется абстрактное синтаксическое дерево (АСД), покрывающее целевые классы, такие как INSTRUCTION, EXPRESSION, LOOP. Эти классы обладают некоторыми базисными свойствами, но новый клиентский инструментарий непрерывно пополняется: форматизатор программ, анализатор, отыскивающий потенциальные ошибки, генератор HTML - все эти средства могут применять новые операции к каждому узлу АСД, но это встречается довольно редко. В EiffelStudio проблема решается использованием образца "Посетитель", обсуждаемого далее.

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

Есть две возможности решения такой проблемы.

  • Образец "Посетитель" (Visitor).
  • Использование механизма агентов.

Схема применения этих приемов состоит в следующем.

Образец "Посетитель" (pattern Visitor)

Образец "Посетитель" позволяет определить произвольные свойства, применимые к экземплярам существующих классов.

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

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

flash_taxi (t:TAXI) do... Алгоритм мигания объекта такси...end [8]

Аналогично можно определить метод flas-tram, flash_bus и так далее.

Вопрос в архитектуре построения — где разместить компоненты и каким способом использовать их, сохраняя расширяемость ПО?

Компоненты не должны принадлежать целевым классам. Если поместить их туда, то динамическое связывание давало бы решение. Но мы полагаем, что это не тот случай; вот почему они должны получать свои цели через аргументы, такие как t: TAXI в приведенном выше примере программы.

Нет никаких причин требовать и от клиентских классов реализации операций. Лучше всего собрать все методы в отдельный класс, который знает, как выполнять специальную операцию, скажем, flash, на различных целях, таких как такси и трамваи.

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

 Посетитель (треугольник операций)

Рис. 4.6. Посетитель (треугольник операций)

Для цели типа T и операции V, такой как flash, рисунок показывает сценарий взаимодействия между:

  • целевым классом, T_TARGET, задающим целевые объекты типа T. Класс TAXI является типичным примером;
  • классом посетителя, V_VISITOR, например, FLASH_VISITOR, задающим применение выбранной операции к объектам многих различных типов;
  • клиентским классом, задающим элемент приложения, которому необходимо выполнять операции над целевыми объектами различных типов.

Часто клиентскому классу необходимо выполнить операцию на множестве целевых объектов, например, операцию flash для всех Traffic объектов из списка. Это объясняет термин "посетитель" : посетитель — экземпляр класса, такого как FLASH_VISITOR, — позволяет клиенту "посетить" каждый элемент некоторой полиморфной структуры, каждый раз выполняя подходящую версию специфицированной операции. Как вы знаете, процесс выполнения таких визитов называется итерированием, или обходом.

Как всегда, в интересах расширяемости и повторного использования при обсуждении архитектуры ПО важно установить, кому какие требуются знания и кто не должен знать деталей. Цель — уменьшить количество информации, распространяемой по структуре, что особенно важно, когда информация изменяется. Здесь:

  • целевой класс знает о специфическом типе, таком как TAXI (так, например, TAXI наследует от VEHICLE, а VEHICLE от MOVING ), а также его контекст в иерархии типов. Он не знает о новых операциях, запрашиваемых извне, таких как flash ;
  • класс "Посетитель" знает все о данной операции и обеспечивает подходящие варианты для релевантных типов, обозначая соответствующие объекты через аргументы. В класс "Посетитель" помещаются методы, такие как flashbus, flashtaxi, flashtram. Он ничего не знает о клиентах;
  • клиентскому классу необходимо применять данную операцию к объектам определенного типа, так что он должен знать типы (только их существование, но не их свойства) и операции (только их существование и применимость к данному типу, но не специфические алгоритмы);
  • используя образец "Посетитель", клиент будет способен применить операцию, например, для всех элементов списка без знания индивидуальности типов элементов, как в следующей программе:
flash_all (fl: LIST [TARGET]    —Смотри ниже о TARGET
    — Мигание(flash) всех элементов в fl.
do
  from fl.start until fl.after loop
    — "Flash fl.item"
    fl.forth
  end
end 
 Классы образца "посетитель"

Рис. 4.7. Классы образца "посетитель"

Образец "Посетитель" обеспечивает реализацию, заданную строкой псевдокода. Она проста (следует из рисунка). Базисной операцией посещения является

 t.accept (v)

Здесь t — целевой объект, v — объект посетителя. Это позволяет заменить строку псевдокода следующим кодом:

fl.item.accept (flasher)

Здесь flasher — это объект FLASH_VISITOR.

Все целевые классы должны обеспечить компонент accept, общая реализация которого имеет вид:

accept (v: VISITOR)
    — Применить релевантную операцию посещения (visit)).
  do
    v. T_visit (Current)
  end

Здесь T_visit — это метод, реализующий запрашиваемую операцию для типа T, например, bus_visit, tram_visit. Эти методы должны быть реализованы в классе "Посетитель":

bus_visit (t:BUS) do flash_bus (t) end
tram_visit (t:TRAM) do flash_tram (t) end
taxi_visit (i:TAXI) do flash_taxi (t) end
...и так далее...

Обычно возможно избежать обертки существующих процедур — вместо этого можно непосредственно реализовать flash_bus под именем bus_visit и так далее.

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

Так что цель может вызвать T_visit на посетителе, где T идентифицирует целевой тип, и передает себя — Current — как аргумент. Теперь в игру вступает посетитель, он использует правильную операцию, идентифицируемую включением T в имя метода, запуская ее на правильном объекте, идентифицируя его как аргумент, переданный методу.

Хотя образец "Посетитель" предназначен для лечения ограничений динамического связывания, сам он основан на использовании этого механизма фактически дважды:

D1 в вызове клиента t.accept(v) для выбора правильной цели.

D2 в вызове цели v. T_visit (Current) для выбора правильной операции (выбирая правильного посетителя).

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

Реализация дважды применяет одиночную диспетчеризацию. Первый вызов, D1, получает цель — объект t — и, выполняя одиночную диспетчеризацию, находит правильный для этой цели метод accept. В методе с использованием переданной ему в качестве аргумента операции осуществляется второй вызов, D2, где операция выступает в роли цели, а тип в роли аргумента. Здесь снова выполняется одиночная диспетчеризация — динамическое связывание, которое и находит нужный алгоритм, выполняющий требуемую операцию, получая при этом объект нужного типа, переданный как Current.

Для того чтобы сработало динамическое связывание при первом вызове D1, все целевые классы должны иметь собственную реализацию метода accept, каждый объявляя его в форме v.T_visit(Current), как показано выше.

 Посетитель (полная схема)

Рис. 4.8. Посетитель (полная схема)

Как показано на рисунке, метод accept приходит от общего предка, где он отложен. Этим предком может быть класс TARGET, который может быть очень простым:

note
  description: "Объекты, которые могут использоваться как цели в образце
    — "Посетитель""
deferred class TARGET feature
  accept ( v: VISITOR)
    - Выполнить операцию посещения (visit) на текущем объекте с целью v
    -| Замечание: типичная реализация v.T_visit(Current)
    -| где T - это специфический эффективный тип потомка.
    deferred
    end
end

Последние две строчки заголовочного комментария используют стандартное соглашение: если комментарий начинается тройкой символов --|, то это комментарий, задающий свойства реализации и не относящийся к клиентам.

Разочаровывает то, что все целевые классы наследуют от специального класса TARGET, поскольку основная идея состояла в том, чтобы использовать целевые классы в исходном виде. С рассмотренными до сих пор приемами программирования, если не предусмотреть такое поведение для целевых классов с самого начала, дело плохо. Это принципиальное ограничение образца "Посетитель". Для его устранения необходимо перейти к технике, полностью отличающейся от образца. Все не так плохо во многих практических случаях. Ведь цель состояла в том, чтобы избежать повторяющейся модификации целевых классов при введении дополнительных операций. Здесь же нужно только убедиться, что целевой класс имеет специального предка с определенной операцией, после чего жизнь становится прекрасной и можно добавлять посетителей по своему усмотрению.

На стороне посетителя также необходим общий предок, назовем его VISITOR. С этим требованием проблем не возникает, поскольку все равно нужно создавать специальный класс посетителя всякий раз, когда появляется новая операция.

Предыдущий рисунок показывает все относящиеся к делу классы и отношения наследования между ними. Имена отложенных классов TARGET и VISITOR теперь начинаются с префикса TRAFFIC, которое следует заменить именем, идентифицирующим приложение в случае создания собственной реализации образца "Посетитель". Эти классы невозможно определить как повторно используемые компоненты. Класс VISITOR должен знать, в частности, все целевые типы в приложении, чтобы он мог перечислить даже в отложенной форме релевантные методы, здесь — tram_visit, bus_visit и так далее.

Как и ранее, T и V задают в прототипах примеров целевой тип и операцию, дополняемую здесь конкретными классами, такими как TRAM и FLASH.

Завершая презентацию образца "Посетитель", я хочу надеяться, что вы поняли ее в полном объеме — не только как технику, но и ее точные цели, область возможного применения, принципы, преимущества и ограничения, — и способны применить ее на практике.

Улучшение образца "Посетитель"

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

  • Для обеспечения "посещаемости" все целевые классы должны быть потомками общего предка. Это не реалистично, если их авторы специально не предусматривали такую цель.
  • Образец не задает повторно используемое решение. Он должен заново программироваться при всяком его использовании. Во всех случаях код выглядит похожим, примером является метод accept, соответствующий общей схеме.

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

Решение, основанное на агентах, просто в использовании. Для каждого применимого целевого класса T пишется метод visit, реализующий требуемую операцию для T. Это не требует модификации существующего класса или создания нового класса. Затем общему механизму, применяющему операцию к объекту, передается как цель, так и агент visit — объект, представляющий операцию. Сам механизм ничего не знает о специфике операций. Упражнение в лекции по агентам попросит вас представить решение. Библиотека "образцов", разработанная в ETH, обеспечивает повторно используемый вариант "Посетителя", основанный на этом подходе.

< Лекция 4 || Лекция 5: 1234 || Лекция 6 >
Надежда Александрова
Надежда Александрова

Уточните пожалуйста, какие документы для этого необходимо предоставить с моей стороны. Курс "Объектно-ориентированное программирование и программная инженения".