Quantcast
Channel: Gio Carlo Cielo » event-driven
Viewing all articles
Browse latest Browse all 4

MinDispatch: Event-Driven Framework In Java Part 2

$
0
0

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.

Event broadcasting

Event broadcasting

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

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 and ChatState.

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.

Event multiplexing

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:

  1. Enabling concurrency and distributed computing
  2. Enabling dynamic user and input and developing a chat AI

Decide. Comment your preference below.


Viewing all articles
Browse latest Browse all 4

Latest Images

Trending Articles





Latest Images