
Из теории алгоритмов и автоматов известно понятие конечного автомата, которое описывает поведение некоторой абстрактной модели в терминах конечного набора состояний и событий, инициализирующих переходы между этими состояниями. На основании этой теории была развита достаточно распространенная сейчас парадигма автоматного программирования, при использовании которой задача решается в терминах конечных автоматов — т.е. в терминах состояний и событий. В явном виде, данная парадигма широко применяется в языках программирования, при построении лексеров. Однако, можно найти огромное количество примеров программных систем, в некотором смысле реализованных на основе данной парадигмы.
В статье рассматриваются особенности применения шаблонов Visitor/Double Dispatch и State при реализации системы на основе конечного автомата. Кроме того, статью можно рассматривать как продолжение цикла публикаций о шаблонах проектирования на Хабрахабре.
Мотивация
Автоматное программирование — весьма удобный и гибкий инструмент, который позволяет решать поставленную задачу в терминах, близких понятиям предметной области. Например, задача о программировании поведения лифта в многоэтажном доме превращается в весьма формализованную модель автомата со следующим состояниями: “Лифт едет вверх”, “Лифт едет вниз”, “Лифт стоит на этаже N” и приходящими от жильцов дома событиями: “Нажата кнопка Вниз на N-ом этаже” и “Нажата кнопка Вверх на N-ом этаже”.
Однако на ряду с очевидными преимуществами такого подхода существует один небольшой недостаток — кодирование подобной системы представляет собой весьма неприятный процесс, сопровождаемый использованием большого количества ветвлений и переходов.
Для решения этой проблемы существует ООП и шаблоны Visitor и State.
Пример
Рассмотрим следующую задачу. Пусть требуется спроектировать и реализовать примитивную автомагнитолу, которая поддерживает два режима работы — “радио” и “CD-плеер”. Переключение между режимами осуществляется с помощью тумблера на панели управления (см. рисунок в начале статьи). Кроме того, магнитола поддерживает механизм переключения радиостанций или треков (кнопка “Next”), в зависимости от выставленного режима.
Отличный пример того как данная задача решается на основе вложенных ветвлений (switch) языка Си можно посмотреть в Википедии. Рассмотрим его наиболее значимый участок.
switch( state ) {
case RADIO:
switch(event) {
case mode:
/* Change the state */
state = CD;
break;
case next:
/* Increase the station number */
stationNumber++;
break;
}
break;
case CD:
switch(event) {
case mode:
/* Change the state */
state = RADIO;
break;
case next:
/* Go to the next track */
trackNumber++;
break;
}
break;
}
Код из примера абсолютно не читаем и тяжело расширяем. Например, добавление нового состояния или события представляется весьма трудоемкой операцией, требующей модификацию большого количества кода. Кроме того, такой спагетти-код провоцирует разработчика дублировать некоторые участки (возможна ситуация, при которой в разных состояниях одно и тоже событие должно обрабатываться одинаково).
Шаблон Visitor и его частный случай Double Dispatch призван решить данную проблему, путем разделения понятий состояния и обработчика. При этом, конечная реализация алгоритма обработки события выбирается в процессе исполнения, на основе двух факторов: типа события и типа обработчика (отсюда название — “Двойная Диспетчеризация”).
Таким образом, для добавления нового типа события или обработчика в систему, достаточно лишь реализовать требуемый класс, наследника классов “Событие” или “Обработчик” соответственно.
Диаграмма классов
Основные сущности системы:
- Gramophone — магнитола, которая может включаться — enable(), выключаться — disable() и принимать события — dispatch(event);
- GramophoneEvent — интерфейс возможных событий с одним единственным методом — apply(handler) для “применения” обработчика к событию;
- GramophoneHandler — интерфейс обработчика, который содержит полиморфные методы (handle) обработки всех существующих в системе событий;

Наибольший интерес в рассмотренной диаграмме представляет интерфейс GramophoneHandler, который одновременно является частью конструкции Visitor (в качестве самого посетителя) и самодостаточной конструкцией State (в качестве состояния для Gramophone). Т.е. можно считать, что в рассмотренном примере используется своего рода композиция двух шаблонов.
Реализация
Один из вариантов использования системы будет выглядеть следующим образом:
public static void main(String args[]) {
Gramophone gramophone = new Gramophone(); // it's CD by default
gramophone.enable(); // start event loop
gramophone.dispatch(new ToogleEvent()); // toogle to radio
gramophone.dispatch(new NextEvent()); // next station
gramophone.dispatch(new ToogleEvent()); // toogle to CD player
gramophone.dispatch(new NextEvent()); // next CD track
gramophone.disable(); // stop event loop
}
Опишем возможные варианты внешних событий, приходящих системе.
/**
* Events
*/
interface GramophoneEvent {
void apply(GramophoneHandler handler);
}
class ToogleEvent implements GramophoneEvent {
@Override
public void apply(GramophoneHandler handler) {
handler.handle(this);
}
}
class NextEvent implements GramophoneEvent {
@Override
public void apply(GramophoneHandler handler) {
handler.handle(this);
}
}
В рассмотренном коде, метод apply() имеет одинаковую реализацию во всех потомках. В этом заключается основная идея шаблона — полиморфное определение типа события в процессе исполнения кода. Т.е. у обработчика будет вызываться метод handle() в зависимости от типа самого события (типа ссылки this).
В языках, не поддерживающих полиморфизм (например в JavaScript), можно инкапсулировать информацию о типе обрабатываемого события в названии метода. Тогда в нашем случае методы буду выглядеть как handleNext(event) и handleToogle(event) а вызывающий код так:
var NextEvent = function() {
this.apply = function(handler) {
handler.handleNext(this);
}
}
Реализация возможных состояний системы представляет собой следующий код. В нашем случае состояние = обработчик.
/**
* Visitor
*/
interface GramophoneHandler {
void handle(ToogleEvent event);
void handle(NextEvent event);
}
class RadioHandler implements GramophoneHandler {
private Gramophone gramophone;
public RadioHandler(Gramophone gramophone) { this.gramophone = gramophone; }
@Override
public void handle(ToogleEvent event) {
gramophone.toogle(new CDHandler(gramophone));
}
@Override
public void handle(NextEvent event) {
gramophone.nextStation();
}
}
class CDHandler implements GramophoneHandler {
private Gramophone gramophone;
public CDHandler(Gramophone gramophone) { this.gramophone = gramophone; }
@Override
public void handle(ToogleEvent event) {
gramophone.toogle(new RadioHandler(gramophone));
}
@Override
public void handle(NextEvent event) {
gramophone.nextTrack();
}
}
Наконец расмотрим реализацию основного класса системы — магнитолы (Gramophone).
/**
* FSM (Finit-State-Machine) implementation
*/
class Gramophone implements Runnable {
private GramophoneHandler handler = new CDHandler(this);
private Queue<GramophoneEvent> pool = new LinkedList<GramophoneEvent>();
private Thread self = new Thread(this);
private int track = 0, station = 0;
private boolean started = false;
public void enable() {
started = true;
self.start();
}
public void disable() {
started = false;
self.interrupt();
try { self.join(); } catch (InterruptedException ignored) { }
}
public synchronized void dispatch(GramophoneEvent event) {
pool.offer(event);
notify();
}
@Override
public void run() {
for (;!pool.isEmpty() || started;) {
for (;!pool.isEmpty();) {
GramophoneEvent event = pool.poll();
event.apply(handler);
}
synchronized (this) {
try { wait(); } catch (InterruptedException ignored) { }
}
}
}
void toogle(GramophoneHandler handler) {
this.handler = handler;
System.out.println("State changed: " + handler.getClass().getSimpleName());
}
void nextTrack() { track++; System.out.println("Track changed: " + track); }
void nextStation() { station++; System.out.println("Station changed: " + station); }
}
В рассмотренной реализации методы toogle(), nextTrack() и nextStation() имеют область видимости только внутри пакета. Это сделано для того, чтобы обезопасить систему от прямых внешних вызовов. Более того, в реальных условиях рекомендуется дополнительно проверять природу вызывающего потока. Например в каждый из методов можно добавить следующий код проверки.
void nextTrack() {
if (Thread.currentThread() != self) {
throw new RuntimeException(“Illegal thread”);
}
track++;
System.out.println("Track changed: " + track);
}
Кроме того, следует обратить внимание на метод run(), который реализует идиому Event Loop. В данном случае, метод содержит два вложенных цикла, обеспечивающих засыпание потока при отсутствии событий в очереди. При этом, добавление каждого нового события в очередь (см. метод dispatch()) его пробуждает.
Заключение
Статья не является пропагандой использования ООП и шаблонов проектирования, а лишь раскрывает особенности использования перечисленных инструментов для решения конкретной задачи.
Код рассмотренного примера доступен на GitHub. Там же можно найти примеры на другие паттерны, о которых уже было написано несколько статей на Хабрахабре.