The hardest part about software engineering is not writing code. It’s the ability to look at each problem with a fresh perspective, independent of solutions you have implemented in the past.
This is, by far, the most important lesson I’ve learnt while working with a bunch of mad chill, crazy smart people who love their craft.
When faced with a problem, it’s inevitable that we may get stuck in a rut and start seeing old problems and offering old solutions which may not ideal in the current context.
To understand why this happens, we’ll take a quick segue into neuropsychology.
Whenever a problem is presented to us, our left brains are automatically engaged and we start recalling the solutions that are familiar to us. Solutions which we may have implemented in the past or provided to us in external resources.
Now, this is not an issue if the said solution provides an acceptable end result in the current context. But, when none of the solutions seem to work, there’s this danger of us trying to reframe the puzzle to fit the available solutions instead.
To a man with a hammer, everything looks like a nail.
So, how do we get from this point to the cliche “a-ha” moment?
In theory, the way to get unstuck is by looking at the problem with a new perspective, by asking different questions and / or refining our language.
Too academic and theoretical? Thought so.
So, let’s follow a piece of advice from Elon Musk instead. That is, when we face a challenge, we should always reason from first principles rather than by analogy.
The normal way we conduct our lives is we reason by analogy. With analogy, we are doing this because it’s like something else that was done, or it is like what other people are doing. With first principles, you boil things down to the most fundamental truths, and then reason up from there.
— Elon Musk
In a previous article, I wrote about moving on from Angular and at the time of writing, I’ve started rebuilding our chat application with Flux. Here, I won’t be repeating it’s benefits as there is already a substantial amount of good resources for that.
Instead, I would like to share what are some of the concepts in Flux that may be foreign, or even revolting, for us who are familiar with the MV* programming paradigm.
It’s not a bad thing. Really.
Stores, not models
Stores are similar to models in a traditional MV* framework, but they are not models. In MV*, a model usually represents a single record of data and exposes a myriad of getter and setter methods in a public API interface.
On the other hand, a store manages the state of a single domain within our application. They could comprise of simple collections of ORM-style objects or they could be a combination of different objects that represents the state of that domain.
Also, stores are not supposed to provide a public setter interface. All updates to its state must be done through an intermediary known as Actions in a Flux application.
But why? Wouldn’t it be more convenient to provide CRUD methods like what we can find in a typical model layer?
Data transfers typically occur over HTTP and Web Sockets. Now, if we threw in the possibility for anyone to freely call methods that updates the application state, which in turn leads to the ability to cascade updates to the state, wouldn’t this lead to a rather unpleasant time in tracking down unexpected behaviours in our application?
By omitting setter methods, more code will need to be written, but with this extra layer of abstraction, we reduce the entry points for data updates and manipulation, which I believe, will lead to a more maintainable codebase in the long run.
In the previous section, I briefly touched upon Actions, which are an intermediary who dispatches messages via the Dispatcher to our stores in a Flux application.
Before moving on, I’ll like to point out that in my Flux applications, I’ve added an additional layer, called Services. Services are simply helper modules who communicate with an API server.
Services and views, are the only entities that call upon methods exposed by actions to dispatch messages to the stores.
For example, a service may retrieve a list of to-do items from the API and send an action indicating that a list of messages has been received. Or, a form submission in our view could use an action to perform an optimistic update of a to-do item in our stores.
Hold on, if actions simply send messages via the dispatcher, why can’t services and views use the dispatcher directly, like a global mediator, to publish those messages?
We could, but this introduces tight coupling between stores and views / services, which makes components less reusable.
There’s already a need to write more code with these layers of abstraction, so don’t torture yourself further. ☺
The dispatcher is perhaps the anchor point in any Flux application and it’s recommended that there is only a single dispatcher in your application.
One important feature about the dispatcher that doesn’t seem to be mentioned frequently is that at any point in time, only one message can be dispatched.
For example, assume that you just sent the message `hello:world`. Now if we try to send another message while the dispatch process is still in progress, an error will be thrown and the message will not be sent at all.
So, what constitutes the completion of a dispatch process?
A dispatch process usually comprises of the following steps:
- A message is dispatched
- Callbacks registered by stores listening for the message are fired
- Stores emit a `change` event
- View components listening for that event fire their respective callbacks
This means that we will not be able to perform cascading updates such as, retrieving a list of users followed by triggering an action to dispatch another message in the view’s callback that gets fired due to the change in the store’s state.
But if I introduce another dispatcher, this problem will be solved right?
Yes, but by doing this, we introduce another layer of complexity into our application stack. For instance, we may need to keep track of which stores are registered with with dispatcher. Same goes for actions.
Plus, this opens up the possibility of stores calling actions to influence the state of another application domain which they’re not responsible for.
Keep things simple, and let’s not break the proposed flow of data through our application.
There are no controllers in a Flux application. Instead, there are controller-views — views that listen to changes in the stores that represent the domain logic it governs and then pass down sets of immutable data to its children.
It may be tempting to allow multiple components under the same hierarchy to listen to changes from the stores, as it prevents the need to trace the flow of data up the tree.
The main drawback in doing this is, we’re introducing multiple points of entry for data updates in the same domain which could potentially make it harder to debug when there’s a need to track down unwanted effects in our application.
Lastly, more components listening to changes from the stores means a greater number of times the `render` method may be called, some of which may be unnecessary.
Flux is no silver bullet. It has its fair share of quirks and idealogies we’ll need to get used to.
Give Flux a try, hopefully you’ll have as much fun as I’m having. ☺