Case Study pt.1: Planning business logic using Redux
This is the first part of a series of articles about a case study for a web app implemented using Redux and Angular. In this article, there is no dependency on Angular, it applies wherever you find Redux. The project follows the architecture depicted on a previous article, but reading that article before this one is not necessary.
For easy referencing, here are the other articles in the series :
- Case Study pt.1: Planning business logic using Redux (this article)
- Case Study pt.2: Implementing Redux on Angular
- Case Study pt.3: Testing Redux Effects on Angular (to be published)
This project’s web app is an online test (or quiz) which, although functional, is simplified as it only serves as subject for this case study:
- Read the exam data from a server
- Allow single and multi-option answers, no open/text answers
- Time the examination using a server side timer
- Allow the user to finish the exam earlier
- Evaluate answers at the end and give result
- Mock the server on the client side, including exam data.
In this article I will present the planning of the business logic using a Redux approach. I normally do these plannings on a notes or text app with good support for bullets and outlining. I take an informal approach with no syntax worries, concentrating on thinking out the solution.
Redux has these entities that should already be familiar: State, Actions and Reducers (please check Redux website tutorials listing for a lot of pointers). State and Actions are data, Reducers are functions. But we cannot put all business logic on these functions since they need to be pure, that is, deterministic using only the provided State and Action data, and also they cannot have external side effects. So, they cannot call a server end point, for example. This is where all the middleware eco-system comes in. And I’m focusing on the Effects (or Epics) concept.
“Effects” as called on the @ngrx library (“epics” is the name used in the similar “redux-observable” library and “sagas” on redux-saga) are chain reactions to specific actions. One action can trigger an effect, resulting in more actions dispatched asynchronously. While at this, the “effect” code can access the outside world like server end points and produce actions accordingly.
Decomposing a solution using all these entities can be a little daunting at first, that’s why I’m using a test case on this article. One thing to always keep in mind is the separation between business logic and view layer (i.e. all the UI, showing data, reading inputs, navigation and interaction). With all the four entities (State, Actions, Reducers, Effects) one can build a headless web app, that works great within the tests and interacting with the server and all. The web app view layer will only project the state into the UI and emit actions upon the user’s input. Also, it can have local view logic (like hiding and showing, animating and such) but not change the application state. To accomplish this, the view layer must emit an action!
For each of the four entities mentioned, let’s see how we can accomplish the logic for the Test web app.
My notes snapshot for the application state:
The “user” top level would store the authenticated user info. It was not implemented on this project but I left it here to better illustrate the application state structure. The “exam” stores the exam info, questions and answers data and the “router” tracks in what application page the user is in.
At this point, the considered state is the necessary to the business logic context. Later on, more state information will be needed: view state for the view logic, like input fields in forms. Likewise, some actions might need to be added when working on the view logic. For now lets concentrate on the business logic.
Actions & Reducers
On this note I jot down all actions I think are needed and special reducer logic beyond setting state. Setting state is assumed when the action has coincident state property names. Once again, at this phase I want to focus on what is important, so simple state change is assumed.
This list concentrates both actions and reducer logic (as a sub-topic of actions) just to keep things DRY.
Note that there are three kinds of actions:
- Commands: EXAM_START, EXAM_END, QUESTIONS_ANSWER.
Means someone must make something about it, a reducer and/or an “effect” should take care of it. Normally these actions originate in the UI.
- Informative: EXAM_STATUS, EXAM_TIME, EXAM_SCORE, QUESTIONS_CURRENT.
Used for direct state change and typically ends up reflecting in the UI. These normally don’t trigger effects but originate in them.
- Data available: EXAM_DATA, QUESTIONS_DATA.
Normally emitted by effects when receiving data from the server. The data is in the action payload and must be incorporated into the state by reducers.
On a bigger project or with more people involved, I would mark the action kind on the notes, so its intended use is explicit.
Note the reducer logic on the note’s last line: “Resets existing answer in non multichoice questions”. This is what the reducer for QUESTIONS_ANSWER() must take care beyond setting state.
For the exam logic we need only two effects, one triggered by EXAM_START() and the other by EXAM_END() action. Note above, that they don’t make any change in state when going across reducers. These actions are intended to be processed by Effects, implementing some logic and accessing the server. The EXAM_START() is dispatched by the UI when pressing some “Start Exam” button. The EXAM_END can be dispatched by the UI or the timer.
The “>” symbol prefixing an action name, means that it is an action to be dispatched at that point.
Notice that server side services are invoked within the effects logic and the results are used to emit new actions.
There are two Actions pertaining the router that were not indicated in the Actions listing above. This is because they pertain to the framework’s application navigation (aka routing) system:
- NAVIGATION(location): Informs that the application is moving to the indicated location or router state.
- NAVIGATION_GO(location): Makes the application move to the indicated location or router state. After this is dispatched, the corresponding “NAVIGATION(location)” will eventually be dispatched.
When navigating the web app, the event of arriving at a page must fire some logic. On my notes of this logic, the “EXAM_START”, “EXAM_QUESTION” and “EXAM_RESULT” are informal page identifiers. Do not mistake them for actions, since they only appear as arguments to the navigation actions.
One might be tempted to put this navigation logic on the UI components that correspond to those pages, namely on the component initialization procedure. Components can produce actions, as they do when the user interacts, but placing this logic on the view has two problems: leaks business logic into view components and also breaks the awesome Redux time-travel capabilities provided by the DevTools Extension.
On this article, I’ve presented a way to fit your business logic into a Redux with side effects architecture, in a way that the business logic and data layer are completely separated from the view layer. This can be very important for bigger and/or specialized teams, allowing different roles to progress more independently.
In the next article we’ll see how this can be implemented in Angular and @ngrx. The full code is already in this repo.
PS: This article also appears on my blog.