проблема проектирования java generics (конечный автомат)

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

class State { ... }

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

class Message { ... } 

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

class Handler { ... } 

каждый обработчик создается только один раз и имеет дело с определенной комбинацией состояния/сообщения.

class StateMachine { ... }

в настоящее время отслеживает текущее состояние и список всех сопоставлений (State,Message) -> Handler. у него есть и другие функции. я пытаюсь сохранить этот класс общим и подклассировать его с параметрами типа, поскольку он использовался в моей программе несколько раз, и каждый раз с другим набором Message / State / и Handler. разные StateMachine будут иметь разные параметры для своих обработчиков.

Подход А

пусть конечный автомат отслеживает все сопоставления.

class StateMachine<MH extends MessageHandler> {
  static class Delivery {
    final State state;
    final Class<? extends Message> msg;
  }
  HashMap<Delivery, MH> delegateTable;
  ...
}

class ServerStateMachine extends StateMachine<ServerMessageHandler> {
  ...
}

позволяет мне иметь пользовательские методы обработчика для этого конкретного конечного автомата. параметры метода handler.process могут быть перезаписаны. Однако обработчик не может быть параметризован типом сообщения.

Проблема: это связано с использованием instanceof проверки работоспособности для каждого обработчика сообщений (чтобы убедиться, что он получает ожидаемое сообщение).

Подход Б

давайте сделаем каждый обработчик сообщений параметризованным по типу сообщения

class MessageHandler<M extends Message> {
  void process(M msg) { .... }
}

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

Подход С

пусть объект состояния обрабатывает все сообщения.

class State<M extends Message> { ... }
class ServerState<M extends ServerMessage> extends State<M> { ... }

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

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

Подход D

имеют процесс сообщения, основанный на состоянии.

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

Подход Е

забудьте о дженериках и обработке состояний/сообщений жесткого кода с помощью оператора switch.

Проблема: здравомыслие

Небезопасное решение:

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

public class State { }

public class Message { }

public class MessageHandler<T extends Message> { }

public class Delivery<T extends Message> {
  final State state;
  final Class<T> msgClass;
}

public class Container {

  HashMap<Delivery<? extends Message>, MessageHandler<? extends Message>> table;

  public <T extends Message> add(State state, Class<T> msgClass, MessageHandler<T> handler) {
    table.put(new Delivery<T>(state, msgClass), handler);
  }

  public <T extends Message> MessageHandler<T> get(State state, T msg) {
    // UNSAFE - i cannot cast this properly, but the hashmap should be good
    MessageHandler<T> handler = (MessageHandler<T>)table.get(new Delivery<T>(state, msg.getClass()));
    return handler;
  }

}

person aepurniet    schedule 19.08.2009    source источник
comment
Таким образом, каждое состояние использует свои обработчики сообщений, чтобы определить, что делать с данным сообщением и в какое дочернее состояние перейти?   -  person Jonathan    schedule 19.08.2009
comment
state просто имеет данные, машина (в идеале) будет просматривать текущее доставленное сообщение и выбирать обработчик на основе комбинации состояние/сообщение. обработчики вернут то, каким должно быть следующее состояние. проблема заключается в том, чтобы каждое состояние могло обрабатывать разные сообщения и иметь к нему чистый интерфейс, без написания переключателей/кастов здравомыслия   -  person aepurniet    schedule 19.08.2009
comment
+1 за хороший, подробный вопрос   -  person Tom Neyland    schedule 19.08.2009


Ответы (5)


Для подхода B не используйте «хорошую» хэш-карту. Вместо этого напишите обработчики сопоставления гетерогенного безопасного с типом контейнера с объектами класса:

interface Handler<T extends Message> {
...}


interface Message {...}

interface HandlerContainer {

    <T extends Message> void register(Class<T> clazz, Handler<T> handler);

    <T extends Message> Handler<T> getHandler(T t);

}


class HandlerContainerImpl implements HandlerContainer {

    private final Map<Class<?>,Handler<?>> handlers = new HashMap<Class<?>,Handler<?>>();

    <T extends Message> void register(Class<T> clazz, Handler<T> handler) {
          if (clazz==null || handler==null) {
             throw new IllegalArgumentException();
          }
          handlers.put(clazz,handler);
    }

    //Type safety is assured by the register message and generic bounds
    @SuppressWarnings("unchecked")
    <T extends Message> Handler<T> getHandler(T t) {
            return  (Handler<T>)handlers.get(t.getClass());

    }

}
person user44242    schedule 20.08.2009
comment
это то, что я использовал, похоже, этот шаблон - способ решить большинство проблем стирания типов. Мне также очень нравится аннотационный ответ Джастина, оба, похоже, правильно решают проблему. этот не использует отражение, что делает его немного чище, хотя для регистрации каждого обработчика требуется больше печатать. я все равно использовал отражение для регистрации обработчиков. - person aepurniet; 20.08.2009

E с перечислениями, представляющими состояния и сообщения, вероятно, самый простой. Но он не очень расширяемый.

C использование шаблона посетителя в классах состояния для отправки по типу сообщения выглядит так, как будто это лучший из ваших вариантов. Учитывая выбор между instanceof и посетителем, я думаю, что посетитель немного чище (хотя все еще неудобен). Проблемы стирания типа действительно представляют собой заметную трудность, и обработка в сообщении кажется несколько отсталой. Типичная нотация конечного автомата имеет состояния в качестве центра управления. Кроме того, вы можете сделать так, чтобы абстрактный класс Visitor для типов сообщений выдавал ошибку во всех состояниях, что позволяет состояниям бесплатно получать отказ от недопустимых сообщений.

C+Visitor был бы вполне аналогичен подходу, который я часто использую при реализации конечных автоматов на C или языках с первоклассными функциями — «состояние» представлено указателем на сообщения функции, обрабатывающие сообщения в текущем состоянии, с возвратом этой функции. указатель на следующую функцию состояния (возможно, саму себя). Цикл управления конечным автоматом просто извлекает следующее сообщение, передает его функции текущего состояния и обновляет понятие «текущее» при возврате.

person Michael Ekstrand    schedule 19.08.2009

Подход E. Забудьте о дженериках и используйте интерфейсы.

class Message { ... }
class State { ... }

class Machine {
  static State handle(State current, Message msg) {
    ...
  }
}

class CustomMessage extends Message { ... }
class CustomState extends State { ... }

class CustomMachine {
  static CustomState handle(CustomState current, CustomMessage msg) {
    // custom cases
    ...

    // default: generic case
    return Machine.handle(current, msg);
  }
}
person Zed    schedule 19.08.2009
comment
Не вижу необходимости/выгоды от дженериков. Я бы оставил код читабельным и понятным, а не красивым. - person Andreas Dolk; 19.08.2009
comment
проблема в том, что каждый экземпляр конечного автомата имеет свой собственный тип обработчиков (обработчики имеют разные сигнатуры методов). это можно сделать, создав подкласс конечного автомата. тем не менее, я хотел бы написать, что каждый обработчик также должен принимать определенный тип сообщения, а не только класс сообщения, а затем кастинг/экземпляр - person aepurniet; 19.08.2009
comment
Я не уверен, что полностью понимаю проблему ... но я добавил схему того, о чем я думаю. Он, конечно, пропускает обработчики, но их, вероятно, можно добавить аналогичным образом. - person Zed; 19.08.2009
comment
я просто пытаюсь избежать написания шаблонного кода для переключения между настраиваемыми случаями (потенциально до одного для каждого типа сообщения) (как указано в вашем методе CustomState.handle), путем написания определенных обработчиков, которые принимают фактический тип объекта Message, а не объект Message и прохождение instanceof, бросая ад. - person aepurniet; 19.08.2009
comment
Извините, это была ошибка в коде. Конечно, он должен принимать CustomMessage. В любом случае, я думаю, что есть два пути: либо у вас должен быть шаблонный код, либо у вас должен быть общий интерфейс, которого придерживаются все ваши классы. Может быть, кто-то докажет, что я ошибаюсь. - person Zed; 19.08.2009

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

public class MyState {
 @OnEntry
 public void startStuff() {
  ...
 }

 @OnExit() 
 public void cleanup() {
  ..
 } 
}

Есть еще несколько разработанных реализаций, я думаю, что Scientific Toolbox был хорош, но я не могу найти правильную ссылку сейчас: http://mina.apache.org/introduction-to-mina-statemachine.html http://weblogs.java.net/blog/carcassi/archive/2007/02/finite_state_ma_1.html http://hubris.ucsd.edu/shared/manual.pdf

person Justin    schedule 19.08.2009
comment
мне действительно нравится это, чем больше я думаю об этом, каждая аннотация может иметь класс сообщения в качестве параметра, и с некоторым отражением может быть обработана должным образом. если объявленный метод, соответствующий классу сообщения, не найден, сообщение может быть передано в родительское состояние. - person aepurniet; 20.08.2009

Подход F:

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

interface StateMachineState<R extends StateMachineState,T> {
    /* returns next state */
    R execute(T otherState); 
}

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

class OtherState {
    public double x1;
    public int i;
}

enum MyState extends StateMachineState<MyState,OtherState>
{
    FOO {
       MyState execute(OtherState otherState) { 
           otherState.x1 += 3.0;
           otherState.i++;
           return BAR;
       }
    },
    BAR {
       MyState execute(OtherState otherState) { 
           otherState.x1 -= 1.0;
           otherState.i--;
           return (i % 3 == 0) ? FOO : BAR;
       }
    },         
}

Затем вы можете сделать что-то вроде:

MyState state = MyState.FOO;
OtherState otherState = new OtherState();
otherState.i = 77;
otherState.x1 = 3.14159;
while (true)
{
    state = state.execute(otherState);
    /* do something else here */        
}

(предостережение: код не проверяется дважды на наличие синтаксических ошибок)

person Jason S    schedule 19.08.2009