Введение наследования позволяет нам повторно обратиться к рассмотрению другого главного механизма расширяемости классов — универсальности. Независимо оба механизма уже рассмотрены, их комбинация добавляет новый потенциал.
У нас уже появлялся пример сотрудничества этих механизмов — полиморфные структуры данных. Рассмотрим контейнер, такой как:
fleet: LIST [VEHICLE]
Элементы в контейнере могут быть экземплярами любого из потомков VEHICLE: На следующем рисунке показан графический образ комбинации "универсальность-наследование". Хотя никаких новых концепций не вводится, но здесь иллюстрируется неформальная интерпретация взаимодействия двух механизмов.
(рис 4.1) Полиморфный список
(рис 4.2) Наследование и универсальность
Начнем с центра, где показан класс, задающий понятие "список такси", — это класс с ожидаемыми компонентами, характерными для списка: добавить и удалить элемент, передвинуть курсор, получить элемент и так далее. Мы можем перейти теперь к концепции нового класса, двигаясь вдоль двух различных направлений.
Рисунок иллюстрирует возможность свободного перемещения по метафорической плоскости, переходя от "списка персон" к "цепочке городов".
Полиморфные структуры данных не являются единственным способом комбинирования универсальности и наследования. Другая важная техника следует из исследования вопроса: что можно делать с сущностями или выражениями универсального типа?
В универсальном классе, таком как LIST[G] или ARRAY[G], в тексте этого класса, как правило, появляется объявление x: G. Какие операции применимы к G?
Прежнее обсуждение может подсказать ответ. Так как G — просто держатель места для любого типа, который будет задан фактическим родовым параметром ( VEHICLE, например), применимыми операциями могут быть только операции доступные любому типу, следовательно — операции, введенные в классе ANY. Так что можно использовать вызовы: x.cloned, x.isequal(y) и так далее, но нельзя использовать компоненты, отсутствующие у ANY.
Что если требуется больше операций? Рассмотрим случай "сортирующего" класса с методом sort, сортирующим элементы структуры данных, — массива, списка, например, списка целых. Естественно, хочется иметь механизм, работающий для многих типов, а не только для целых. Универсальность кажется подходящим механизмом для реализации поставленной задачи.
Алгоритмы сортировки — это обширная область информатики, она не является предметом нашего изучения, хотя
x : = t[i] ; y := t[j]
if x < y then
— Обмен элементов, находящихся в позициях i и j
a [i] := y ; a [j] := x
end
Алгоритм будет выполнять обмен значениями для элементов, нарушающих порядок. Не интересуясь сейчас тем, как найти такие элементы, сосредоточимся только на одном вопросе: а как выполнить само сравнение, какую операцию "<" следует использовать?
Если речь идет о сортировке целых, то все понятно, но что если необходимо ранжировать игроков в теннис, — как тогда выполняется сравнение? Как быть, если в общем случае мы даже не знаем, есть ли вообще такая операция у объектов?
Иногда мы можем это знать, иногда нет. Как отмечалось при рассмотрении класса COMPARABLE, общепринятого тотального порядка не существует на множестве комплексных чисел или матриц.
Чтобы обеспечить общецелевой алгоритм сортировки, применимый для сортировки массивов с элементами многих типов, можно было бы поместить приведенный ранее код в универсальный класс
class SORTER [G...] feature
sort_array (a: ARRAY [G])
— Сортировка элементов a в соответствии с отношением порядка.
local
x, y: G
do
... Код, такой, как [4] с проверками, такими, как x < y...
End
...
end
Но как быть с операцией "<"? Ведь x и y объявлены как сущности типа G, так что над ними определены только операции из ANY, а там нет операции, позволяющей сравнивать объекты. У нас есть потребность в использовании операции сравнения, но она доступна только для специфических классов, таких как класс COMPARABLE.
Классы, такие как класс COMPARABLE? Почему бы не выбрать сам COMPATIBLE? Он отложен, и его эффективные наследники обеспечат реализацию lesser alias "<". Можно пойти дальше и полагать, что любой класс, объекты которого удовлетворяют отношению тотального порядка, должен быть потомком COMPATIBLE. Тогда ответ на наш вопрос становится очевидным: формальный родовой параметр G не должен быть более произвольным типом, он должен задавать тип, согласованный с COMPATIBLE. Следующий синтаксис позволяет выразить это свойство:
class SORTER [ G -> COMPATIBL] feature ... Остаток как выше...
Символ -> соответствует стрелке на диаграмме наследования, указывая на родительский класс (здесь COMPATIBLE). Вся конструкция в целом задает ограничение универсальности. Смысл ограничения в том, что теперь родовое порождение является правильным только при условии, что T удовлетворяет ограничению. Так что и прекрасно подходят, так же как и , при условии, что TENNIS_PLAYER наследует от COMPATIBLE. Но не подходят , если мы только не сделаем транспортные средства сравнимыми. Неподходящие случаи будут отвергнуты на этапе компиляции.
Как вы догадываетесь, базисный случай LIST [G] (неограниченная универсальность) можно рассматривать как краткую форму записи LIST [G-> ANY].
Ограничение универсальности имеет много применений. Некоторые часто встречающиеся случаи используют отложенные библиотечные классы, подобные COMPATIBLE.
ml + m2, где ml и m2 относятся к типу MATRIX[T]. Учитывая тип NUMERIC, к решению приводит объявление: MATRIX[T->NUMERIC]. В этом случае можно использовать MATRIX[INTEGER], но не MATRIX[STRING]. Стоит ли сделать сам класс MATRIX наследником NUMERIC? Этот интересный прием имеет смысл, так как обеспечивает все требуемые операции (модель NUMERIC соответствует математическому понятию кольца ). В этом случае становятся возможными такие родовые порождения классов, как MATRIX[MATRIX[INTEGER]] или MATRIX[MATRIX[MATRIX[REAL]]] и так далее.C [T] необходима возможность сохранять элементы типа T в хеш-таблице. Это предполагает, что для каждого такого элемента можно вычислить целочисленную хеш-функцию. Требование простое, но не все типы ему удовлетворяют, нужно иметь дело в этом случае с потомками класса HASHABLE, которые задают реализацию отложенного запроса hashcode. Сам класс HASHTABLE объявляется как
class HASH_TABLE [ELEMENT, KEY -> HASHABLE] feature...
Теперь становится понятным, почему родовой параметр в классе TOPOLOGICAL_SORTER также был ограничен HASHABLE — мы хотели помещать элементы в хеш-таблицу.
Объявление HASH_TABLE демонстрирует, что универсальный класс может иметь несколько параметров, некоторые из которых могут быть ограниченными. Также ясно, что имена формальных параметров можно выбирать по собственному вкусу.
Замечание : можно задавать множественные ограничения универсальности. Например, можно объявить класс:
class C [G -> (COMPARABLE, NUMERIC, HASHABLE)] feature
Это ограничение говорит, что фактический тип при родовом порождении должен соответствовать всем перечисленным типам. Правильным примером может служить класс INTEGER. Пример немного экстремальный, но требование сравнимости элементов и возможности выполнять над ними числовые операции встречается достаточно часто.
Ограниченная универсальность иллюстрирует фундаментальную роль типов в современном программировании. Для решения обсуждаемых проблем возможны и другие подходы, такие как передача процедуры, выполняющей сравнение, методам класса . Но данный подход наиболее согласуется с ОО-идеями — сделать требуемую функциональность частью типа. Это также означает, что по-прежнему можно полагаться на компилятор, который будет выполнять все необходимые проверки корректности, что позволит избежать появления подобных ошибок в период выполнения. Если компилятор отвергает ваш класс из-за несогласованности типов, помните, что эта новость, кажущаяся плохой, по-настоящему является хорошей, — лучше вы поймаете "жучка", чем он поймает вас.
Цените мощь статической типизации. Это настоящий механизм верификации программ. Изощренность системы типов, которой вы теперь владеете, — с классами, ограниченной и неограниченной универсальностью, включающей множественные ограничения, одиночное, множественное и повторное наследование, полиморфные переменные и структуры данных, правила вызова методов, передачи аргументов и присваивания, — определяет каркас с математической строгостью, который компилятор использует для выполнения критически важных согласованных проверок. Типы элементов вашей программы отражают семантику, лежащую в основе строящейся модели внешнего мира.
Помните вопрос, задаваемый в начале этой лекции: "Что, если я знаю, что последний элемент списка является экземпляром TAXI, а не просто VEHICLE, и хочу применить к нему операцию, специфическую для такси"? Обсуждение вклада динамического связывания в архитектуру ПО объясняет, почему для полиморфных структур данных нет срочной необходимости в его рассмотрении.
(рис 4.3) Полиморфный список
Вы рассматриваете элемент списка, известного вам как список транспортных средств:
fleet: LIST [VEHICLE]
Такое объявление отвергает специфику — идентичность, характерную для такси, автобуса или трамвая, — перед нами просто транспортные средства. У такого подхода есть достоинства и недостатки. Недостаток в том, что нельзя использовать преимущества знания "трамвайности" или "таксичности". Достоинство подхода в том, что можно применить операцию, допустимую для транспортных средств, без знания того, какое именно средство используется в данном случае, даже если эта операция выполняется специфическим образом для каждого вида транспортного средства. Так что всякий раз, когда мы хотим применить специфическую операцию, обычно необходимо сделать эту операцию применимой для всех транспортных средств и задать специфическую реализацию для каждого вида. Это единственный способ справиться с синдромом множества явных вариантов.
Такой подход неприменим в двух случаях.
ANY ).Рассмотрим важный пример второго случая. Любое хорошее ОО-окружение поддерживает механизм сериализации — записи структуры объектов в файл, и десериализации — получения их оттуда.
retrieved: ANY
— Объект, полученный последней операцией десериализации
Объект можно объявить только типа ANY, поскольку операция десериализации общецелевая. Она должна работать для любой проблемной области и возвращать любой извлекаемый из файла объект, будь то такси, трамвай, город или еще что-нибудь. В конкретном приложении и для конкретного файла можно ожидать определенный тип объекта, например, такси, но нет возможности, задав t: TAXI, просто написать:
t := my_serializer.retrieved
Тип ANY не согласован с TAXI— справедливо лишь обратное утверждение. В этой ситуации необходимо явное преобразование, называемое кастингом или приведением типа. Зная, что полученный объект является такси, мы должны вернуть ему индивидуальность, отвергнув идентификацию его как ANY, присущую всем объектам в этой толпе. Для поддержки процесса идентификации нам необходимо нечто большее, чем наследование, полиморфизм и динамическое связывание.
В поисках нового механизма заметим, что кастинг не может быть безусловным. Когда объект приходит из файла или по сети, можно ожидать, что он принадлежит определенному типу, но быть уверенным в этом нельзя. Теперь мы имеем дело с объектами, не подлежащими полному контролю в программе. Когда объекты создаются программой, объявление t: TAXI гарантирует, что t всегда будет присоединен к экземпляру TAXI. Для объектов, приходящих из дикого внешнего мира, таких гарантий нет.
Даже, если структура объектов была сериализована вами, при ее десериализации нет гарантии, что она сохранила девственность, — файл мог быть поврежден случайно или злонамеренно. Отсюда для релевантного механизма языка следует правило:
Принцип Кастинга
Любой механизм приведения типа объекта, заданного ссылкой, приходящего без соответствующих статических правил согласования, должен основываться на проверке периода выполнения, чтобы убедиться, что тип объекта согласован с ожидаемым.
Другими словами, такой механизм должен быть условным. Если вы ожидаете TAXI, а обнаружили некий другой тип, не согласованный с такси, попытка кастинга должна закончиться неуспехом.
Механизм, не удовлетворяющий этому принципу, свойственен кастингу, реализованному в языке С (в языке С++ принцип также не выдерживается, но там есть более изощренная форма приведения к типу, соответствующая принципу). В языке С, написав (T) e, где T - тип, а e - ссылка, получим ссылку на объект типа T с сохранением значения e, независимо от
Общим термином для описания механизма кастинга, удовлетворяющего принципу, является " динамический кастинг" (приемлем также термин "условный кастинг"). Это еще одна область, где отсутствует стандартная терминология. Можно встретить в литературе "сужение типа" (narrowing) или "приведение вниз" (downcasting). Эти термины используются, когда речь идет о приведении общего типа к специальному типу, например, от VEHICLE к TAXI. Такой случай часто встречается на практике, но он не единственный. Мы увидим, почему изучаемый механизм может применять динамический кастинг к объектам произвольных типов.
Наиболее общий термин, покрывающий все случаи нахождения типа объектов во время выполнения, носит название RTTI (Run Time Type Identification).
Прошу прощения за громкие термины, не мной они введены. Вы должны знать, что они существуют, но что действительно следует знать, — это лежащие в основе концепции и общее решение, которое сейчас будет представлено.
Давайте рассмотрим два механизма динамического кастинга, один — текущий, другой — устаревший, но все еще используемый.
(рис 4.4) Полиморфный список
Предположим, вы убеждены, что последний объект fleet — это объект TAXI, как на рисунке. Потому вы хотите применить специфический компонент, такой, как take, к этому элементу. Простейшее решение не будет работать:
fleet.last.take (...)
Причина этого должна быть ясна, но ее можно проанализировать тщательнее, если разделить выполнение оператора на части:
— Предыдущие объявления: fleet: LIST[VEHICLE] t:? — Держатель места, должен быть заменен фактическим типом ...
t := fleet.last
t.take (...)
При объявлении t использован знак вопроса вместо
t как VEHICLE, то выражение будет правильной конструкцией, а выражение нет, так как take не является компонентом VEHICLE.t как TAXI, то выражение будет правильной конструкцией, но не выражение, так как нарушается правило полиморфизма типов: потомку нельзя присваивать родительский объект — это противоречит направлению Конструкция теста объекта обеспечивает решение в таких случаях. Тест объекта — это булевское выражение, которое (как форма
if attached (TAXI} fleet.last as t then t.take (. ) ... Любая другая TAXI-операция над t, присоединенному к TAXI-объекту... else ...Делай что-нибудь еще (не над такси), если необходимо... end
В соответствии с принципом кастинга операция является условной. Она тестирует тип fleet.last — фактического объекта, присоединенного к ссылке во время выполнения.
Возвращается значение false, если нет согласования с заданным типом TAXI. В этом случае будет выполняться else -ветвь данного оператора, если она присутствует. Если же тип согласован, то в нашем распоряжении появляется объект такси, и он становится локально доступным через заданное имя t, так называемую локальную переменную теста объекта. По семантике это соответствует корректному объявлению t и присвоению ему значения, как в выражении.
Если тест объекта служит условием оператора if, как в данном случае, то областью локальной переменной теста является then -ветвь, где можно использовать t как переменную TAXI, обозначающую значение fleet.last. Поскольку это и в самом деле TAXI, что установлено как статически в результате объявления, так и динамически — в результате проверки присоединенного объекта во время выполнения, то без всякого риска можно применять к t операции, специфические для такси.
Рассмотрим, как определяется область локальной переменной теста, где безопасно можно выполнять операции. Следующие случаи покрывают все практические потребности (если возникает более сложная ситуация, можно ввести свою локальную переменную).
if, то областью является then -ветвь оператора. Сюда же включается случай, когда тест комбинируется с другими условиями через and then, как if attached (TAXI) fleet.item as t and then v.ismoving then...or else ), то областью является else -ветвь, как if not attached (TAXI) fleet.item as t then.until, то областью является тело цикла. И снова возможна комбинация теста с другими булевскими выражениями.Как пример последнего случая, рассмотрим, как подсчитать число объектов, предшествующих первому объекту специального типа в полиморфном списке.
pre_taxi_count (fleet: LIST [VEHICLE]): INTEGER
— Число объектов fleet, перед первым TAXI
do
from fleet.start until
fleet.after or else attached (TAXI) fleet.item as t
loop
Result := Result +1 ; fleet.forth
end
ensure
non_negative: Result >= 0
at_most_length_of_list: Result <= fleet.count
end
Этот конкретный алгоритм не использует локальную переменную теста t ; как следствие, тест можно записывать в этом случае проще, как attached (TAXI) fleet.item.
Ограничений на типы, включаемые в тест, не существует: attached! U exp as x, где exp является выражением (T — типы U и T могут быть произвольными. В наиболее распространенном случае, названном "приведением вниз", тип U является потомком типа T. Пример с типами VEHICLE и TAXI иллюстрирует эту ситуацию. Но это не является абсолютным требованием, и для множественного наследования может понадобиться тест для типов U и T, не связанных отношением наследования. Типичный пример показан на следующем рисунке.
(рис 4.5) Непрямые отношения наследования
Классы NUMERIC и HASHABLE разделяют потомков, таких как B и C. Если имеется список numlist из элементов NUMERIC, то может потребоваться хеширование тех элементов списка, которые допускают эту операцию:
if attached (HASHABLE} numlist.item as h then your_hash_table.put (h, h.hash_code) end
В этом фрагменте numlist имеет тип LIST[NUMERIC], а hashcode — это метод класса HASHABLE. Приведение типов в данном случае не является приведением вниз, так как HASHABLE не согласован с NUMERIC. Случай вполне законный и реально существующий. Он иллюстрирует, почему необходим общий механизм динамического кастинга, а не ограниченное сужение типа.
(Этот раздел является дополнительным и может быть пропущенным при первом чтении, но уже следующий раздел "Разумно используйте динамический кастинг" прочесть необходимо) Тест объекта вытесняет старый механизм — попытку присваивания. Дадим короткий обзор этого механизма. Вернемся к примеру, показывающему неспособность базисного присваивания обеспечить приведение вниз.
fleet: LIST [VEHICLE]
t:TAXI
— t : = fleet.last — Только для сравнения: закомментировано, поскольку
— неверно!
t?= fleet.last
t.take (. ) — Правильно, но не безопасно, смотри далее
Мы объявили t как TAXI, что делает t : = fleet.last неверным: присваивание в направлении, ошибочном для (:=) давайте будем использовать "попытку присваивания", использующую символы "?=". Семантика такова:
TAXI), то цель, здесь t, присоединяется к объекту как в обычном присваивании;t получает значение void. Безопасное использование попытки присваивания должно непосредственно после попытки присваивания и до использования полученной переменной как цели вызова компонента выполнить "тест на void ":
t?= fleet.last if t /= Void then t.take (. ) — Правильно и безопасно ... Любая другая TAXI операция над t, присоединенному к TAXI-объекту... else ... Делай что-нибудь еще (не над такси), если необходимо... end
Ясно, что попытку присваивания можно проводить всюду, где может использоваться тест объекта. Новый механизм имеет преимущество не по причине загромождения теста локальными переменными, такими как t, — локальная переменная теста играет ту же роль. Но дело в том, что она появляется точно в том месте, где необходимо ее использовать. Помимо этого, использование Void в качестве ошибки является небезопасным, так как ничто не заставляет вас выполнять тест на void. Отсутствие проверки приводит к void -вызову со всеми вытекающими последствиями, а при применении теста объекта этого не происходит. В результате честно служившая в течение десятилетий попытка присваивания " подала в отставку", и теперь в стандарте языка Eiffel используется тест объекта.
Следует всегда помнить о маячащей угрозе синдрома множества явных вариантов. Динамический кастинг делает возможным реализовать структуру решения в форме: "Если TAXI, то делай это, иначе, если , то делай то, иначе, если BUS.".
Детали, кстати, должны быть тщательно учтены — по той причине, что тесты используют согласование типов. Если выполняется разбор случаев MOVING -объектов, то следует помнить, что, например, объект согласован также с типом VEHICLE, так что порядок тестов играет значение.
Все же вы можете делать это.
Ясно, что это не лучшая идея. Слова, поясняющие, почему это плохо, уже сказаны. Динамический кастинг полезен, но не как соперник динамическому связыванию, который побеждает в схватке, когда оба соперника применимы. Динамический кастинг необходим, когда объекты приходят из внешнего мира — файлов,
В таких случаях часто приведение делается к одному типу, не выполняя проверки всех возможных случае"приведение вниз"Разумно используйте динамический кастинг. Это довольно хороший критерий использования динамического кастинга. Если ожидается некоторый тип объекта и выясняется, что реальность согласована с ожиданиями, то все прекрасно. Если же приходится делать выбор, анализируя весь возможный набор типов, то почти наверняка динамический кастинг используется не по назначению и его место должно занять динамическое связывание.
Есть еще один выход. Если нет свободы в создании новых классов, то применяйте образец "Посетитель", описание которого является заключительной темой этой лекции.
Дисциплина проектирования, рассматриваемая в этой лекции, связана с комбинацией наследования, отложенных классов, полиморфизма, переопределения, динамического связывания, поддерживаемая контрактами, она вырабатывает в результате элегантную и гибкую архитектуру приложения. Но есть и темная сторона, которую нельзя оставить без исследования.
Грязный маленький секрет
"Грязный" секрет (не совсем уж секрет, поскольку коротко о нем упоминалось в начале обсуждения в этой лекции) состоит в том, что наш рецепт по сохранению стабильности архитектуры ПО годится только для одного из двух принципиальных вариантов внесения изменений. Применяемые до сих пор приемы программирования обеспечивают гладкую эволюцию программной системы, когда операции известны, а изменения связаны с появлением новых типов. Но ничего не было сказано о противоположном варианте.
Мне никак не хотелось бы, чтобы вы рассматривали прочитанные десятки страниц этой лекции как увертюру к опере, которую предстоит теперь услышать. Эксперименты показывают, что сценарий, глубоко нами изученный — расширение старых операций на новые типы, — наиболее частый в эволюции системы, он требует наиболее тонкого обращения. Здесь проявляется мощь полиморфизма и динамического связывания, демонстрируются успехи объектной технологии.
Но встречается и противоположный сценарий, игнорировать его нельзя.
Предположим, например, что в интересах ряда приложений в систему Traffic следует ввести изменения, позволяющие показ объектов разных видов сопровождать вспышками — повторяющимся несколько раз миганием. Такие вспышки можно разрешить всем объектам или только избранным видам объектов списка. Можно ли добавить эти свойства в ПО, сохраняя ту же степень скрытия информации, в частности, без проверки специфического типа объектов?
Позвольте нам называть целевыми классами те классы, в которые добавляются новые свойства, и клиентскими классами — классы приложения, которые будут применять новые операции к целевым объектам. В системе Traffic класс TAXI может быть примером целевого класса.
В благоприятных ситуациях уже изученные приемы все еще могут успешно применяться.
FLASHABLE, а затем, благодаря множественному наследованию, сделать все классы, объекты которых могут мигать, потомками класса FLASHABLE. Эти приемы работают, но они плохо масштабируются, если необходимость в новых операциях возникает довольно часто. Наряду с "миганием" может возникнуть необходимость "вращения", а позже их "подъема" — еще многое, что может придумать человек. Множественное наследование позволяет вводить все новые маленькие классы — ROTATABLE, RAISABLE и другие — но все это не выглядит впечатляющим.
В качестве еще одного важного для практики примера рассмотрим среду разработки, такую как EiffelStudio или Eclipse. В качестве фундаментальной структуры используется абстрактное синтаксическое дерево (АСД), покрывающее целевые классы, такие как INSTRUCTION, EXPRESSION, LOOP. Эти классы обладают некоторыми базисными свойствами, но новый клиентский инструментарий непрерывно пополняется: форматизатор программ, анализатор, отыскивающий потенциальные ошибки, генератор HTML - все эти средства могут применять новые операции к каждому узлу АСД, но это встречается довольно редко. В EiffelStudio проблема решается использованием образца "Посетитель", обсуждаемого далее.
В некоторых ситуациях полностью отсутствует возможность модификации целевых классов, например, если они находятся в библиотеке, недоступной для ваших изменений. Тогда предыдущие рецепты не помогают.
Есть две возможности решения такой проблемы.
Схема применения этих приемов состоит в следующем.
Образец "Посетитель" позволяет определить произвольные свойства, применимые к экземплярам существующих классов.
Идея очень проста: пусть операции знают о типах. Если мы применяем различные операции к различным типам, то либо операция должна знать о типах, либо тип об операциях. При динамическом связывании каждый тип знает о применимых операциях. Теперь будем рассматривать противоположную ситуацию — клиентскому классу необходимо выполнять операцию на экземплярах многих возможных классов (целевых классов). Мы можем называть объекты, такие как такси и трамваи в нашем постоянном примере, целевыми объектами или просто целями.
Проблема не в том, как определить операцию. Предполагается, что подходящие алгоритмы доступны для написания методов, например
flash_taxi (t:TAXI) do... Алгоритм мигания объекта такси...end [8]
Аналогично можно определить метод flas- и так далее.
Вопрос в архитектуре построения — где разместить компоненты и каким способом использовать их, сохраняя расширяемость ПО?
Компоненты не должны принадлежать целевым классам. Если поместить их туда, то динамическое связывание давало бы решение. Но мы полагаем, что это не тот случай; вот почему они должны получать свои цели через аргументы, такие как 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 задают в прототипах примеров целевой тип и операцию, дополняемую здесь конкретными классами, такими как и FLASH.
Завершая презентацию образца "Посетитель", я хочу надеяться, что вы поняли ее в полном объеме — не только как технику, но и ее точные цели, область возможного применения, принципы, преимущества и ограничения, — и способны применить ее на практике.
Проект образца "Посетитель" представляет популярную и полезную технику, но страдает двумя отмеченными недостатками.
accept, соответствующий общей схеме.Используя универсальность, базисную схему можно улучшить. Полное удовлетворительное решение основано на механизме, изучаемом в следующей лекции, — агентах.
Решение, основанное на агентах, просто в использовании. Для каждого применимого целевого класса T пишется метод visit, реализующий требуемую операцию для T. Это не требует модификации существующего класса или создания нового класса. Затем общему механизму, применяющему операцию к объекту, передается как цель, так и агент visit — объект, представляющий операцию. Сам механизм ничего не знает о специфике операций. Упражнение в лекции по агентам попросит вас представить решение. Библиотека "образцов", разработанная в ETH, обеспечивает повторно используемый вариант "Посетителя", основанный на этом подходе.
Образец "Посетитель" дополняет динамическое связывание, позволяя просто добавлять операцию в множество существующих типов (в противоположность обращенной задаче). Эта статья представляет образец, предлагая реализующий его повторно используемый компонент, который является частью библиотеки образцов ETH. В литературе можно найти много описаний образца "Посетитель" (включая Википедию), большинство которых используют в качестве источника описание из книги E. GAMMA и др.,
select.Не пугайтесь длине последующего списка, поскольку здесь приводятся вариации, используемые разными авторами и в разных языках программирования. Число фактических концепций не столь велико, например, динамическая диспетчеризация означает то же, что и динамическое связывание. Убедитесь, что вы понимаете все концепции.
| Предок | Assignment attempt | Попытка присваивания | |
| Cast | Кастинг приведение к типу | Client (of a visit) | Клиент (визита) |
| Conformance | Согласование | Constrai-ned genericity | Ограниченная универсальность |
| Deferred feature, class, type | Отложенный метод, класс, тип | Descendant | Потомок |
| Double dispatch | Двойная диспетчеризация | Downcasting | Приведение вниз |
| Dynamic binding | Динамическое связывание | Dynamic cast | Динамический кастинг |
| Dynamic dispatch | Динамическая диспетчеризация | Effect, effective, effecting | Эффект, эффективный, эффективизация |
| Flat view | Плоский облик | Generic constraint | Ограничение универсальности |
| Immediate feature | Непосредственный компонент | Inheritance | Наследование |
| Inherited feature | Наследуемый компонент | Interface (Java, C#) | Интерфейс (Java, C#) |
| Introduce (a feature) | Введение (компонента) | "Is-a" relation | Отношение"является" |
| Multiple inheritance | Множественное наследование | Name clash | Конфликт имен |
| Object test | Тест объекта | Object-Test local | Переменная теста объекта |
| Overriding | Переопределение | Parametric polymorphism | Параметрический полиморфизм |
| Полиморфное выражение | Polymorphism | Полиморфизм | |
| Полиморфная структура данных | Precursor | Precursor (версия родителя) | |
| Programs with | Программы с дырами | Proper | Подходящий (правильный) предок, потомок |
| Redeclaration | Переобъявление | Redefinition | Переопределение |
| Refinement | Уточнение | Repeated inheritance | Повторное наследование |
| Replication (of a feature) | Репликация компонента | Routine table | Таблица методов |
| Single dispatch | Одиночная диспетчеризация | ||
| Subclass | Подкласс(субподрядчик) | Subcontracting | Субконтракт |
| Superclass | Суперкласс (подрядчик) | Taxonomy | Таксономия |
| Target (of a visit) | Цель (визита) | Type narrowing | Сужение типа |
| Unconstrained genericity | Неограниченная универсальность | Virtual table | Виртуальная таблица |
| Visitor | Посетитель (образец) |
Дайте точные определения терминам словаря.
Добавьте новые термины в карту концепций, построенную в предыдущих лекциях.
Это подготовительное упражнение для следующих двух (написать интерпретатор и компилятор). Цель — построить программы, использующие абстрактный синтаксис, избегая построения парсера. Задача не сложная, но использует приемы, которые не были рассмотрены.
Рассмотрим язык программирования L0 со следующими конструкциями, выраженными здесь неформально с конкретным синтаксисом.
integer (целые).+, -, *skip (пустой), read (чтение), compound (последовательность операторов), assignment (присваивание), conditional (условный), loop (цикл).skip не имеет эффекта.read x, где x — имя переменной, читает целое значение от интерактивного пользователя и присваивает его переменной x.x:= e, где x — переменная, e — выражение.0 означает ложь, а любое другое значение — истина.if e then il else i2 end, где e — выражение, рассматриваемое как булевское значение, il, i2 — операторы.from il until e loop i2 enddo... endPROGRAM, COMPOUND и так далее, позволяющих создавать представление абстрактного синтаксиса L0-программ. Используйте наследование подходящим образом. Убедитесь, что классы содержат подходящие процедуры создания, так, чтобы L0-программы были представлены как структуры объектов без конкретного синтаксиса.(Это продолжение предыдущего упражнения. Его задача — создать запись текста программы в конкретном синтаксисе по АСТ. Это задача, обратная к парсингу)
Напишите программу, которая печатает конкретное представление с конкретным синтаксисом, описанным в предыдущем упражнении. L0-программа дается как экземпляр класса PROGRAM и ассоциированных объектов (экземпляров и так далее). Каждый оператор должен начинаться на новой строке. Для составного оператора предусмотрите вложенность. Ветви условного оператора и тело цикла даются с отступами.
Проверьте результаты вычислений и убедитесь, что программа работает корректно.
(Это продолжение последних четырех упражнений)
Напишите интерпретатор — программу на Eiffel, которая может выполнять любую L0-программу, представленную в виде экземпляра класса PROGRAM и ассоциированных объектов (экземпляров и так далее). Семантика L0-такова:
Испытайте ваш интерпретатор на построенных примерах программ.
(Это продолжение предыдущих упражнений)
Напишите компилятор — программу на Eiffel, которая транслирует любую L0-программу в Eiffel-систему в форме корневого класса и множества вспомогательных классов при необходимости. Проверьте, что результаты работы компилятора и интерпретатора совпадают.
Образцом для этого упражнения может служить функция pre_taxi_count. Рассмотрите fleet: LIST[VEHICLE].
Покажите, как улучшить образец "Посетитель", представляя целевой класс как родовой параметр класса VISITOR. Убедитесь, шаг за шагом, что полученное решение находится на том же уровне детализации, что и обсуждение в этой лекции. Нужно ли вам все еще множество вариантов VISITOR? Является ли решение полностью повторно используемым? Обсудите достоинства и недостатки решения в сравнении с базисным вариантом VISITOR.
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.