Implement your own Redux in Java. Part 3: First approach on Middlewares

javaredux

In this series of blog posts I'm implementing my own Redux in Java to use with JavaFX.

However, the purpose is not to create a production-ready library (because there is already ReduxFX available) but instead to learn by re-implementing it.

In the previous part I've showed how to connect the basic redux store to a JavaFX view. In this part I will take a first look into the first extension mechanism that Redux provides: Middlewares.

Topics in this blog series (maybe I will adjust this in the process of writing):

Middlewares in Redux are an extension mechanism that let's you intercept and manipulate actions before they reach the reducer. While the idea of middlewares in Redux is not that complicated the API can be a little confusing at first. For this reason first I will start with a simplified API and then later on will advance it to match the original API.

Simply put, a middleware is a wrapper around the dispatch function. It can be expressed as a function that takes the original dispatch function as argument and returns a new enhanced dispatch function as result. To put this into code let's first take a closer look at the dispatch function itself. At the moment our Store has the method void dispatch(Object action). It takes the action as argument and returns nothing (by the way: The JavaScript Redux library returns the action object itself but I'm using 'void' here. We will see why in one of the next posts). We can also define a functional interface for this:

@FunctionalInterface
public interface DispatchFunction extends Consumer<Object> {
	@Override
	void accept(Object action);
}

With this definition we can define our first approach for a middleware interface:

public interface Middleware extends Function<DispatchFunction, DispatchFunction> {
    @Override
	DispatchFunction apply(DispatchFunction dispatch);
}

Now let's implement some middlewares. Of course the simplest possible middleware is one that just returns the argument without touching it (in functional programming we call such a function "identity"). The implementation could look like this:

public interface NoopMiddleware implements Middleware {
	@Override
	DispatchFunction apply(DispatchFunction dispatch) {
		return dispatch;
	}
}

or

public class NoopMiddleware implements Middleware {
    @Override
    public DispatchFunction apply(DispatchFunction dispatch) {
        return action -> {
            dispatch.accept(action);
        };
    }
}

Another quite useless middleware would be one that "eats up" actions without further dispatching them:

public class BlackHoleMiddleware implements Middleware {
	@Override
	public DispatchFunction apply(DispatchFunction dispatch) {
		return action -> {
			// nothing
		};
	}
}

Ok, these examples don't make much sense. Let's implement a more useful middleware: The LoggingMiddleware print's a log-message everytime an action is dispatched. This way developers can get a better understanding of what's happening in their apps.

public class LoggingMiddleware implements Middleware {
    @Override
    public DispatchFunction apply(DispatchFunction dispatch) {
        return action -> {
            System.out.println("dispatched action:" + action);
            
            dispatch.accept(action);
        };
    }
}

Even cooler would be to be see what the state was before and after processing the action. This way developers could see what effects an action had.

Because it's useful for many middleware to have access to the current state of the Store, the middleware API of redux also provides a "getState" function to the middleware. Let's adjust our Middleware interface:

public interface Middleware<S> extends BiFunction<DispatchFunction, Supplier<S>, DispatchFunction> {

    @Override
    DispatchFunction apply(DispatchFunction dispatch, Supplier<S> getState);
    
}

We see that now a middleware takes two arguments: The dispatch function and a Supplier<S>. java.util.function.Supplier is a standard functional interface with the method S get(). It's our "getState" method that returns the current State of our Store. For typing reasons we need to add a generic type-param S to our middleware interface.

Now we can improve our LoggingMiddleware like this:

public class LoggingMiddleware<S> implements Middleware<S> {

    @Override
    public DispatchFunction apply(DispatchFunction dispatch, Supplier<S> getState) {
        return action -> {
            S stateBefore = getState.get();
            
            dispatch.accept(action);
            
            S stateAfter = getState.get();
            
            System.out.println("dispatched action:" + action);
            System.out.println("state before:");
            System.out.println(stateBefore);
            System.out.println("state after:");
            System.out.println(stateAfter);
        };
    }
}

We request the state before we pass the action to the dispatch function. After the dispatch function is finished we again request the state and print everything on the screen.

Using other types of actions

Another common use-case for middlewares is to enable users to dispatch other types of actions and react to them in the middleware. This can be used to enable async behavior. In the next post in this series I will show this in more detail. But for now I will show a middleware that let's you dispatch a List of actions instead of just a single one. This might be useful if you like to compose action-creators so that multiple actions are dispatched by a single action creator.

This type of middlewares is possible because the type of actions that the dispatch-function takes is Object. Therefore almost everything can be passed as action. However, our Reducer still takes an Action as argument and our current implementation of dispatch in the Store casts the action object to Action. This would lead to a ClassCastException. We could change the signature of our Reducer interface and remove the cast but then the reducer would need to know which types of actions might be passed which would introduce a tighter coupling between the reducer and the installed middlewares. Instead we let our middleware make sure that only "real" actions may arrive at the reducer. So for middlewares it's a common pattern to check for the type of an action and to implement a special handling for a given type while ignoring actions of other types.

For our List-Middleware the code looks like this:

public class ListMiddleware<S> implements Middleware<S> {
	@Override 
	public DispatchFunction apply(DispatchFunction dispatch, Supplier<S> getState) {
		return action -> {
			if(action instanceof List) {
				List actionList = (List) action;
				
				actionList.forEach(dispatch);
			} else {
				dispatch.accept(action);
			}
		};
	}
}

As you can see I'm checking if the action is of type List. In this case I'm doing a cast and invoke the dispatch function for each action in the list. If the action is not a list I'm just passing it down to the normal dispatch function.

Before we take a look at how we add support for middlewares to our Store, lets see how the usage of our new ListMiddleware could look like. In our todo example app we have the TodoViewController which has a reference to the Store. When the user clicks the "add" button an AddItemAction is created and passed to the dispatch function of the Store. If (for whatever reason) we like to dispatch another action in the same button-handler we could simply invoke the dispatch method a second time. However, in redux it's a common practice to encapsulate the creation of actions to so-called "action-creators". The UI passes just the needed data as parameter to the action-creator and the actual creation of actions is hidden from the UI. This minimizes the coupling of the UI to the actual creation of actions. If later on we decide to change the implementation of the action-creation the UI can stay untouched.

An example action-creater could look like this:

public static Object addItemAction(String text) {
	return new AddItemAction(text);
}

// usage:
store.dispatch(addItemAction(input.getText()));

If we now come to the conclusion that we need a second action to be dispatched for the same user-interaction we can refactor our action-creator to this code:

public static Object addItemAction(String text) {
	List actions = Arrays.asList(
			new AddItemAction(text),
			new SomeOtherAction()
	);
	
	return actions;
}

We doesn't need to change the UI code for this refactoring. Our ListMiddleware will now detect that an instance of List was dispatched and react accordingly. In the next blog post I will come back to this code to show the limitations of our simplified Middleware API. Maybe you can already see what can go wrong with this approach?

Chaining middlewares

In this blog post we have already seen some implementations of middlewares and you can implement many more if you like. But can we use multiple middlewares in combination? The answer is: Yes.

A middleware is like an enhancer of the dispatch-function that takes a dispatch-function as parameter and returns an enhanced dispatch-function. This enables us to chain multiple middlewares. The first middleware in the chain get's the original dispatch-function from the store and returns an enhanced dispatch-function. This function is then passed to the next middleware in the chain and so on and so forth until the last middleware produces it's dispatch-function.

This final dispatch-function is then the one the Store has to use when a user dispatches a new action. The action can then "flow" through the middlewares until it reaches the original dispatch-function and is processed by the reducer.

In functional programming this is called "function composition". Maybe in a future blog post I will go into more detail on the theory on function composition and how it applies to middlewares. But for now you should just keep two things in mind:

  1. The order of middlewares in the chain is important. A different order may change the semantics of the action processing. For example if the LoggerMiddleware "sees" an action before the ListMiddleware, the logger will print a List. On the other hand if the ListMiddleware comes before the LoggerMiddleware, the logger won't ever notice that a List was dispatched.

  2. The invocation order is back-to-front. The "first" middleware that enhances the original dispatch-function will see an incoming action as the last. This is generally the case for function composition and can be a little confusing at first.

Implementing the simplified middleware API

Now let's start implementing the actual support for this simplified middleware API in our Store. Middlewares are getting the original dispatch-function as argument and are creating a new one that has to be used by the Store afterwards. To enable this we have to make the dispatch-function of the Store exchangeable. At the moment it's a normal Java method but now we will create a field for it:

Before:

public class Store<S> {
	// ...
	
	public void dispatch(Object action) {
		S oldState = this.currentState;
		
		S newState = reducer.reduce(oldState, (Action) action);
		
		if(oldState != newState && !oldState.equals(newState)) {
			this.currentState = newState;
			
			notifySubscribers();
		}
	}
}

After:

public class Store<S> {
	// ...
	
	private DispatchFunction dispatch = action -> {
		S oldState = this.currentState;
        
		S newState = reducer.reduce(oldState, (Action) action);

		if(oldState != newState && !oldState.equals(newState)) {
			this.currentState = newState;

			notifySubscribers();
		}
	};
	
	public void dispatch(Object action) {
		this.dispatch.accept(action);
	}
}

This enables us to switch the dispatch-function by re-assigning the "dispatch"-field.

The second step is to take the middlewares and invoke them one after the other to get the final dispatch-function. For this we extend the constructor of the Store to take a variable number of middlewares:

public class Store<S> {
	// ...
	
	public Store(S initialState, Reducer<S> rootReducer, Middleware<S> ...middlewares) {
        this.currentState = initialState;
        this.reducer = rootReducer;

        for (Middleware<S> middleware : middlewares) {
            this.dispatch = middleware.apply(dispatch, this::getState);
        }
    }
    
    // ...
}

And that's basically all we have to do. We can now adjust our example app to use the LoggingMiddleware and ListMiddleware:

Store<TodoState> store = new Store<>(
		initialState, 
		new TodoReducer(),
		// middlewares
		new LoggingMiddleware<>(),
		new ListMiddleware<>()
);

For simple middlewares like the LoggingMiddleware this simplified middleware API is sufficient. However, the original Middleware API of Redux.js is a little more complex and this is for a reason. In the next blog post of this series I will show the limitations of the current API and I will refactor it to match the original API. After that in another post I will show how you can enable async behavior with middlewares.


You can find the code for redux example in this github repository. This commit is pointing directly to the state of this blog post.