The deepest dive yet!
Oh my — the SOLID! Over the course of 2019 I have led 20 interviews with iOS developers, and I never hesitated to fire the question of “Alright, let’s talk about the SOLID, tell us, what is it about?”. And over the past 2 years I have myself been interviewed 15 times and the very same topic has unsurprisingly resurfaced consistently.
The comedy of this seemingly bureaucratic procedure is that everyone, who is prepared to rapidly spew details on what’s SOLID about, has a 50% chance of knowing some aspect or a compelling example reinforcing any of the rules that the interviewer has never even heard about! The multitude of interpretations of the rules makes me even wonder if the whole affair is for me to evaluate them or to learn myself — and it’s fantastic, these interviews are really something!
But the tragedy of the SOLID is that I’ve just hired 2 guys who have both perfectly carefully explained to me what’s the deal with it during the interview, but once the pull-requests have started pouring in, I constantly resort to inquire:
- why does this state have to be mutable?
- why is class doing all of these many things at the same?
*BBC Top Gear music suddenly starts playing*
Tonight, on Top Clean!
- Richard improves the architecture
- James implements new functionality
- And I show that there are 2 ways to violate the “Single Responsibility” principle
Who is this article for
Everyone. Literally, show it to you friends, say you’ve got it from the gift shop. Also, if you’ve never heard of Clean architecture or want to see how it looks like at the level of the relation between a “view” and “business-logic unit”, this will be an especially sweet article.
The humble beginning
As with many softwares we work on, we are going to start simple. Here are our requirements:
- There is a table of transaction cells
- Each cell should have a sender name, sender icon and the transaction amount
So we implement exactly that:
The code for the layout of the cell can be generally described like this:
Not a lot is happening inside the cell, and by the literal executable code, the cell does the following:
- It configures it’s own visuals by setting the shadow, corner radius, etc.
- It adds the subviews to the tree and configures the layout of widgets
- It binds the data from the viewmodel
This is a great opportunity for us to ask a question: does this class conform to the “Single Responsibility” principle from SOLID? One of the more convenient ways of answering it is to further inquire: “What exactly is the responsibility of this class?”. What does the class do?
The executable code, which, as we figured out earlier, consists of 3 activities. Two of them:
- the arrangement of subviews
- configuration of styling
, — can all be recognized as practically the same thing since they both contribute to the appearance of the cell. This is a good spot to formulate the responsibility of the cell: the cell is responsible for uniting and laying out these widgets into a container that can be reused, carried around and most importantly, considered an atomic, inseparable thing. Without the presence of the cell and without it taking this responsibility, the table that contains the cells would have to be aware of the existence of the
amountLabel. But instead, the cell provides a unifying abstraction and simple interface for the table so that it can mind not these details.
With the lack of the cell, if there was no cell, we can observe, that the responsibility of the table grows and with this growth, we might be able to find out that our table has suddenly acquired more of responsibility than we could consider to be “a Single Responsibility”. With the presence of the cell however, the responsibilities are split across 2 separate objects and thus they are both in their own ways closer to having “a Single Responsibility”. And why is that? Because we know that one is not doing the work that the other is doing and vice versa. We might not know what exactly are they doing, but we can be assured that they are not doing the same thing. If there are 5 responsibilities on the screen, the bigger the quantity of objects, the more the probability of each object having “a Single Responsibility”. Conversely, given a single object, it is highly unlikely that with 5 responsibilities to do it will end up having “a Single Responsibility”.
And for that reason, to reiterate:
The Single Responsibility of the cell is to unify 3 widgets into a single easy to use abstraction.
NB: the third activity found in the cell’s code is the data binding — the data is being extracted from the viewmodel and converted into “visual effects”. We do not consider this to be a separate responsibility of the cell for several reasons:
- it is not the self-purpose of the class’s existence, but merely a mean to an end. The data is being injected into the cell through the mechanism of a viewmodel, but this is necessitated purely by the business rules, and it’s not something that class established as the first and foremost. The data could easily be constant values with no need for a viewmodel whatsoever just due to the business requirements of the system. However, the cell’s primary focus would still remain with the layout, abstraction and unification.
- it takes up 5% of the total code base. The code of constraints creation is just more innately complex and long than data binding.
And thus, data binding doesn’t count.
A Wrench in the Works
With the next iteration of our software a new requirement is introduced:
- there are now multiple types of transactions
- a transaction might have a status of
- in the transaction table, a cell of a
pendingtransaction must have a new widget
- this widget is an Accept button that does something
Naturally, with how minuscule are the changes, it is only right that we employ the existing layout to add the support for the new UX. So here is the result:
In order to implement the support for the button that appears under certain conditions, we might want to modify the constraints inside the cell. One of the valid solutions if we don’t might activation and deactivation layout passes is to:
- Icon bottom to cell bottom, deactivate
- Button top to amount label bottom, activate
isHidden = false
This is the most straightforward, the least efficient way, and a perfect match for rapid development. Alternatively, there are 3 more techniques which eliminate constraint reactivation and involve priority tuning, collapsible widgets and layout guides, but this article is about architecture, so we’re not going to dig deeper than this.
In addition, to having the support for this view mutation, we need a determining factor which will let us to make the decision, whether if a given cell should contain a button. The answer to this is simple — since the need for the button appearance is derived from the data deeper into the app’s business logic, it’s safe to say that the view model of the cell should have a getter that implements the decision. The view will be calling this getter and applying mutations accordingly.
Here comes the new getter on the viewmodel:
And there goes its usage:
Then we’ll add the event propagation… and we’re square. And that’s it! We’ve done it! The capacity is extended, the functionality has been added! We, as the programmers, have detected the smallest possible incision we can perform on the system, without wrecking any of the existing functionality.
And believe me, in a healthy business, this is exactly the way that things are meant to be done — I see a business goal, I apply the smallest possible effort in achieving it as efficiently as possible! From that perspective, we are WINNAR.
This code will indeed go to production, however, after the awe has passed and we’re back to our senses, we can take a deeper look at just what exactly have we done here.
And this is a great-o’clock to analyze the resulting code and to determine — how does responsibility distribution appear now?
The Concealed Complexity
We apply the staple dialectic: what are the responsibilities of this widget? And the correct answer to formulate is:
- to layout Icon, Name and Amount widgets into a single cell
- AND to additionally layout the Accept button based on a decision
We can highlight three important problems with this situation.
Problem 1: “Special case”
The presence of this decision is something that we cannot just comfortably embed into the first statement. The main reason for it is the lack of uniformity. While the first statement is as simple as an array of widgets with which we work in equivalent ways, the same thing can not be said about the Accept button. All of the widgets from the first line require the same approach, a “uniform” fashion management. This can not be said about the Accept button because it requires 2 separate code paths and it represents a mutable state.
Problem 2: Scarce scaling capacity
Furthermore, with the increase of mutable possibilities, this setup is prone to being incompatible with scaling. It will be very tempting to add more mutability to the cell once additional rules, widgets and decision making get introduced. And this will lead to an exponential growth in complexity of the state, not to mention that presence or absence of widgets over 5 different scenarios will easily become a hassle to lay out.
Problem 3: Under-engineering
Also, we are focusing the breaking point in the same spot. For the surrounding architecture it’s very convenient that with 5 mutable variables, the cell is the one that will be taking a hit. In this set up, the cell is handling all responses to the mutation, and we are implicitly congregating this decision into an isolated single class, which makes the outside interface appear to be “single line, hassle free, сел-поехал”. This creates a dangerous illusion of the system being “not THAT complex”. This complexity is being purposefully discarded and concealed into a single unit to deal with it and this is exactly the problem with modern software. The objects that participate in the architecture of the entire screen are not establishing team work with each other and instead, they have found themselves a scapegoat. This scapegoat is doing everything for them and this is how you get the God object anti-pattern.
To illustrate, the table is now seeing the cells as the simplest thing ever:
While in reality,
TransactionCellViewModel can look like this:
These 3 problems are a proof: the “Single Responsibility” principle in SOLID is violated:
- the cell has more than a “single” responsibility
But what is the most interesting part is that we can also say:
the table has LESS than a “single” responsibility
Interesting concept? Trust me! I’m telling you, we can move SOME of the responsibility to the table, and they both will end up having exactly 2 separate and “single” responsibilities. Here’s how we do it!
Class Count? Barely Any
Mutable state. In the real world, in the world of business it’s usually faster to program, it allows for optimization, it sometimes helps us. But in the world of textbook programming, we avoid it everywhere we can because only a tree of pure functions has the capacity to infinitely scale. We cater to the open-closed principle and utilize composition over inheritance for simple reason that reassembly of reusable components allows us to build machines of infinite complexity over infinite amount of time, while with pure modification of an existing rigid structure we can achieve only so much. Mutable state is a no-no.
We have 1 cell class with 5 of states. But instead, we can:
- seek mutual exclusivity in the state combinations
- have separate immutable cells for each combination
- move the decision making outside of the cell
I have been deliberately withholding the implementation of the
TransactionTableViewModel and it’s time for you to see it.
This is the minimal possible implementation with proper DI, architecture and event handling. We, at the pro-devs water cooler back at the factory, call it the “Clean architecture in a nutshell”.
What we can see is just that it’s as lazy as the table. We can even say that they inherently do the very same thing — sure, the viewmodel:
- converts models into viewmodels
- handles events
But besides that it really boils down to just taking an array of stuff and exposing this same array of stuff without doing much to it. This responsibility is rather “single”, wink-wink, but after the refactoring we are going to perform, we will learn that it’s quite less than the “single” it could be!
A cell with a button and without one = 2 different cells. Right?
- The properties of the viewmodels of these cells differ. There is no necessity for the object of the Accept button to exist in the view. Additionally, if we look at some other potential widget, a viewmodel might supply a piece of text for the case when it’s needed, but when it’s not needed, all other implementations will have to keep in mind that state and make decisions on which value to supply. It might be simple, like leaving a
nil, but it quickly grows complex once modifications strike.
- The events differ too. In case if the Accept button is hidden, the event binding that it involves still proceeds to exist as clutter and excess. There might be 1 case when the event is used and with the rest the event is allocated but it’s permanently idle. During the modification you can usually ignore this, but in cases where someone will expect it to emit, it might not because it depends on the configuration of the Accept button state and the fact that the view shows everything correctly, which will also take effort to ensure. Also, event propagation primitives use additional space even when unused, but it’s insignificant and, thus, a shit-tier argument, but nevertheless an imperfection.
What we do is we start to acknowledge the fact that deep inside those are two separate cells. But not only do we do this at our “view” level! (there goes that MVC-like lingo). We start by incorporating this into our business-logic, hence modifying the table viewmodel!
Aaaaah, the enumerator with associated values! Here, at the pro-devs water cooler, if your language doesn’t support union types or enums with associated values, it can not be legally called statically typed. This is the most important advancement in programming technologies since lambda expressions. And we use its godly powers here to introduce the distinction.
No need for code, regular
TransactionInfoCellViewModelnever heard about Accept button or its events. They just don’t exist for it.
The table viewmodel follows suit:
Things to note:
- Both viewmodels still accept the
transactionModelin the constructor, but we fully expect
TransactionInfoCellViewModelto never take interest in the
TransactionInfoCellViewModelwill not have any properties or business logic related to Accept button
- The Accept button event is handled in one case, but not another since there is just nothing to handle, it doesn’t exist
TransactionTableViewModelhas a Single Responsibility
The Single Responsibility of the
TransactionTableViewModel is now:
- make decision on which model to represent with which view model
Everything else this class does now becomes just a fluff, an expense of communicating with other objects in the architecture. Before, these interactions took center stage, but otherwise, the viewmodel’s existence was essentially meaningless.
Let’s take a look at the table itself:
Things to note here with the table implementation:
- Literally does the same things as before
- Doesn’t try to look like a convenient one liner
- Fully reflects the plurality and diversity of the data
- Still has a Single Responsibility, but this time it’s deeper, thicker, more aware
Without it, an eager programmer could create a generic class
GenericTable<Cell> that would have the same one liner as the original implementation. He would then:
typealias TransactionTable = GenericTable<TransactionCell>
, — and call it a day. But in return, it shoots the customizability in the head and can be considered just a fancy equivalent of class inheritance. As soon as any specifics are introduced — like, completely out of thin air, deletion on cell swipe — you will be stripping this contraption down as fast as you’ve assembled it.
This is what I call a primal programmer instinct! If it involves repeating lines of code, I MUST BUILD ABSTRACTION. I MUST REUSE, tells the programmer’s monkey brain. But the reality, is that this is also an implicit attempt to conceal the complexity, to represent entire clusters of functionality as trivial one-liners. And in the long term, it always introduces cost.
Case in point, the code of the cells will involve some repetition. It’s important at this stage, for the cells, to engage in a 5 step combo:
- prepare reusable widget for icon
- same for name
- same for amount
- assemble the
- prepare the Accept button
- assemble the
Alternatively, for Sega Genesis gamepad users, a simpler, 3 step combo:
- prepare reusable widget that contains icon, name and amount in one box
- assemble the
- assemble the
This option will come in handy if you are somehow able to guarantee that you won’t need to introduce mutations inside the big widget, but otherwise, keep in mind, you are still sacrificing the flexibility.
This will bring an influx of some repetition. Repeating and thinking twice over the same problem is slow and tedious, so I would like to encourage you: copy-paste! Seriously!
I’m not blind to the notion that each and every single programmer, including me, at the beginning of career was hearing about the importance of abstractions, generalization and reuse. And every single programmer had this moment of quintessential understanding, this “I finally get it!” moment when they have tracked something that can be abstracted, generalized and reused in the code and even successfully done it. However, generalizations take away flexibility.
Abstractions, generalization and reuse only works when the system is stagnant and never gets modified. However, one of the biggest sources of architecture degradation is when the programmer is scared of dismantling an abstraction, generalization or reusable unit. This fear drives him to dance around it and preserve an abstraction that might be already totally invalid for the given system, yet its simplicity and “one-liner” convenience and forced uniformity are looking so appealing, he thinks that it’s not smart to sacrifice it. In reality, it is.
Copy-pasting is not a bad thing!
When we know that this is the last possible commit, this is exactly the time to create the best abstractions that we can because we know that nothing will need change. Well, except this is the last commit, so why bother, who are we trying to impress, might as well just make do and be over it. Ironic, isn’t it!
So get out there and do it. Copy-paste that layout because the pesky manager will find a way to make whatever abstraction you have established invalid. Though, be mindful about the pure reusable views — the icon, the name and the amount.
Also, sidestepping, did you know that one of the core tenants of “Clean architecture” is to strive to make as little decisions as possible? From this perspective, an abstraction over the views that reappear in both cells is a clear example of the decision. And copy-pasting is a decision that you never took because it is not constraining the architecture in any way. This only reinforces the idea — copy-pasting is a good thing.
What to take away from this article:
- Stop being stingy on the amount of classes your MVC stack has. Many classes reflect the complexity of the requirements without over-engineering while few classes conceal and trivialize the complexity, deceiving the developer
- Stop cramming decision making into a single unit inside the stack and instead carefully spread the complexity over the entire stack. In the example provided by the article, I demonstrated how at first one object was a scapegoat and under a constant hit, but then team work of all of the MVC stack members led to an architecture that spreads the responsibility fairly
- Make sure stuff exists only when it’s needed. Dangling state = confusion, loose ends, potentially undesired behavior and general lack of focus in the data flow. Also, it appears to be so simple, yet requires so much effort to maintain
- Reuse small views, compose big ones. Like an IKEA table, big atomic parts, quick assembly, quick disassembly and rearrangement. Don’t be stingy on the views class count either. I have a view at work that is assembled from 8 giga-widgets. The architecture is perfect, but if the
UIViewControllerwas managing every little
UIView‘s inside these giga-widgets, in other words, дрочил копейки, I would shoot myself, and you would’ve had to too
- Copy-paste because it’s jolly fast, rarely harmful and never unfixable!
And I would like to end the article on an acknowledgement:
In real life we are constantly violating these rules because sometimes it’s cheaper and faster. However my goal here is to show you the meta game and what are the tradeoffs of these decisions.
Thank you for reading!
Dan Abramov, The WET Codebase
A tremendous read and a watch, goes into ins and outs of the genius of copy-pasting + voice of the man himself included!
Robert C. Martin, Architecture: The Lost Years
The classic Cliff’s Notes version of “The Principles of Clean Architecture” by the Uncle Bob himself, fastest possible way of getting ahead in your knowledge of architecture.
About Me 👋 👋 👋
Hello, I’m Isaac Weisberg!
I really wish my Uni taught me how to write Mutter plugins for all the money my parents paid, but oh well — Otherwise I’ve been doing commercial iOS programming for more than 4 years, I’m completely self taught and I reside in Saint-Petersburg, Russia.
If you enjoyed the story and/or enjoy system engineering as much as I do, be sure to:
- Clap 50 times 😳 😳 😳
- Check out the “Further Reading” section 😉 😉 😉
- Copy-Paste that B as hard as you can 😼 😼 😼
I also have a stock trading mobile game on the App Store
You should check it out, might as well, it’s completely 100% free! 📈 📈 📈
Not even a single in-app purchase, wow! 🔥 🔥 🔥
And it’s written with Clean architecture, wow, this is epic! 🔥 🔥 🔥