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

Агенты как функциональный тип данных

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

Мир без агентов

Нельзя понять по-настоящему агентов, если не задаться вопросом: а как пришлось бы действовать в случае отсутствия подобного механизма?

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

Интегрирование типичный случай. При определении функции, выполняющей интегрирование, integral, введем аргумент, задающий подынтегральную функцию, типа INTEGRATABLE_FUNCTION. Соответствующий класс будет отложенным классом, который может выглядеть так:

note
  description: " Функции, допускающие интегрирование на конечном интервале"
deferred class INTEGRATABLE_FUNCTION feature
  item (x: REAL): REAL
      — Значение функции в точке x.
    Deferred
    end
end

Можно спроектировать более изощренную форму этого класса, например, добавив запрос defined(x: REAL) и использовать его в предусловии item, но этой простой версии достаточно для понимания архитектурных проблем.

Классы для математических функций

Рис. 5.3. Классы для математических функций

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

    - В классе COSINE_FUNCTION
item (x: REAL): REAL
    - Значение функции в точке x.
  do
    Result := cosine (x)
  end

Тогда для получения интеграла от функции cos(x) на интервале [a,b] необходимо объявить переменную f:INTEGRATABLE_FUNCTION, убедиться, что динамически она присоединена к объекту типа COSINE_FUNCTION, и вызвать

your_integrator.integral(f)
Листинг 5.4.

Функцию integral(f:INTEGRATABLE_FUNCTION) написать несложно, используя любой алгоритм численного вычисления интегралов. Всякий раз, когда необходимо вычислить значение функции в некоторой точке x, используется вызов f.item(x). Заметьте роль динамического связывания: тип f во время выполнения, такой как COSINE_FUNCTION, определяет, какая подынтегральная функция (item) будет использована. Чтобы убедиться, что вы понимаете схему и фундаментальные ОО-приемы программирования, — хорошая идея написать integral самостоятельно.

Время программирования!

Библиотека интегрирования без агентов

Напишите класс INTEGRATOR с методом integral, который вычисляет интеграл на конечном интервале от функции, передаваемой в качестве аргумента типа INTEGRATABLE_FUNCTION. Спроектируйте подходящих потомков INTEGRATABLE_FUNCTION, примените вашу работу для вычисления интегралов различных функций. В качестве простого алгоритма интегрирования можно использовать модель, описанную ниже, для версии интегрирования, основанной на агентах1 Можно построить и более простую версию, рассматривая отложенный класс INTEGRATOR как "программу с дырами". Вместо того чтобы вводить отдельный класс INTEGRATABLE_FUNCTlON, достаточно в классе INTEGRATOR ввести отложенный метод integrated_function - аналог item. Потомки класса будут переопределять эту функцию, задавая таким образом нужную им подынтегральную функцию. В этом варианте у метода integral нет необходимости вводить аргумент f..

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

  • Для итерации можно создать отложенный класс ITERATABLE_ACTION, эффективные потомки которого обеспечат специфическую версию процедуры call, описывающей выполнение итерируемой операции.
  • Для наблюдателей события (программирование, управляемое событиями) отложенным классом является класс OBSERVER, потомки которого и обеспечивают свою версию метода update, вызываемую издателем при возникновении события. Хорошо известный образец "Наблюдатель" (OBSERVER) будет обсуждаться в следующей лекции.
  • Для откатов-повторов (Undo-Redo) для каждой команды интерактивной системы следует создать класс с двумя методами: execute, выполняющий команду, и cancel, выполняющий откат, — устраняя эффект последнего выполнения execute. Экземпляр этого класса описывает информацию, появляющуюся в результате однократного выполнения команды, и необходимый откат, выполняемый в случае запроса. Например, в текстовом редакторе экземпляр LINE_DELETION имеет два поля: содержимое строки перед удалением и позицию этой строки в тексте, — так что метод cancel сможет восстановить строку, удаленную при выполнении команды execute. Все такие командные классы наследуют от отложенного класса COMMAND, в котором execute и cancel являются отложенными методами. Список истории может быть реализован, например, как LINKED_LIST[COMMAND]. Это схема еще одного классического образца проектирования — COMMAND.

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

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

COSINE_FUNCTION, присоединенный к f в [5.4]. Класс с одним экземпляром известен как "Одиночка" (Singleton). Но здесь объект не только присутствует в единственном экземпляре, но у него и полей вообще нет — странный объект, в самом деле. Каждый из классов представляет капсулу с одной единственной процедурой. Мы можем называть такие классы "певец одной песни".

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

Среди наших примеров один из них — откаты и повторы — не кажется с этих позиций искусственным, поскольку абстракция COMMAND обоснована. У нее есть два важных метода — execute и cancel, так что у певца есть, по крайней мере, две песни, есть атрибуты, так что потомки описывают имеющие смысл различающиеся экземпляры.

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

Таким механизмом являются агенты. Если f — это метод, то нетрудно получить в качестве подарка этот метод в красивых одеждах, просто написав agentf Мы получим объект, обладающий всем, что есть у f, включая возможность вызова f (используя call ) для любых применимых аргументов, который можно вызывать всюду и всякий раз, когда это понадобится. Во всех рассмотренных случаях и во многих других агенты являются соперниками образца "много маленьких оберток". Надеюсь, маленькое отступление — как обойтись без агентов — дает нам лучшее понимание преимуществ простого, встроенного механизма, позволяющего работать с действиями, как с объектами.

Агенты для итерации

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

Базисные схемы итерирования

Простой пример из Traffic иллюстрирует использование и определение итератора через агенты. Рассмотрим понятие маршрута ROUTE. Мы можем добавить в ROUTE метод do_at_every_stop, который принимает действие как аргумент и применяет его к каждой остановке. Это делает возможным вызовы:

your_route.do_at_every_stop (agent print_stop_name)
your_route.do_at_every_stop (agent append_restaurants)
...
your_route.do_at_every_stop (agent other_operation)
Листинг 5.5.

Предполагается, что print_stop_name, append_restaurants, other_operations являются методами, принимающими STOP в качестве аргумента.

Как должен выглядеть метод do_at_every_stop, чтобы все эти вызовы стали возможными? Он абстрагирует стандартную схему итерации, приведенную в этой лекции [3]:

do_at_every_stop (action :...)
    — Применить действие к каждой остановке данного маршрута.
  do
    from start until after loop
      action.call ([item])         [6]
      forth
    end
end
Листинг 5.6.

Для включения ассоциированного метода используется вызов call — процедура, доступная всем агентам, чей эффект состоит в вызове метода агента с заданными аргументами; более точно, аргументом является единственный кортеж, здесь [item]. Как вы знаете, последовательность значений, заключенная в квадратные скобки, представляет манифестный кортеж2 В Eiffel манифестными называются неименованные константы, заданные своими значениями, такие, как 17 или "это константа". Тип манифестных констант однозначно определяется синтаксисом их записи. Манифестный кортеж [item] однозначно позволяет установить, что это константа, задающая кортеж с одним элементом..

Так как предполагается, что action представляет метод, такой как print_stop_name, у которого один аргумент, кортеж, используемый здесь, [item], имеет ровно один элемент.

Эффект от вызова action.call([item]) точно такой же, как и при прямом вызове соответ ствующего метода, такого как

print_stop_name (item)
Листинг 5.7.

Это справедливо при условии, что аргумент, переданный do_at_every_stop, был agent print_stop_name. Если же аргумент — agent append_restaurants, то это соответствует вызову

append_restaurants (item)
Листинг 5.8.

Разница с [5.6] в том, что внутри do_at_every_stop мы ничего не знаем, какой фактический метод задает action.

Эта техника является базисным механизмом итераторов в библиотеке EiffelBase. Далее мы, фактически, будем изучать библиотечную реализацию.

Итерирование для исчисления предикатов

Интересное приложение итерирования состоит в непосредственной реализации механизмов исчисления предикатов: кванторов "Для всех" (∀) и "Существует" (∃). Предположим, что вы хотите установить, что все элементы массива целых a в границах a.lover и a.upper являются положительными. В исчислении предикатов это выражается записью:

∀ s: a.lower .. a.upper | a [i] > 0
Листинг 5.9.

Без агентов вы могли бы использовать all_positive(a), написав функцию all_positive(ia: ARRAY[INTEGER]), получающую результат, выполняя цикл. С агентами нет необходимости в такой функции, можно написать проще:

(a.lower|..|a.upper).for_all (agent is_positive))
Листинг 5.10.

Такая запись по стилю близка к предикатной записи [5.9]. Символика |..| является операторным псевдонимом (alias) функции interval из класса INTEGER. Результат выполнения данной функции принадлежит типу INTEGER_INTERVAL — библиотечному классу, содержащему функции for_ all и exist. Данные конкретные функции for_ all и exist применимы к массивам, но вскоре мы познакомимся с аналогичными функциями, применимыми к спискам и другим последовательным структурам.

В [5.10] все же требуется проверка на положительность и запрос is_positive(n:INTEGER): BOOLEAN. Это более разумно, чем требовать нечто подобное all_positive для каждого такого случая. В конце лекции мы научимся, как избавиться даже от is_positive, написав нужный агент без введения явной процедуры.

Время проведения исследований!
Есть смысл теперь взглянуть на функции for_ all и exist, как они заданы в классе INTEGER_INTERVAL. Пусть вас не смущает объявление типов (о них мы еще поговорим), но убедитесь, что вы понимаете реализацию. Посмотрите также и на функцию exist1.
< Лекция 5 || Лекция 6: 1234 || Лекция 7 >
Надежда Александрова
Надежда Александрова

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