The secret is to separate business logic into self-contained units.
I like SOLID, but I’ve found that a lot of the advice on how to apply SOLID in practice is either focused on small scale examples or geared towards backend architectures. There is very little advice on how to deal with the kind of large stateful applications I deal with in my professional career – namely, video players.
I know of one player team that almost worships SOLID. During the interview, he quizzed the prospective engineers on SOLID principles. Every pull request was to flash your solid credit or be kicked out of the club.
In the beginning, almost every part of their codebase could be viewed as a gleaming nugget of SOLID gold, but that yearning for purity didn’t stop this team from eventually building up a huge steam pile. He created a controller manager model, as my previous article, The connective tissue of their architecture could not hold the weight of the elephant and collapsed.
I heard recently that this team is rebuilding their codebase, ‘learned’ from all their mistakes on this same basic architecture. Surely, they’ll get it right this time, right?
Creating cohesive decoupled code looks very different at large scale than at small scale, and as far as I can tell, no one has ever talked about how to scale up SOLID for an application such as a player.
That’s why I’m inspired to write these articles! The good news is: other communities have already moved on from their own version of controller manager clutter. We can start our journey by learning how they turned their paradigms away from straight object oriented design.
Take layered architecture. It is more or less the embodiment of object oriented design on the backend. Looks something like this:
The idea is to separate the concerns of the application into layers. There is a layer for data access, a layer for business logic, a layer for views, etc. Theoretically, each of these layers can be changed separately.
However, this separation breaks down under complexity as the flow of execution of any part of the business logic is spread across layers and within layers.
If you just wanted to isolate that feature, how easy would that be? Because the code is spread throughout the application, no matter how strict the principles you follow, it gets stuck.
as i have done described, If you can’t move your business logic like pieces on a chessboard, you can’t change their timing, which makes the state difficult to manage. When we lose control of our state, it becomes harder and harder to scale the application; That’s when we get into the grip of the complicated hockey stick.
This isn’t such an issue for small applications, applications that don’t have a lot of state, or applications that don’t change very much, but need to scale out and out the back end often, sometimes on a whim. up to degree.
So, how did the backend community deal with this issue? He broke his layered monolith into smaller and smaller pieces. He created microservices.
Microservices, when used correctly, are pieces of business logic floating in a sea of infrastructure.
Of course, that infrastructure can be dizzyingly complex. The issues of deployment, analytics, security, etc make microservices very challenging to pull off well. Unfortunately, the usefulness of microservices is usually explained in terms of how they interact with their infrastructure; Services can be deployed and scaled independently. This is a very important aspect of microservices, but it misses the point.
The greatest value of microservices lies in encapsulating business logic into discrete units that can change independently of the infrastructure. Applications that make better use of microservices scale because they allow each service to be tweaked as needed, having control over the production and consumption of state while avoiding the complexity hockey stick.
This reality is created by microservices that break this contract. I’ve seen microservices hit other microservices as if they were calling inline functions. This is surprisingly easy in Java and leads to “distributed monoliths”.
Such applications reintroduce the same old scaling problems. All that painstaking infrastructure work to enable microservices is wasted.
So, what can player developers learn from our backend brethren?
We should learn that we can and should move towards separating business logic into self-contained units. This gives us control over when and how we express business logic, which gives us room to modify the flow of execution, which allows us to re-gibber our state and scale our application.
There came a time in the history of the web when websites began to become proper web apps. We were moving beyond Blink tags and web forms and adding interactivity.
To deal with this complexity, developers turned to the Model View Controller pattern. It was a way of separating concerns in the user interface.
However, there is a slight flaw in the MVC pattern. It assumes that the user will handle the business logic.
The pattern was designed for all those unlucky data input jockeys who had to tab through screen after screen on bulky CRT monitors. I remember going into a doctor’s office in the nineties and waiting for the tired receptionist to click clack click click on her beefy keyboard as she carefully entered my name, address, date of birth, etc.
The job of the view was to show the contents of the model to the user. The user will then decide what to do. The controller will translate those decisions into changes to the model. When the user was done, they would hit ‘save’, and the model would be flushed to the server, the transaction would be committed.
MVC was not built to handle business logic, yet when it was repurposed for more interactive web applications, interactivity, a type of business logic, had to be put somewhere.
And it was placed, logically enough, in the controller… and in the model, and in the view.
It was not clear where the business logic should go. Some aspects of interactivity naturally go in the View, some in the Model and some in the Controller. In an effort to control the process, the MVC pattern turned to the MVP and MVVM patterns, but they all suffered from the same issues.
Which I hope you all are familiar with by now. Features are scattered across layers, leading to inflexibility of state management and scaling issues. It doesn’t matter how well you engineer your MVC-style code. Complexity will eventually knock.
How did this community find its way out of the MVC maze? He collapsed the model, view, and controller into two separate structures, each handling a different application scale.
First, there are libraries like React. Despite the hype around features like Virtual DOM, the real value of React is that it uses components to encapsulate business logic.
Each component is self-contained. It is completely model, view and controller. The code is not spread throughout the application if you want to see how a particular button works or the table formats itself (unless you have a poorly built React app, but hey, you can lead a horse to water …). Components can be independently tested and changed independently, allowing applications to scale in complexity.
Second, business logic or state often requires cross-component boundaries in large applications. Libraries like Redux encapsulate this functionality so components don’t need to deal with them directly.
When a component needs to send a message to start an external business logic process, it sends an action to the central store and forgets about it.
Many of us have become accustomed to using Redux, so we don’t think about how big a departure it is from typical object-oriented design.
as i have done describedCoupled message passing is the cardinal sin of the controller manager model. When bits of code call other bits of code and are expected to ensure that the returned values are passed correctly to the next bits of code, the flow of execution in the embers of all those calls is frozen. goes.
Actions separate message passing.
An action indicates that a specific piece of data is ready to start a specific business process. The component dispatching the action is responsible for making sure the data is correct, but after that, its responsibility is over. There is no return data to pass to the next object. The parts do not get stuck when holding the bag.
An action fires off a function called a reducer, just waiting for that action to be called. A reducer has one job: to modify the global state using the data supplied with the action. Once the state has changed, Redux passes this state to React, which automatically recreates itself with that new state.
Each reducer is, therefore, a piece of business logic that is decomposed and floats in a sea of infrastructure that takes care of all the message passing. Familiar? (shh. microservices)
Object-oriented design and the layered architectures that result from this encapsulate state, leaving business logic a secondary concern. It doesn’t matter how well you convolute your code, eventually you will have scaling issues.
Modern architectures have switched to encapsulating business logic. State becomes extern, business logic is passed in. This allows the application to scale.
How would we apply these techniques to an application like a video player? I’ll explore that in my next article.
Thank you for reading. Until next time, happy coding!