Глава     18

Среды и схемы разработки


Разделы

Содержание

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

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

18.1. Среда разработки

Разделы

Среда разработки═≈ это набор классов, тесно сотрудничающих между собой. Вместе они представляют разработку, предназначенную для многократного использования и пригодную для решения общих проблем. Самое распространенное применение сред разработки заключается в создании графических интерфейсов пользователя или GUI-приложений. Мы подробно рассмотрим одно из таких приложений в главе═19. Однако данная концепция применяется не только при разработке пользовательского интерфейса. Например, существуют среды, которые рассчитаны на построение разнообразных редакторов, компиляторов, финансовых моделей [Gamma═1995, Deutsch═1989, Weinand═1988].

Мы всегда можем абстрагироваться и обсуждать элементы разработки, на которых основывается среда разработки, по отдельности. Однако, как правило, такая среда остается набором специфических классов, которые обычно реализованы только для одной конкретной платформы. В качестве примера можно привести среду Model-View-Controller в языке Smalltalk или библиотеку OWL на PC.

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

Например, Малая Среда Разработки LAF, о которой мы рассказываем в главе═19, содержит примерно дюжину классов. Центральный класс application реализует поведение, ожидаемое от окон: способность к движению и изменению размера. Однако он не реализует функциональность, типичную для конкретного приложения. Ожидается, что пользователь среды создаст новый класс, потомка application. В нем будут переопределены некоторые методы (например, реакция на перемещение мыши или способ перерисовывания окна). Ниже приводится описание класса для карточного пасьянса, подобного тому, что был написан нами в главе═8. Здесь класс разрабатывается на основе LAF.

class cardApp : public application
{
public:
    // конструктор
    cardApp();

    // поведение, специфичное для приложения
    virtual void mouseButtonDown(int x, int y);
    virtual void paint();

private:

    CardPile * piles[13];
};

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

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

В приложениях традиционного типа код, специфичный для конкретной задачи, определяет общий поток вычислений, время от времени вызывая библиотечные процедуры (такие, как математические подпрограммы или операции ввода/вывода) для выполнения некоторых специфических функций.

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

Таким образом, мы наблюдаем смену ролей общего и специфического кодов. Поэтому иногда среда разработки называется ╚библиотекой наизнанку╩ [Wilson═1990].

18.1.1. Java API

Среда разработки

Другим примером среды разработки является Java API (Application Programming Interface, программный интерфейс приложений). Он состоит из классов, используемых программистом для конструирования новых приложений Java, называемых апплетами. Как и в случае со средой LAF, создание нового класса, наследника базового класса API, ≈ это основной механизм включения нового поведения в приложение.

Фундаментальный класс для всех Java-приложений═называется Applet. Он определяет общую структуру приложения через метод main, который обычно не переопределяется программистами. Этот метод вызывает ряд других методов, которые переопределяются, чтобы обеспечить поведение, специфичное для приложения. Некоторые из вызываемых методов вкратце перечислены ниже:

init()                     вызывается при инициализации апплета
start()                    вызывается в начале работы приложения 
paint(Graphics)            вызывается при перерисовке окна  
mouseDown(Event, int, int) вызывается при нажатии кнопки мыши
keyDown(Event, int, int)   вызывается при нажатии клавиши
stop()                     вызывается при удалении окна

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

18.1.2. Среда моделирования

Среда разработки

Для того чтобы проиллюстрировать, что не все среды должны быть связаны с интерфейсами пользователя, мы вкратце рассмотрим среду моделирования, которую можно использовать, например, при моделировании бильярдного шара (см.═главу═6). Как описано в последней части главы, мы могли бы начать с определения всех объектов в модели как подклассов общего класса графических объектов:

GraphicalObject = object
  (* поля данных *)
  link : GraphicalObject;
  region : Rect;

  (* инициализирующие функции *)
  procedure setRegion(left, top, right, bottom : 
    integer);

  (* операции, выполняемые графическими объектами *)
  procedure draw;
  procedure erase;
  procedure update;
  function intersect(anObj : GraphicalObject) : 
    boolean;
  procedure hitBy(anObj : GraphicalObject);
end;

Графические объекты занимают область на экране, знают, как нарисовать самих себя, и сообщают о пересечении с другими объектами. Таким образом, среда моделирования создана, коль скоро определен класс общего назначения для работы с графическими объектами:

GraphicalUniverse = object
  (* поля данных *)
  moveableObjects : GraphicalObject;
  fixedObjects  : GraphicalObject;
  continueUpdate : boolean;

  (* методы *)
  procedure initialize;
  procedure installFixedObject (newObj : GraphicalObject);
  procedure installMovableObject (newObj : GraphicalObject);
  procedure drawObjects;
  procedure updateMovableObjects;
  procedure continueSimulation;
end;

Основа среды моделирования═≈ это подпрограмма для обновления всех перемещаемых объектов. Она просто проходит по списку перемещаемых объектов, сообщая каждому из них о необходимости обновить себя. Если какой-нибудь из объектов просит продолжить цикл обновления (путем вызова процедуры continueSimulation), его просьба удовлетворяется. В противном случае моделирование прерывается:

procedure GraphicalUniverse.updateMovableObjects;
var
  currentObject : GraphicalObject;

begin
  repeat
    continueUpdate := false;
    currentObject := movableObjects;

    while currentObject <> nil do
    begin
      currentObject.update;
      currentObject := currentObject.link;
    end
  until not continueUpdate
end;

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

Другой тип моделирования═╚управляется событиями╩. События хранятся в приоритетной очереди, упорядоченной по ╚времени╩ их возникновения. Соответствующие значения извлекаются из очереди и последовательно выполняются. Каждое событие может вызвать новые события, которые также добавляются в очередь событий. Среда для разработки приложений такого типа описана в [Budd═1994].

18.2. Схемы разработки

Разделы

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

18.2.1. Схемы с посредником

Схемы разработки

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

Передача. Посредник═≈ это агент, который посылает запросы по определенным каналам, например через компьютерную сеть. Фактическое исполнение запроса производит сервер на другом конце сети. Ответ на запрос пересылается обратно. Использование посредника скрывает детали передачи информации, придавая процессу видимость элементарного сообщения.

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

Транслятор (или адаптер). Фактическая работа производится одним объектом, однако протокол, используемый этим объектом, отличается от протокола клиента. Посредник действует как переводчик, переводя сообщения клиента на язык, понятный исполнителю.

Декоратор (он же упаковщик, он же фильтр). В то время как исполнитель выполняет основной объем работ, посредник вносит свою небольшую лепту. При этом интерфейс остается без изменения. К примеру, ╚декоратор╩ может обеспечивать обрамление окна (например, полосы прокрутки), в то время как исполнитель обновляет содержимое окна. Клиент взаимодействует с фильтром, как будто это═≈ исполнитель, хотя основная часть работы фактически выполняется другим объектом. При таком подходе фильтр прозрачен для клиента. Часто фильтр═≈ это менее дорогостоящая и более гибкая альтернатива, чем создание подклассов, поскольку его можно добавить или убрать во время исполнения программы.

Мост (или состояние). В данном случае посредник опять-таки отвечает за интерфейс, но не за выполнение поставленной задачи, которая решается несколькими возможными способами, причем выбор того или иного способа зависит от времени. Посредник вызывает на исполнение подходящий вариант. Использование посредника обеспечивает единый интерфейс, в то время как фактическое выполнение варьируется в зависимости от времени или условий (состояния).

Схемы с посредником чаще всего возникают в ситуации, когда промежуточный объект относительно ╚легковесен╩, то есть он сам не выполняет почти никакой работы, а вместо этого просто передает команды другим агентам. Например, схема интерпретатора, описанная в следующем разделе, могла бы в некотором смысле рассматриваться как разновидность схемы с посредником, поскольку клиент взаимодействует с одним объектом (интерпретатором), который представляет собой фасад гораздо более сложной структуры. Однако интерпретатор по своей природе также довольно ╚тяжеловесен╩. В этом смысле он отличается от остальных схем проектирования, описанных в этом разделе.

18.2.2. Схемы обхода

Схемы разработки

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

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

template <class T> class TreeNode
{
public:
    TreeNode (T & initial);
    void visit (Visitor & v);

private:
    T value;
    TreeNode<T> * left;
    TreeNode<T> * right;
};

Здесь каждый узел содержит значение и указатели на правый и левый узлы нижнего уровня; каждый из указателей может быть нулевым.

Все посетители узлов дерева должны происходить из общего класса Visitor. Они переопределяют виртуальный метод с именем action. Метод visit класса TreeNode осуществляет в определенном порядке просмотр узлов, выполняя для каждого узла действие action. Альтернативой может быть кодирование прохода по узлам со стороны клиента, однако обычно проще (к тому же, это более соответствует идеям ООП) поручить проход собственно дереву:

TreeNode::visit (Visitor & v)
{
  v.action(value);
  if (left != NULL) left->visit(v);
  if (right != NULL) right->visit(v);
}

Изменчивость возникает из-за отложенного метода action. Процедура visit не знает в точности, какое именно действие будет выполняться этим методом. С═другой стороны, схема требует, чтобы все посетители были потомками общего класса Visitor.

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

Данная схема отличается от первой тем, что действие, которое необходимо выполнить, обычно закодировано в самом дереве, а не определяется внешней структурой данных. Например, нашему абстрактному синтаксическому дереву передается текстовая строка. Задается вопрос: соответствует ли строка регулярному выражению? Чтобы ответить на вопрос, каждый узел в выражении производит различные последовательности действий. Узлы-символы соответствуют только точно заданным величинам. Узел выбора проверит по очереди каждую имеющуюся альтернативу, чтобы выяснить, не подходит ли одна из них, и т.═д. Клиент передает строку, которая должна быть проверена на совпадение, корню дерева, а затем получает ответ.

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

Итератор. Одна из проблем со схемой типа ╚посетитель╩ состоит в том, что клиент и структура дерева слишком тесно связаны между собой. Если клиент выполняет обход дерева, то это подразумевает, что он имеет достаточно полное представление о структуре дерева. Если дерево осуществляет обход узлов, то выполняемое действие должно быть ему известно (по крайней мере, в форме отложенного метода). Необходимость согласования между деревом и клиентом может быть уменьшена путем помещения посредника между клиентом и значениями данных.

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

initialize инициализировать итератор корнем дерева
current    вернуть текущее значение
atEnd      вернуть true, если значений больше нет
advance    верейти к следующему значению

Используя итератор, клиент выполняет цикл, аналогичный следующему:

itr.initialize();
while not itr.atEnd() do
begin
  (* некоторое действие со значением itr.current() *)
  ... 
  itr.advance();
end;

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

18.2.3. Схема двойной диспетчеризации

Схемы разработки

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

Представим себе, что у нас есть разные многоугольники, представленные в виде базового класса Shape, и соответствующие подклассы (Triangle, Square и═т.═д.). Предположим, у нас есть два устройства вывода: принтер и терминал. Они представляются подклассами класса Device. Команды, необходимые для выполнения операции вывода на эти устройства, настолько различны, что общий интерфейс невозможен. Вместо этого каждая фигура сама инкапсулирует информацию о том, как печатать ее на принтере и как изображать ее на терминале:

class Triangle : public Shape
{
public:

    Triangle (Point, Point, Point);
    // ...
    virtual void displayOnPrinter (Printer);
    virtual void displayOnTerminal (Terminal);
    // ...

private:
    Point p1, p2, p3;
};

Вопрос: как полиморфную фигуру Shape (или любой ее подкласс) отобразить на полиморфном устройстве типа Device? Ответ состоит в том, что нужно использовать пересылку сообщений для ╚вылавливания╩ одного из двух значений. Для определения обеих величин мы просто по очереди посылаем им сообщение. (Обобщение этой идеи на случай трех и более переменных очевидно═≈ перешлите сообщение каждой неизвестной переменной.)

Например, сначала мы передаем команду с аргументом типа Shape на устройство вывода. Эта команда описана как отложенная в классе Device и переопределяется в каждом его подклассе. Поэтому пересылка сообщения выбирает для выполнения правильную функцию:

function Printer.display (Shape aShape)
begin
  aShape.displayOnPrinter(self);
end;

function Terminal.display (Shape aShape)
begin
  aShape.displayOnTerminal(self);
end;

Обратите внимание: метод display не знает о том, какая фигура рисуется в настоящий момент. Но каждый из методов displayOnPrinter и displayOnTerminal в свою очередь является отложенным (он описан в классе Shape и переопределяется в каждом подклассе). Предположим, что обрабатываемая фигура является треугольником Triangle. К тому времени, когда будет вызван метод класса Triangle, оба источника изменчивости (вид фигуры и тип печатающего устройства) уже привязаны к определенным категориям:

procedure Triangle.displayOnPrinter (Printer p)
begin
 	// код, специфичный для принтера
 	// вывести треугольник
 	// ...
end;

procedure Triangle.displayOnTerminal(Terminal t)
{
 	// код, специфичный для терминала
 	// вывести треугольник
 	// ...
}

Основная сложность, связанная с данной техникой, состоит в большом количестве требуемых методов. Например, каждая фигура должна иметь по методу на каждое печатающее устройство. Несмотря на этот недостаток, данная техника очень эффективна для борьбы с одновременной неопределенностью двух (и более) величин [Ingalls═1986, Budd═1991, LaLonde═1990a, Hebel═1990] 14 .

Комментарий
14 Двойная диспетчеризация впервые была описана Даном Ингалсом [Ingalls 1986], который называл ее множественным полиморфизмом. Альтернативный термин двойная диспетчеризация получил большее распространение, поскольку позволяет избежать путаницы с понятием множественного наследования.

18.2.4. Классификация схем разработок

Схемы разработки

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

В то время как конкретные схемы описываются довольно легко, более сложной проблемой является их категоризация, которая могла бы использоваться для записи и воспроизведения схем в последующих проектах. Большая часть текущей работы в этой области связана с созданием подобных баз знаний [Gamma 1995, Coplien═1995, Pree═1995].

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

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

Упражнения

Разделы

  1. Опишите роль наследования и отложенных методов в среде разработки. Возможно ли создание среды для языка программирования, не являющегося объектно-ориентированным?
  2. Опишите другое приложение, которое могло бы быть создано с помощью графической среды моделирования из подраздела═18.1.2.
  3. Можете ли вы привести другие примеры схемы типа ╚посредник╩?
  4. Объясните, почему инкапсуляция становится более полезной в схеме ╚посетитель╩, если проход дерева осуществляется структурой данных (деревом), а не клиентом?
  5. Объясните, каким образом в языке программирования без строгого контроля типа данных (например, Smalltalk) для выполнения арифметических действий с операндами смешанного типа может быть использована техника двойной диспетчеризации. Как, в частности, сложить два числа, каждое из которых может быть как целым, так и вещественным?

Среды и схемы разработки