Anton Korzunov
Posted on October 31, 2021
One of the first false assumptions
one could face during a long journey of becoming a developer, is that said journey is just about development, about you just writing some code.
Like - start a new project by writing code, and finish it in the same way.
Only later one will be told about testing, or the need to solve real customer problems, and other "business as usual" stuff, not sure which one came first.
It's fine to start your journey in this way, everything needs a beginning, but this is not how it should continue.
This is not how it could succeed.
Our job is not about writing code, it's about writing the right code, writing it "Once and only Once", testing it, solving problems and finishing assigned tasks.
It's not about creating >new< things,
but more usually about changing the >old< ones.
Software engineering is programming over time. Engineering is about considering the long-term effects of your code. Both direct and indirect. Link
Read it this way - while moving forward think hard about what you are leaving behind and what you need to make the next step.
💡 Applicable to your life as well.
While the vast majority of information you can find out there is focused on how to "make" things, let's talk about future maintenance, about reducing different burdens - from the classical technical debt
to cognitive load
.
Let's talk about the multidimensional "Cake" approach, also known as Multitier architecture, also known as Onion Architecture, and how it applies to UI-based applications.
Where is the Problem?
The problem is not only "where", the problem is also "when".
Let's imagine that you are working on a very agile project, of course you are, and you just bootstrapped a new application that already has experienced two pivots and going to have another one.
The essence of Agile is about being reactive to the changes.
It is absolutely ok to start a random redesign, it's absolutely ok to abandon an almost complete feature and start redoing it in a little different way, it's ok to adapt for the new requirements and the only thing a developer should be focused on this point - be able to preserve as much as possible, about how NOT to start every time from the scratch. That happens to all of us but is not any efficient.
While the majority might understand the solution for a "Happy Live" as Pit of Success, where a well-designed system makes it easy to do the right things and annoying (but not impossible) to do the wrong things, it's still about making things(note "do the right thing"), not changing something existing, even something created yesterday (we "agile", right 🙄?).
I reckon the Solution for the change might have roots in the Chaos Engineering, where you have to accept that something will go wrong, a build a system resilient to it. While the essence of this philosophy is something you should always have in mind, there is another way to address the same problem.
Chaos? This is what happens during(before or instead of) the family Christmas dinner, meetup and especially during the Gala Concert - too many things are happening at once and everybody is just running around screaming. In all cases, the path to success is to have some protocols, procedures and preparation.
Path to Success is a proper foundation.
Standing on the shoulders of Giants - a general concept that prior knowledge, and not only knowledge, could and should be used today 👇
Using the understanding gained by major thinkers who have gone before in order to make intellectual progress
- every time you use
webpack
, or any other bundler, and do not create your own one - you stand on the shoulders - every time you use
React
, or any other UI abstraction - you stand on the shoulders - every time you use any library, not writing code from scratch - you stand on the shoulders
The majority of developers would use some preexisting (third party) solution to solve their problem, and would stand on the shoulders of other developers and "The Platform", but the same majority of developers are also missing the ability to stand on their own shoulders.
- 👉 every time you need to change something, there should be a giant you can rely on. You have to giant yourself.
A digital platform is a foundation of self-service APIs, tools, services, knowledge and support which are arranged as a compelling internal product. Autonomous delivery teams can make use of the platform to deliver product features at a higher pace, with reduced coordination. What is a 'Platform' anyway?
I've seen it
We will jump into some concrete examples shortly, but let's first create some concepts to act as a foundation, let's create our first small Giant, the one you should know very well.
- 🤖 Terminator 1 -> 🦾 Terminator 2 -> 🦿Terminator 3. They all are backing plot of each other. Without the very first you cannot have the second.
- 📖Book (Hobbit) -> 💍Film (Hobbit, well 3 films). While there are many differences between the book and the film, they share the same foundation
- 🤯Total Recall(1990) -> 🤯Total Recall(2012). Those films have nothing in common, except 😉 the same foundation.
Every remake, every sequel or prequel, every film based on a book or a book based on a film are examples of Standing on the Shoulders of Giants
Which other Giants exist?
Layers
Before you run away let us pick some examples you definitely will understand. Probably it will be more correct to say - that a lot of people for some strange reason expect you to understand it, and once upon a time during every second interview for a JavaScript position you might be asked about this thing, and it always was not very clear for me, like it's 100% not related... until today.
The Layers of OSI Model, also known as a internet layer cake
Hey! I said do not run away! Look how one layer of OSI stands on the shoulders of another.
There is no difference for you how the device you are reading this information from is connected to the internet - Ethernet, Wifi, 2G, 4G or 5G - it just works. The top-most(7th) layer is unbound from any network hardware.
Implementation details are abstracted into many layers and some of those layers are even interchangeable(WiFi!==Ethernet!==Cellular).
I hope you would like to experience the same smooth journey during UI development. Strangely, but often developers are trying to shorten processes, collapse layers and especially not separate concerns and trying to get something valuable from it. Again and again, with no Giants supporting them.
Well, might be using OSI as an example was a little too much, but
- would you consider
React
as alayer
? - would you consider
webpack
as alayer
? - would you consider
MaterialUI
as the nextlayer
? - and
NextJS
as one more extralayer
?
For the User, there is no difference if an Application has been built with Nuxt
, Next
or bare webpack
. For webpack
there is also no difference if it is used by the application directly or hidden inside Next.
Can you see all those giants, on the shoulder of which your application is standing?
If you ever wonder why your
node_modules
are so huge, better to say GIGANTIC -> that's it! 😅
Another good example is Redux
, as "Redux" by itself means nothing. It can be very different and you'll never know which recipe was used to bake it.
Redux
+Thunk
, or Redux
+Saga
provides a little more context for an expected taste of a given solution, however only RTK looks like a properly layered cake. Mmmm, tasty!
The Whole and the Parts
Speaking of Redux, there is a very common "mistake" in the understanding of the difference between "Redux" and "Context API". To be more concrete - the difference between useReducer
+ React.Context API
and Redux
as the latter is technically the same as "Reducer + Context".
Right after the React Context presentation many people, really many people, were wondering - 🤔 do they really need Redux or what?
Well, probably they didn't, but the more proper way to explain what is wrong with such a common and simple misconception is to refer to Weinberg’s Law of Decomposition, which states "the whole is greater than the sum of its parts".
Very easy to prove, just combine baking 🤯 soda and vinegar 💥.
By the fact, Redux is not only reducers, but also patterns, DevTools, and different middlewares for different use cases.
While Redux is ContextAPI + Reducer, it is BIGGER than a sum of its parts.
👉 the real work of an engineer is to see such bigger wholes looking at the pieces. Some things are visible only from a distance. Take a step back, do not focus too much on low-level details.
An interesting moment about said law is that it simultaneously states the opposite:
Weinberg’s Law of Decomposition is subtler. It says that if you
measure a system according to some gauge of functionality or
complexity, and then decompose it, and measure up what you
end up with, that the sum of the parts is greater than the whole.
Huh? This seems to contradict the Law of Composition.
The best way to read this is to accept that you are never going to consume something in full, as a whole, only the required pieces. And there will be always some stuff left unused.
Very easy to prove, just combine Cola and Whiskey 🤢
Foundation: the Essence and the Variables
The very first step towards our goal is the ability to... leave something behind. Separate flyes and cutlets, extract placeholders from templates, and split a single whole into the Essence and the Variables
.
The best and the most common example for this are DSL
s - Domain Specific Languages, including any Template languages, including React.
😉 is there any difference between
<div>{{userName}}</div>
and<User name={userName}>
?
A very important moment is that the Essence/Variables
Separation can be performed by:
- moving the
Essence
to the Layer below (sinking functionality) -
Variables
would be "kept"(emerge) automatically, as you will need to find a way to configure the underlying functionality.
This concept is almost equal to Software Product Line - methods, tools and techniques for creating a collection of similar software systems from a shared set of software assets using a common
means of production
.
This is also quite close to the Ports and Adapters
(hexagonal architecture), where the "actual functionality"(Platform capabilities) is hidden behind Adapters (Essence in this case), which are in turn hidden behind Ports(Variables in this case).
To better understand, let's create a few examples:
Button Group
At many sites you might see Buttons
positioned next to each other. Technically speaking they are nothing more than two Buttons
placed in one parent and separated by some Gap
. However, does it mean that this is what you should do?
const ActionButtons = () => (
<div style={{display:'grid', gridGap:'16px'}}>
<Button>Do</Button>
<Button>Something</Button>
</div>
)
How many different ways you know to create said gap, and how many different gaps
you can use - 2px, 4px, 20px?
Probably said gap
should be proportional to Button
size to create a "coupling" between two buttons and let you use a larger gap to create a "distinction".
This is why it's very important to create an abstraction - ButtonGroup
<ButtonGroup /* don't think how*/>
<Button>Do</Button>
<Button>Something</Button>
</ButtonGroup>
Or even give underlying logic more control over look-n-feel, and create an opportunity to collapse a few Buttons in one group into one Dropdown
on mobile devices.
{ ifMobile ? (
<Dropdown caption="Edit">
<Button>Edit</Button>
<Button>Delete</Button>
</Dropdown>
): (
<ButtonGroup>
<Button>Edit</Button>
<Button>Delete</Button>
</ButtonGroup>
// ⬇️⬇️⬇️⬇️
<ResponsiveButtonGroup
actions={[
{ label: 'Edit', action: ... },
{ label: 'Delete', action: ... },
/>
Move one giant shoulder up. There are so many reasons to have buttons grouped somehow, and all those use cases can be named to be used for a known reason!
Table
Table
is another example where second-abstractions can help you a lot.
Let's imagine you need to display a table. You have two options:
- render the table by yourself
- use some other library to do it
In the first case you might need to spend more time than needed to handle edge cases, implement virtualisation, sorting, you name it.
In the second case, you might find any particular library to not matching your expectations in some details with no ability to change the pre-backed solution.
Often in such case, developers are picking the first case as the only possible, while they always need the second - some "solution" they can just use. It just has to be "as they want".
In the Component Approach
such a solution is known as a Component
🤷♂️, no more, no less.
So, yes, go with option one, pick your way to render HTML, not a big deal, pick the way you do(if you do) virtualisation, pick the way to handle "table data" - there are many headless tables on NPM, and assemble in a way you need.
If one day later you will have another use case with slightly different requirements - create another Component
, assembled in another way.
But it is important to have this intermediate abstraction layer, which states "This is how tables are made here", as exactly this point may change in time (redesign) and you want to avoid Shotgun surgery or Domino Effect. You want a single change to a single component on UIKit/Design system side, not any client code.
You want to stand on Giant Shoulders.
Modal
Modal is a combination of both cases above.
-
Modal
itself should just provide you aModalDialog
functionality. - But the Application might need:
-
ConfirmModal
, having aCancel button
and theAction button
, next to each other in some particular order (depending on the operation system), with (probably)Cancel
autofocused. -
InformationModal
, having only oneGot it button
-
OperationModal
to indicate some process and having no buttons.
-
Plus FileModal
, that is not a "Design Primitive", but a separate experience with its own rules and principles.
HTML Dialog Element
->JS Modal Dialog
->Use Case Dialog
🤷♂️ We are ok to use window.alert
and window.confirm
, but almost no "UI-library" provide a second-abstraction over their modals to reflect the same functionality.
Modal is a lower-level construct that is leveraged by the following components:
- Dialog
- Drawer
- Menu
- Popover
(source Material UI)
If it is ok for you to use a Modal
in some patterns, some of which do not look that modal, why not create more patterns that are closely relayed to the Modal
/Dialog
, but represent particular use case?
Once you have a foundation - try to build something from it
and create a foundation for the next layer.
Then try to build something from it.
Unidirection flow
Another crucial but constantly overlooked part of separating something into layers is the controlling of the relations
.
- level above should be built on the level below
- in no circumstances level below can know about layer above
- level above should be confident in the level below
- level below should change less frequently
This translates into two well known and well hated and thus ignored laws:
- Law of Demeter or principle of least knowledge. Read it as - you can know about webpack, webpack cannot know about you.
-
Stable Dependency Principle stating that components dependencies should be in the direction of stability, so you can build your code
Once and only once
, and not rewrite it every month to adjust to ever-changing platform.- mind Shotgun surgery and Domino Effect, there is basically no time for it.
Middle-level architecture
So, UI is a piece a cake?
👉 Yes, it is if you think about it as about Cake. Layer on top of another Layer.
Are you already using Onion Architecture, where layers are separated?
👉 Of course. Look inside your node_modules
, and think how many other packages, libraries and layers are hidden behind the ones you know about.
There are High-Level Architecture(read pictures) and Low-Level Architecture(building primitives), what is this one about?
👉 And this one is about something exactly in between - Middle-level Architecture, combining "some given blocks" to create an Application according to HLA.
The one usually forgotten, and the one you always have to define for yourself by yourself.
Actionable advice
Take a single component and try to find another structure inside it. Find a Modal
behind a Dialog
, find a FocusLock
behind that Modal
, go to the very end on the left (atoms) and then go back to the very right(combinations).
Think in Atoms -> Molecules -> Organisms, not from Atomic Design point of view, but as a unidirectional complexity flow.
Remember the Table
– you should be able to have a Complex Component A
break it into pieces and assemble into Complex Component B
. Then go back to those pieces and break them down.
👉 That is converting a single whole to the Essence and the Variables.
The point here - layers should not interfere, and should not be used in skip levels(Organism should never use Atom) that will enable their reusability and provide maintainability free from Shotgun Surgery
.
Create a fancy cake, starting from More Generic
layers and going to Less Generic
ones.
And a cherry on top.
Posted on October 31, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.