14.4 Реализация — Ic |
Предыдущая Содержание Следующая |
Очевидно, gate() является динамически компонуемым методом, потому что подклассы Ic используют его, чтобы получить и обработать информацию. Сам Ic владеет out, указателем на другой объект, в который должна быть отправлена информация. Сам по себе Ic главным образом абстрактный базовый класс, но Ic_gate() может обращаться к out и даже передавать информацию:
% Ic gate { %casts return self -> out ? gate(self -> out, item) : reject; }
Это мотивировано сокрытием информации: если метод подкласса gate хочет переслать информацию, он может просто вызвать super_gate(). wire() тривиален: он соединяет Ic с другим объектом, храня адрес объекта в out:
% Ic wire { %casts self -> out = to; }
После изобретения класса мультиплексора Mux мы понимаем, что wire() также должен быть динамически компонуемым. Mux перезаписывает wire(), чтобы добавить свой целевой объект в List:
% Mux wire { // добавляем другой приёмник %casts addLast(self -> list, to); }
Mux_gate() может быть определён разными способами. Вообще-то он должен предложить входящую информацию некоторым целевым объектам; однако, можно выбрать порядок, в котором это делается, и хотим ли мы выйти, как только объект принимает информацию — есть место для подклассов!
% Mux gate { // посылает первому ответчику unsigned i, n; enum react result = reject; %casts n = count(self -> list); for (i = 0; i < n; ++ i) { result = gate(lookAt(self -> list, i), item); if (result == accept) break; } return result; }
Это решение проходит последовательно через список в порядке, в котором он создавался, и завершается, как только один из вызовов gate() возвращает accept. LineOut необходим, чтобы мы могли протестировать микросхему для вычислений без подключения к графическому интерфейсу пользователя. gate() был определён достаточно гибко, чтобы LineOut_gate() был немного большим, чем вызовом puts():
% LineOut gate { %casts assert(item); puts(item); // надеемся, что это строка return accept; }
Конечно, LineOut был бы более надёжным, если бы мы использовали на входе объект String. Класс, созданные к настоящему времени, уже могут быть протестированы. Следующий пример hello соединяет Ic с Mux и оттуда сначала с двумя другими объектами Ic, а затем дважды с LineOut. Наконец, строка отправляется первому Ic:
int main () { void * ic = new(Ic()); void * mux = new(Mux()); int i; void * lineOut = new(LineOut());
for (i = 0; i < 2; ++ i) wire(new(Ic()), mux); wire(lineOut, mux); wire(lineOut, mux); wire(mux, ic); puto(ic, stdout); gate(ic, "hello, world"); return 0; }
Вывод показывает соединения, описываемые puto() и строку, отображаемую LineOut:
$ hello Ic at 0x182cc wired to Mux at 0x18354 wired to [nil] list List at 0x18440 dim 4, count 4 { Ic at 0x184f0 wired to [nil] Ic at 0x18500 wired to [nil] LineOut at 0x184e0 wired to [nil] LineOut at 0x184e0 wired to [nil] } hello, world
Хотя объект Mux соединён с объектом LineOut дважды, hello, world выводится только один раз, потому что объект Mux передаёт свою информацию только пока какой-нибудь gate() возвращает accept. Прежде чем мы сможем реализовать Button, необходимо сделать несколько предположений о классе Event. Объект Event содержит информацию, которая обычно отправляется от одного Ic другому. Информация от клавиатуры может быть представлена как строка, но щелчок мыши или позиция курсора выглядит по-другому. Поэтому мы позволяем Event содержать число kind, чтобы характеризовать информацию, и указатель data, который скрывает фактические значения:
% Event ctor { // new(Event(), 0, "text") и тому подобное. struct Event * self = super_ctor(Event(), _self, app);
self -> kind = va_arg(* app, int); self -> data = va_arg(* app, void *); return self; }
Теперь мы готовы разработать Button в виде фильтра информации: если входящий Event — строка, он должен соответствовать тексту кнопки; любая другая информация принимается без предварительного просмотра, поскольку она уже должна была быть проверена в другом месте. Если Event будет принят, Button пошлёт свой текст:
% Button ctor { // new(Button(), "text") struct Button * self = super_ctor(Button(), _self, app);
self -> text = va_arg(* app, const char *); return self; }
% Button gate { %casts if (item && kind(item) == 0 && strcmp(data(item), self -> text)) return reject; return super_gate(Button(), self, self -> text); }
Это также может быть проверено маленькой тестовой программой button, в которой объект Button соединён с LineOut:
int main () { void * button, * lineOut; char buf [100];
lineOut = new(LineOut()); button = new(Button(), "a"); wire(lineOut, button); while (gets(buf)) { void * event = new(Event(), 0, buf);
if (gate(button, event) == accept) break; delete(event); } return 0; }
button игнорирует все входные строки, кроме строки, содержащей a, которая является текстом кнопки:
$ button ignore a a
Только одна строка a введена, другая распечатана LineOut. LineOut и Button были реализованы главным образом для того, чтобы проверить микросхему для вычислений до того, как она будет соединена с графическим интерфейсом. Микросхема для вычислений Calc может быть настолько сложной, как только мы пожелаем, но для начала будем придерживаться очень примитивного подхода: цифры собираются на экране в значение; арифметические операторы выполняются так быстро, как только возможно, без приоритета; = выдаёт результат; C сбрасывает текущее значение; а Q завершает программу, вызывая exit(0);. Этот алгоритм может быть выполнен конечным автоматом. Практический подход использует переменную state в качестве индекса, выбирающего одно из двух значений, и переменную op, чтобы запомнить текущий оператор, который будет применён после завершения ввода следующего значения:
%prot typedef int values[2]; // стек с левым и правым операндами
% IcClass Calc: Ic { values value; // левый и правый операнды int op; // оператор int state; // состояние конечного автомата %}
Следующая таблица показывает алгоритм, который должен быть закодирован:
И он действительно работает:
$ run 12 + 34 * 56 = Q 1 12 3 34 46 5 56 2576
ВыводыКлассы Ic очень просто реализовать. Тривиальный LineOut и входной цикл, который читает данные от клавиатуры, позволяют нам проверить Calc до того, как он будет встроен в сложный интерфейс. Calc осуществляет связь с кнопками ввода и экраном вывода, вызывая gate(). Это связь достаточно свободна, поэтому Calc может отправлять меньше (или даже больше) сообщений на экран, чем получает сам. Calc работает строго снизу вверх, то есть он реагирует на каждый ввод, переданный в него через Calc_gate(). К сожалению, это исключает метод рекурсивного спуска, представленный в третьей главе или другие управляемые синтаксисом механизмы, такие как грамматика yacc, но это характерно для основанного на сообщениях подхода. Рекурсивный спуск и подобные механизмы запускаются из основной программы вниз, и они сами решают, когда стоит посмотреть на ввод. В противоположность им управляемые сообщениями приложения используют основную программу в качестве цикла для получения событий, и объекты должны реагировать на те события, которые им отправляются. Если мы настаиваем на нисходящем подходе для Calc, то должны дать ему собственный поток управления, то есть это должна быть сопрограмма, поток в Mach и аналогичных системах, или даже другой процесс под UNIX, и парадигма сообщений должна быть заменена процессом коммуникации.
| ||||||||||||||||||||||||||||||||||||||||||||||
Предыдущая Содержание Следующая |