Объектно-ориентированное программирование и программная инженерия

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

Показывать лекцию целиком

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

Полиморфные структуры данных

У нас уже появлялся пример сотрудничества этих механизмов — полиморфные структуры данных. Рассмотрим контейнер, такой как:

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). Вся конструкция в целом задает ограничение универсальности. Смысл ограничения в том, что теперь родовое порождение SORTER[T] является правильным только при условии, что T удовлетворяет ограничению. Так что SORTER[INTEGER] и SORTER[STRING] прекрасно подходят, так же как и SORTER [ TENNIS_PLAYER], при условии, что TENNIS_PLAYER наследует от COMPATIBLE. Но не подходят SORTER [COMPLEX] и SORTER [VEHICLE], если мы только не сделаем транспортные средства сравнимыми. Неподходящие случаи будут отвергнуты на этапе компиляции.

    Как вы догадываетесь, базисный случай 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. Пример немного экстремальный, но требование сравнимости элементов и возможности выполнять над ними числовые операции встречается достаточно часто.

    Ограниченная универсальность иллюстрирует фундаментальную роль типов в современном программировании. Для решения обсуждаемых проблем возможны и другие подходы, такие как передача процедуры, выполняющей сравнение, методам класса SORTER. Но данный подход наиболее согласуется с ОО-идеями — сделать требуемую функциональность частью типа. Это также означает, что по-прежнему можно полагаться на компилятор, который будет выполнять все необходимые проверки корректности, что позволит избежать появления подобных ошибок в период выполнения. Если компилятор отвергает ваш класс из-за несогласованности типов, помните, что эта новость, кажущаяся плохой, по-настоящему является хорошей, — лучше вы поймаете "жучка", чем он поймает вас.

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

    Обнаружение фактического типа

    Помните вопрос, задаваемый в начале этой лекции: "Что, если я знаю, что последний элемент списка является экземпляром 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, то выражение будет правильной конструкцией, но не выражение, так как нарушается правило полиморфизма типов: потомку нельзя присваивать родительский объект — это противоречит направлению согласования типов.
  • Конструкция теста объекта обеспечивает решение в таких случаях. Тест объекта — это булевское выражение, которое (как форма RTTI) определяет, согласован ли тип объекта с ожидаемым типом. В случае согласования возникает дополнительный эффект — появление локальной переменной, присоединенной к объекту. Применяя тест объекта в нашем примере, получим:

    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, то делай это, иначе, если TRAM, то делай то, иначе, если BUS.".

    Детали, кстати, должны быть тщательно учтены — по той причине, что тесты используют согласование типов. Если выполняется разбор случаев MOVING -объектов, то следует помнить, что, например, объект TRAM согласован также с типом VEHICLE, так что порядок тестов играет значение.

    Все же вы можете делать это.

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

    В таких случаях часто приведение делается к одному типу, не выполняя проверки всех возможных случае"приведение вниз"Разумно используйте динамический кастинг. Это довольно хороший критерий использования динамического кастинга. Если ожидается некоторый тип объекта и выясняется, что реальность согласована с ожиданиями, то все прекрасно. Если же приходится делать выбор, анализируя весь возможный набор типов, то почти наверняка динамический кастинг используется не по назначению и его место должно занять динамическое связывание.

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

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

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

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

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

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

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

    Предположим, например, что в интересах ряда приложений в систему 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, обеспечивает повторно используемый вариант "Посетителя", основанный на этом подходе.

    Дальнейшее чтение

  • Бертран Мейер: "Объектно-ориентированное конструирование программных систем" Изд. Русская Редакция, Интернет-Университет, Москва, 2005 г. Содержит детальный анализ наследования на протяжении нескольких глав.
  • Бертран Мейер, Карин Арнаут: "Componentization: the Visitor Example", Computer (IEEEE), vol. 39, no. 7, July, 2006, pages 23-30 также доступна в интернете: se.ethz.ch/'meyer/publications/computer/visitor.pdf
  • Образец "Посетитель" дополняет динамическое связывание, позволяя просто добавлять операцию в множество существующих типов (в противоположность обращенной задаче). Эта статья представляет образец, предлагая реализующий его повторно используемый компонент, который является частью библиотеки образцов ETH. В литературе можно найти много описаний образца "Посетитель" (включая Википедию), большинство которых используют в качестве источника описание из книги E. GAMMA и др., Design Patterns, Addison-Wesley, 1994.

    Ключевые концепции этой лекции

  • Наследование означает, что некоторый класс (наследник) получает компоненты и инвариант от другого класса (родителя). Экземпляры наследника могут быть обработаны таким же способом, что и экземпляры родителя.
  • Согласование распространяет на типы отношение "быть потомком" между классами.
  • Совместно с универсальностью наследование помогает определить изощренную систему типов, которая превращает компилятор в инструментарий, строящий доказательства и проверяющий свойства согласования элементов программной системы.
  • Полиморфизм, применяемый только к ссылкам, позволяет выражению, имеющему "статический" тип по объявлению, обозначать объекты разных типов (их динамические типы) во время выполнения программы. Система типов гарантирует, что все динамические типы являются потомками статического типа.
  • Форма полиморфизма, основанная на универсальности, позволяет определять контейнерные структуры, которые во время выполнения могут наполняться объектами разных типов.
  • Динамическое связывание гарантирует, что в присутствии полиморфизма любой вызов компонента всегда использует версию, наилучшим образом адаптированную к динамическому типу цели.
  • Полиморфизм и динамическое связывание используют преимущества скрытия информации, позволяя клиентам игнорировать точный тип объектов, которые они обрабатывают, и не знать, какая именно версия операции была применена. У них нет необходимости в знании этой информации.
  • Правила типизации ограничивают полиморфизм: тип любого объекта, к которому может быть присоединена во время выполнения переменная (динамический тип переменной), должен быть согласован с объявленным (статическим) типом. Это требует, чтобы при любом присваивании и передаче аргументов методу тип источника был согласован с типом цели.
  • Отложенные компоненты имеют спецификацию, включающую сигнатуру и возможные контракты, но не имеющие реализации. Класс отложен, если он содержит, по меньшей мере, один отложенный компонент, хотя другие компоненты могут быть эффективными (не быть отложенными). Отложенные компоненты позволяют задавать абстракции высокого уровня и, в частности, полезны при проектировании подходящей таксономии. Нельзя создавать экземпляры отложенных классов.
  • Класс может переопределить наследуемый компонент, чтобы обеспечить другую его реализацию, отличающуюся от реализации родителя. Это позволяет комбинировать повторное использование и адаптацию.
  • Универсальность и наследование являются дополняющими механизмами для расширения типа. Универсальность обеспечивает параметризацию типов, наследование обеспечивает обобщение и специализацию.
  • Ограниченная универсальность делает возможным применять специальные операции к переменным формального универсального типа, требуя, чтобы все соответствующие родовые параметры были согласованы с данным типом, заданным ограничением универсальности.
  • Множественное наследование дает классу дополнительные преимущества в результате комбинирования нескольких абстракций. Это простая и эффективная техника. Во избежание неоднозначности любой конфликт имен компонентов должен быть устранен в момент наследования.
  • Повторное наследование возникает в результате множественного наследования, когда класс является потомком другого класса более чем по одному пути наследования. Повторно наследуемые компоненты сливаются, если они наследуются под одним и тем же именем, и сохраняются раздельно в противном случае. Любая потенциальная неоднозначность при динамическом связывании разрешается благодаря введению предложения select.
  • Архитектура программной системы, обеспечивающая гладкую эволюцию, должна строиться так, чтобы минимизировать объем знаний, необходимый каждой части системы для взаимодействия с другими частями.
  • Динамическое связывание обеспечивает прекрасное решение эволюции ПО в случае подключения новых типов, имеющих собственные варианты "старых" операций.
  • В случае новых операций у "старых" типов широко используемое решение задает образец "Посетитель". Он основан на объектах-"посетителях", действующих в качестве посредников между клиентскими классами и целевыми классами. Возможно построение более общего, полностью повторно используемого решения. Оно основано на механизме агентов.
  • Новый словарь

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

    AncestorПредок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Параметрический полиморфизм
    Polymorphic expressionПолиморфное выражениеPolymorphismПолиморфизм
    Polymorphic data structureПолиморфная структура данныхPrecursorPrecursor (версия родителя)
    Programs with Holes patternПрограммы с дырамиProper ancestor, descendantПодходящий (правильный) предок, потомок
    RedeclarationПереобъявлениеRedefinitionПереопределение
    RefinementУточнениеRepeated inheritanceПовторное наследование
    Replication (of a feature)Репликация компонентаRoutine tableТаблица методов
    RTTI (Run-Time Type Identification)RTTI (идентификация типа во время выполнения)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 end
  • Составной оператор является последовательностью операторов. В конкретном синтаксисе последовательность берется в скобки do... end
  • Программа является составным оператором.
  • Используя конкретный синтаксис, напишите программу на L0, которая получает от пользователя два целых и вычисляет наибольший общий делитель — НОД, применяя алгоритм Эвклида и не используя умножение (вы можете написать другие L0-програм-мы как примеры для следующих вопросов).
  • Спроектируйте множество классов — такFLASH_VISITOR378442их как PROGRAM, COMPOUND и так далее, позволяющих создавать представление абстрактного синтаксиса L0-программ. Используйте наследование подходящим образом. Убедитесь, что классы содержат подходящие процедуры создания, так, чтобы L0-программы были представлены как структуры объектов без конкретного синтаксиса.
  • Напишите программу, которая создает абстрактное синтаксическое дерево (АСТ) для программы вычисления НОД (и любого другого примера, который вы подготовили, выполняя пункт 1).
  • Разбор АСТ

    (Это продолжение предыдущего упражнения. Его задача — создать запись текста программы в конкретном синтаксисе по АСТ. Это задача, обратная к парсингу)

    Напишите программу, которая печатает конкретное представление с конкретным синтаксисом, описанным в предыдущем упражнении. L0-программа дается как экземпляр класса PROGRAM и ассоциированных объектов (экземпляров COMPOUND и так далее). Каждый оператор должен начинаться на новой строке. Для составного оператора предусмотрите вложенность. Ветви условного оператора и тело цикла даются с отступами.

    Проверьте результаты вычислений и убедитесь, что программа работает корректно.

    Интерпретатор, работающий на абстрактном синтаксисе

    (Это продолжение последних четырех упражнений)

    Напишите интерпретатор — программу на Eiffel, которая может выполнять любую L0-программу, представленную в виде экземпляра класса PROGRAM и ассоциированных объектов (экземпляров COMPOUND и так далее). Семантика L0-такова:

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

    Компилятор, работающий на абстрактном синтаксисе

    (Это продолжение предыдущих упражнений)

    Напишите компилятор — программу на Eiffel, которая транслирует любую L0-программу в Eiffel-систему в форме корневого класса и множества вспомогательных классов при необходимости. Проверьте, что результаты работы компилятора и интерпретатора совпадают.

    Как много такси

    Образцом для этого упражнения может служить функция pre_taxi_count. Рассмотрите fleet: LIST[VEHICLE].

  • Напишите функцию, которая вычисляет число экземпляров такси в списке транспортных средств.
  • Напишите функцию, которая вычисляет число прямых экземпляров такси в списке транспортных средств.
  • Универсальный "Посетитель"

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

    Вернуться к учебному плану