From the last post in this series, we developed a fixed, event-driven chat simulation. In this post, we will extend this example by refactoring. The objective of this tutorial is to teach effective design patterns in an event-driven model.
First we will begin by designing the structure and behavior of the user and chat to describe our application. Second, we will bind the aforementioned chat state to the event handlers to fix constant parameters. Finally, we will use an event queue for separation of concerns.
Continuation
We will continue with our previous example which you can find on GitHub. It will be easier to follow along with the source code.
Beware that I will no longer provide a full example of the code. It is expected that you are aware of where the highlighted code modifications occur because the code file has grown large.
Structural Design
We begin by specifying the structural requirements of our chat. That is, we must specify the fields associated with our two primary data structures: User
and ChatState
.
User State Structure
We begin with the atomic data structure, the User
. A user in a chat has a name.
private static class User {
public String name;
public User(String name) {
this.name = name;
}
}
Simple enough.
Chat State Structure
Next,the ChatState
must model the configuration of a chat room: it should maintain the list of users currently in the chat.
private static class ChatState {
private ArrayList<User> users;
public ChatState() {}
}
Now to move on to the behaviors of our design.
Behavioral Design
The structure of our design has been outlined. Now we must enable interactions between these structures relative the possible events that may occur. Recall the events of a chat application:
- User Arrival
- Occurs when a user arrives to a room.
- User Departure
- Occurs when a user departs from a room.
- User Message
- Occurs when a user sends a message to a room.
Both the Chat
and User
must respond to these events accordingly. The following two sections will describe their behavioral implementation respectively.
Chat State Behaviors
Beginning with the ChatState
this time, we must handle each of the aforementioned events. Specifically, we must support the following operations:
- the addition of users to the chat,
- the removal of users from the chat,
- and broadcasting messages to all users currently in the chat.
Broadcasting must be handled in a special manner. For broadcasting, we must specify our recipients: all users should receive a copy of an event dispatched. Since we have outlined the design, consider the following interface which expresses our intent,
private static class ChatState {
private ArrayList<User> users;
public ChatState() {}
public void broadcast(Event evt) {}
// Mutators
public void addUser(User user) {}
public void removeUser(User user) {}
}
The implementation is relatively straightforward: the users
list maintains the current users in the chat and events can be broadcasted to subsequent users.
private static class ChatState {
private ArrayList users;
public ChatState() {
this.users = new ArrayList();
}
public void broadcast(Event evt) {
for (User recipient : users)
recipient.dispatch(evt);
}
// Mutators
public void addUser(User user) {
users.add(user);
}
public void removeUser(User user) {
users.remove(user);
}
}
The ChatState
is responsible for broadcasting events to each of the registered users. So, the ChatState
must broadcast the message to individual users so that they may handle the messages individually. See the diagram for more information.
When we construct the event queue, the ChatState
will act as an event forwarding mechanism for UserMessage
events.
Beware that there is a dependency the above code. The broadcast
method dispatches events to users by calling the User.dispatch
method which doesn’t exist yet. So, let us continue onto the User
behaviors.
User State Behaviors
We will outline the behavioral implementation of our User
class now. Since the user is capable of receiving events, we should demultiplex the incoming events and handle them appropriately. Specifically, we want to know if a user received a message. Consider the implementation then:
private static class User {
public String name;
public User(String name) {
this.name = name;
}
// Event demultiplexing
public void dispatch(Event evt) {
if (evt.getClass() == UserMessage.class) {
UserMessage message = (UserMessage) evt;
processMessage(message.user, message.message);
}
}
// Event processing
public void processMessage(User user, String userMessage) {
// Ignore messages by me
if (user.equals(this))
return;
System.out.println(
name + " received message from " +
user.name
);
}
}
Take a look at the broadcast
method. The type of the event argument is compared against UserMessage.class
. This if-ladder is an example of event demultiplexing.
Event demultiplexing occurs when a stream of events split its channels thereby processing the events individually rather than as a stream. This may warrant a need for an event dispatcher within a
User
.
When demultiplexing events, we route events to their respective handlers. Specifically, we route all of the UserMessage
events to the processMessage
handler and ignore the rest (arrival and departure are ignored). Once the events have been handled after demultiplexing, the behaviors of the data structure are complete.
Binding Chat State to Event Handlers
Unfortunately, now that a ChatState
exists, we must pass the object, as a parameter, to the each of the event handlers so that they may change the state of the object. Consider the event handler setup for UserArrival
state.registerChannel(UserArrival.class, new ChatHandler() {
@Override
public void dispatch(Event evt) {
UserArrival arrival = (UserArrival) evt;
arrival.state.addUser(arrival.user);
System.out.println(
arrival.user.name + " has entered the room."
);
}
});
Passing state along with each event may cause significant code duplication as well as unnecessary runtime overhead. With the current design of event handlers, each of the previously designed handlers to the user events, UserArrival
, UserDeparture
and UserMessage
must store a reference to the ChatState
that they operate on.
There exists a solution which removes the code duplication and the runtime overhead. We can push the responsibility of maintaining state to the event handlers by binding the ChatState
to a custom event handler. We know that this is feasible because ChatState
is the same throughout the execution of this simulation.
Application-specific Chat Handlers
We will implement our own event handlers, ChatHandler
, specifically for handling chat-specific events on a ChatState
. Simply, this custom handler should fix the parameter common to all of our handlers, ChatState
.
private static class ChatHandler extends Handler {
protected ChatState state;
public ChatHandler(ChatState state) {
this.state = state;
}
}
Afterwards, we may access the state of the chat for each subsequent ChatHandler
. So, the handler registration will be slightly different with an inherited handler.
public static void registerHandlers(EventDispatcher dispatcher, ChatState state) {
dispatcher.registerChannel(UserArrival.class, new ChatHandler(state) {
@Override
public void dispatch(Event evt) {
UserArrival arrival = (UserArrival) evt;
state.addUser(arrival.user);
System.out.println(
arrival.user.name + " has entered the room."
);
}
});
dispatcher.registerChannel(UserDeparture.class, new ChatHandler(state) {
@Override
public void dispatch(Event evt) {
UserDeparture departure = (UserDeparture) evt;
state.removeUser(departure.user);
System.out.println(
departure.user.name + " has left the room."
);
}
});
dispatcher.registerChannel(UserMessage.class, new ChatHandler(state) {
@Override
public void dispatch(Event evt) {
UserMessage message = (UserMessage) evt;
String userMessage =
String.format(
"%s: %s",
message.user.name,
message.message
);
System.out.println(userMessage);
// Broadcast messages
state.broadcast(message);
}
});
}
In our new implementation, we will use a
registerHandlers
helper function to initialize our event handlers with a specified event dispatcher andChatState
.
The state of the chat is updated in the above event handlers using the behavioral design that we have previously specified. Hence, we have effectively decoupled the state of the chat from the event dispatching.
Using an Event Queue
Next, we will utilize an event queue to separate concerns.
In computer science, separation of concerns (SoC) is the process of breaking a computer program into distinct features that overlap in functionality as little as possible. A concern is any piece of interest or focus in a program.
The event queue will enable us to separate the event dispatcher from the application-specific users and the chat state. That is, users should be unaware of the existence of a dispatcher especially when generating events themselves.
Additionally, when we introduce concurrency into this application (hint), the queue will also serve as a shared buffer between distributed users and the server which decouples application-independent concurrency mechanisms from our application-specific method functionality.
Conceptually, the event queue acts as a multiplexed channel which interleaves events from individual users since there is no particular order in which users may send messages. The event queue’s only concern is event multiplexing.
For now, we will simply use a simple Queue<Event>
in the Java standard library to express our intent. So, to instantiate this, we use a java.util.LinkedList
.
import java.util.LinkedList;
// ChatState declaration here
public static void main(String[] args) {
EventDispatcher dispatcher = new Dispatcher();
ChatState state = new ChatState();
Queue<Event> eventQueue = new LinkedList<Event>();
// Further simulation code such as event handler registration
}
Integration with Dispatcher
Since the Dispatcher
is responsible for dispatching events, we should dispatch all of the events in queue when flushing the buffer.
import java.util.LinkedList;
// ChatState declaration here
public static void main(String[] args) {
EventDispatcher dispatcher = new Dispatcher();
ChatState state = new ChatState();
Queue<Event> eventQueue = new LinkedList<Event>();
// Further simulation code such as event handler registration
// Possibly generate events beforehand
// Dispatch all queued events
while (!eventQueue.isEmpty()) {
Event evt = eventQueue.remove();
dispatcher.dispatch(evt);
}
}
Furthermore, notice that the event queue does not interact with the ChatState
. This is a highlight of separated concerns because event queues are application-independent.
Integration with Users
Before dispatching events with users, we must connect users to the event queue. Simply, we enable each individual user to reference the event queue in the implementation.
private static class User {
public Queue<Event> eventQueue;
public String name;
public User(Queue<Event> eventQueue, String name) {
this.eventQueue = eventQueue;
this.name = name;
}
// Behavioral methods
}
Once users have a reference to the event queue, they are able to generate events. Specifically, we want to enable users to send messages to the chat, thereby sending a message to all other users currently in the chat.
private static class User {
public Queue<Event> eventQueue;
public String name;
public User(Queue<Event> eventQueue, String name) {
this.eventQueue = eventQueue;
this.name = name;
}
// Event demultiplexing and handling methods
// Event generation
public void sendMessage(String message) {
eventQueue.add(new UserMessage(this, message));
}
}
Thus, users are now capable of sending messages without being aware of the event dispatcher and the chat state. Effectively, this is a highlight of modularity where modifications to a user’s capability in the system is independent of the modifications to the chat state and event dispatcher.
Testing the Simulation
Finally, the final source should be similar to my source code on GitHub. Now, we can test the simulation using the following main
and hardcoded events:
public static void main(String[] args) {
EventDispatcher dispatcher = new EventDispatcher();
ChatState state = new ChatState();
Queue<Event> eventQueue = new LinkedList<Event>();
registerHandlers(dispatcher, state);
// Initialize users
User foo = new User(eventQueue, "foo");
User bar = new User(eventQueue, "bar");
dispatcher.dispatch(new UserArrival(foo));
dispatcher.dispatch(new UserArrival(bar));
// Enqueue events from individual users
foo.sendMessage("hello, bar!");
bar.sendMessage("hello, foo!");
foo.sendMessage("goodbye, bar!");
// Dispatch all queued events
while (!eventQueue.isEmpty()) {
Event evt = eventQueue.remove();
dispatcher.dispatch(evt);
}
// Finish up simulation
dispatcher.dispatch(new UserDeparture(foo));
dispatcher.dispatch(new UserDeparture(bar));
}
The following output should be produced:
foo has entered the room.
bar has entered the room.
foo: hello, bar!
bar: hello, foo!
foo: goodbye, bar!
foo has left the room.
bar has left the room.
Thus, our chat simulation is complete.
Conclusion
Effective application design coupled with an event queue makes modification of the code far easier simply because we have a separation of concerns and modularity. That is, modifications to our application-specific handlers or data structures are independent of modifications to the application-independent event-driven framework, MinDispatch framework on GitHub..
It is easy to see that using the MinDispatch framework significantly simplifies the design for an event-driven application by handling the application-independent work.
Further Reading
I recommend reading Douglas Schmidt’s collection of papers on event handling and concurrency. Specifically, the Reactor Pattern has significantly influenced the design of my framework.
Another Continuation
There are two paths we can take from here:
- Enabling concurrency and distributed computing
- Enabling dynamic user and input and developing a chat AI
Decide. Comment your preference below.