You might see a lot of people talking that declarative is better because it is simpler and give an example like this:
declarative code (what would you like):
a tea
vs imperative code (what would you like me to do):
- boil water
- pour water into a cup
- add a tea bag to the water
One short line of declarative code vs three long lines of imperative code. Evidently declarative must be better. But is this a fair example? What if we modify it a bit:
declarative code:
a tea
vs imperative code:
make a tea
Not much a difference, is it?
So, is declarative superiority just a hoax?
Spoiler alert: no, but if you want to know the real reasons, read on.
The basics
With declarative code, we specify what we'd like - the end result or state.
With imperative code, we specify a set of steps - an instruction that we'd like to be performed.
What are pros and cons of both? From a developer perspective, when looking at the code we are mostly concerned about what the code does (the end result), not so much how the code makes it happen (the steps). That's why we tend to put imperative code like this:
- boil water
- pour water into a cup
- add a tea bag to the water
into a module with a meaningful name like makeTea
. This saves a bit of time and effort needed to figure out what that three lines are all about. With declarative code we declare the final goal so we have this feature inherently. That's a slight advantage of declarative code - we immediately know the end goal, with imperative code the end goal is implicit (it is there but we need to analyze the lines to get it or otherwise comment the code, or put it into a function or a module with a meaningful name).
The real advantage of declarative code is that it doesn't specify the starting point. With imperative code an initial state is implicit and mostly we start from scratch (which is the easiest way) like here:
- boil water
- pour water into a cup
- add a tea bag to the water
It implies that we are starting with cold water. What if we already had a hot water? It would get thrown away or would be boiled twice. A wasted effort.
With declarative code the system can be smart and reuse the resources it already has. The way of figuring out how to go from current state to the desired state is not our business anymore because we can't really specify the initial state in a declarative language - the state change is managed internally by the engine.
That's the real deal here:
- With declarative, you specify the end goal and let the system work it out by itself.
- With imperative you implicitly specify the starting state (or states if you want to be more resource efficient) by explicitly specifying a set of steps that lead to some final state.
What are some real-world implications of this?
Let's say we want some function that takes a string of HTML and renders it.
The easiest implementation would be:
const render = (html) => document.body.innerHTML = html;
The problem with this is that DOM is stateful, for example, the user might type something to an <insert>
element and we want to preserve that. The above code doesn't care about what is currently in the <body>
, it throws away everything and puts new content there.
You might've heard that React is declarative. In fact React render()
function is declarative. It takes a description of how view should look like and figures out a way to make it happen. It is declarative because it takes a description as an input. It might work similarly to the code above - discard current content and replace it with the new one - but is much more intelligent and reuses the elements when it can.
That's the power of declarative. You only need to specify the desired state and let the system figure out how to get there. This is really important in places where resource reuse is important, where you can't just throw away everything and start from scratch like in DOM and network clusters because it takes this really hard problem of figuring out how to go from any state of the system to the desired state off your back.
Top comments (7)
So, the real power of a declarative approach is, that you can use REACT in an environment, that was not made for it?? Why not use an approach that fits better to the DOM? If you are in the mountains, why would you try waterskiing? Go climbing!
Let's say you want to do business in China but the problem with this is that you don't speak Chinese. You can either learn Mandarin or use a translator.
The reason why you should use a declarative framework instead of talking directly to the imperative DOM API is the same as the reason why you don't write assembler code anymore but use higher-level languages - even if it adds some overhead it's still worth it because it's much easier to read, write and organize.
You probably program computers for years and still don't know the only true computer language: machine code and that's all right because it's for computers, not humans. If you're in the mountains you don't have go native and start sheep breeding; you can just stay yourself and do some rock climbing.
If I could only use Assembler or Haskell, maybe I would choose Haskell. But there are several opportunities. So we can decide.
We have tried to implement a clean object oriented design patter close to the DOM, and it comes out this fits well. You get minimal overhead, as you can tightly couple your objects to DOM elements. It seems, most of the problems you get with the declarative approach (e.g. correctly manage state transistions) do not exist in the class based approach.
I assume you can write efficient code using one or the other design pattern. But with a declarative approach on a stateful DOM it seems, 80% of the work is just done to solve problems you would just not have with an imperative approach.
We can theoretisize all day about it or let the code talk.
Here is a simple Todo App created with a declarative framework.
Build an app with equivalent features in your framework and we'll compare strengths and weaknesses of both approaches in practice.
Nice competition, just the example already existed:
See the toDo example here
For the example page, the base is just procedural code. There is no clue using a class if you have only one object. In a real world application I would have used two classes to make the whole thing resuable:
See the "markdown component" example for an example how this would look. Usually this is very simple to put all the "generating" code into the constructor and let the rest be class methods.
It's interesting that you haven't implemented filtering todos in your example because that's the perfect case of why declarative is better than imperative.
At this point your todo app interacts with the DOM in two ways:
These are relatively simple operations to carry out in DOM API. Shifting elements around is a bit more difficult. I don't have to worry about it because my declarative framework does it for me automatically.
If I have currently displayed a list of completed todos:
[1, 3, 5]
and want to see all todos[1, 2, 3, 4, 5]
I don't have to move DOM elements around myself - I just pass the new list to a component and it gets done automatically. Can your imperative framework do it for you?Same goes with your
todo
state. You're touching the DOM not only inconstructor
but also insetState
so you have to remember that keep those two places in sync so that just createdtodo
looks identical totodo
that transitions fromchecked
to!checked
(initial) state. You played smart and putsetState
at the end of constructor instead of just setting the proper style directly inthis.ta
.But this has its own problems too. In
readData
you first create a todo:so you're calling
setState
once already inconstructor
. Then, you're calling it again explicitly the second time:That's a code smell. You're also changing
todo
element styles there:So now you're modifying the DOM element in three places:
constructor
,setState
andreadData
. I'm smelling something... something like... spaghetti? ;)In declarative approach things concerning how something looks are all located in just one place: a component.
Another problem with OO approach is serialization. Because your todos are custom objects you can't just stringify them but rather use some tinkering to get it done. Unfortunately, the logic don't properly clean up todos removed by the user so they could stay in user's
localStorage
forever.In declarative approach data is just plain objects so
JSON.stringify
does the job.All in all, declarative approach provides better code organization and less code to write and maintain since you only care about transitions from
0 -> x
and0 -> y
. Transitions fromx -> y
andy -> x
are worked out automatically.Well, first let me thank you for your thoroughly analysis. I´m pretty sure it depends much on the task which approach plays better.
You should also see: The DML-library I´m using is just a wrapper to the HTML-DOM-API, so what you see is the whole machine, nothing much behind. There is no big framework in the background that does all the heavy lifting for me. Some will find this a disadvantage, but it make the usage very lightweight and compatible to almost anything.
The ToDo-App was ready, so the filter was just not implemented. This can be handeled by the base app, but as the todoObjects handel their state autonomously, i would prefer to let them control their visibility too. So, I just would send a broadcast to all toDos and let them decide. As DOM can hide elements, this would be the preferred method.
You are right, the solution for "readObj" is not very elegant, as setState is called twice. This would be simple to avoid, but usually we would have a better organized persitence class in our projects that does the job. It´s the kind of "better do it double than never" thing you will find sometimes, but that does not happen very often.
But, this has nothing to do with "spaghetti": As the objects are tightly coupled to the DOM (in fact, nobody else has access to the DOM elements than the todoObj), state management has to be done by the objects. As the DOM elements are owned by the JS-objects, there is nothing wrong about this.
With the serialization you are pretty right, we always need som "tools" to do the job. But that was never a topic I got headaches from.
Bottomline I would say, we can do the job using both approaches.
There are pro´s and con´s on every approach, so I think it is not worth trying to win the contest. We can just try to get the best of each world.