Leveraging declarative programming to improve cross team collaboration
Many articles have been written about what imperative and declarative paradigms are. There is a lot of information out there but in this article I would like to share an analogy with those two concepts which I believe is useful to improve the collaboration within the development team and also between development and design teams.
The start has to be quite theoretical but I believe at the end the concepts are connected with the collaboration, so let’s jump into a bit of mud.
According to Wikipedia, imperative and declarative programming are:
In computer science, imperative programming is a programming paradigm that uses statements that change a program’s state.
In computer science, declarative programming is a programming paradigm that expresses the logic of a computation without describing its control flow.
Imperative is how, declarative is what
Translating those sentences to a more plain language: imperative programming describes the steps to accomplish a task while declarative describes the task itself. It is an approach, does not entirely depend on the language we use.
Examples of declarative vs. imperative
As usual, the theory is clearer with a couple of examples. We will use Javascript to illustrate the concepts. Let’s imagine we have an array of numbers and we need a second array mapping the same values but adding one to every number from the original array.
Typically, in imperative paradigm we would create the loop ourselves using a for statement
with its initialized var, the increment, its limit and a push
to the results array.
const numbers = [0, 1, 2, 3];
const plusOne = [];
for (let index = 0; index < numbers.length; index++) {
plusOne.push(numbers[index] + 1);
}
While in declarative paradigm we would just declare what we want, in this case a map
method with the transformation function.
const numbers = [0, 1, 2, 3];
const plusOne = numbers.map(n => n + 1);
The next example is probably clearer: creating a paragraph with inner text and an on click
event.
In imperative we would create step by step the elements: the paragraph, the text node, the listener, and then append the text node to the paragraph.
const paragraph = document.createElement('p');
const text = document.createTextNode('Lorem ipsum');
paragraph.addEventListener('click', () => console.log('hey!'));
paragraph.append(text);
Leveraging React
and JSX
we could code this same example in a declarative way, focusing only in what we want, not how to create it.
const Paragraph = () => (
<p onClick={() => console.log('hey!')}>
Lorem ipsum
</p>
);
Also, HTML is by nature a declarative language. It describes the elements tree, but does not instruct the browser about how to internally build the DOM.
Advantages of declarative programming
My conclusion from the last examples is that declarative programming relies on proven tools that allow us to work on a higher abstract level. Following the examples, the map
method, or the React components, are tools those tools.
The declarative approach allows us to:
- Write less code.
- Work faster.
- Test less, since there is less code to test.
- Increase maintainability.
- Increase the single responsibility level of our code.
And what in my opinion is one of the most important things: the developer focus doesn’t get spent in implementation details, but in bigger pieces of the puzzle more architecture related.
In any case, I believe there are different degrees between declarative and imperative approaches. Not everything is 100% on one of the sides, and also is a matter of interpretation.
Why declarative concept is important for collaboration
At this point, I think is pretty clear what those two approaches are and their differences.
Now, I believe declarative paradigm is a concept we, as developers collaborating between us and with design teams, should keep in mind in our every day work.
Apart from the advantages of the declarative approach listed earlier, it has another strong key aspect. Declarative programming increases the abstraction level and it is posible to leverage those higher abstraction layers to build conventions to better collaborate between devs and with design teams.
As we said before, declarativity relies on proven tools which allow us to work on a higher level of the stack. We don’t have to code an array transformation, we use one, we don’t have to manage the DOM directly, React does it for us.
Those tools are abstraction layers, the abstraction layers are conventions, and conventions mean agreements. Agreements that in our every day work could be:
- What is the responsive approach for our websites.
- How to treat typographies or colors.
- What is the vertical and horizontal spacing, including wrappers or grids.
- What to approach componentization.
- How the components get variants or modifiers.
- How to approach animations.
And a long list of etceteras… basically anything that can be coded in the Frontend of a website. Anything that developers need to be aligned between them and also with design teams.
CSS abstractions to increase declarativity
I will cover more abstraction layers in other articles, but in this one I believe one of the key aspecs to collaborate with design profiles is the abstractions related to UI, that is why from here on I will give some CSS examples.
I understand CSS is a declarative language itself, but higher abstraction layers make it more declarative, so bear with me for the analogy / application of the concept here.
Let’s imagine we are coding the typography for a section title. We have the values from the design team for every breakpoint. It would typically look like this at the lowest css (ok, scss) level.
.section {
.title {
font-family: Lato, sans-serif;
font-size: 20px;
line-height: 24px;
letter-spacing: 0.1px;
@media (min-width: 768px) {
font-size: 26px;
line-height: 38px;
letter-spacing: 0.15px;
}
@media (min-width: 1200px) {
font-size: 32px;
line-height: 36px;
letter-spacing: 0.2px;
}
}
}
From there, you can increase the declarativity with different strategies or abstraction layers: vars for the different values, mixins or extends to group the typography props, functions to get or calculate values, etc.
An even higher abstraction layers is what I like to call typesets
. I understand typesets as groups of multi breakpoint typography props. Meaning, a typeset
is a group of props to define a typography including the whole responsive behavior based on the breakpoints defined in the setup of the project.
In future posts I could show how I implement the whole solution and the setup expected, but for the purpose of this post, I think it is enough to see how I use the typeset mixin.
.section {
.title {
@include typeset(header-1);
}
}
That mixin encapsulates all the properties predefined for the typeset header-1
including its responsive behavior. That mixin will expand the css code once compiled to be exactly as it was the previous example.
Typesets are, hence, a convention I use along with the different designs teams to improve the collaboration. That convention allows developers to be more declarative in the code while giving to designers the ownership of the design values.
Another very useful example of this: the wrappers.
A wrapper manages the horizontal distance from the content to the edges of the screen. It is equivalent to the classic container
concept in Bootstrap.
Following the last example structure, at the lowest abstraction level it could be something like this:
.footer {
.footer-wrapper {
padding: 0 20px;
width: 100%;
@media (min-width: 768px) {
padding: 0 4vw;
}
@media (min-width: 1200px) {
max-width: 1104px;
}
}
}
Leveraging a convention, it could be coded this way:
.wrapper {
@include wrapper(default);
}
Where default
is a set of values (paddings, widths, max-widths, etc) owned by the design team.
Intentionally I used a global class called .wrapper
, instead of the specific class inside the footer as in the previous example. That way the wrapper can be instantiated in HTML, as it is part of the general site structure.
<div class="wrapper">
...
</div>
There are plenty of examples illustrating these conventions, some of them describe specific solutions, others are smaller internal conventions, but I believe understanding the declarative approach can make us better developers and better teams.
Find the right declarativity level for your team
But hey! Does this mean the more declarative the better? Not necessarily.
For example, rising some abstractions to one of the top levels, HTML in our case, could result in a highly declarative code.
<p class="typeset-header-1 color-dark background-blue">
Hey ho!
</p>
Does this work for all the teams and projects? Probably not. I find this solution interesting for teams where backend developers need to integrate quickly UI solutions or to quickly prototype. For other purposes probably this is not the best solution because of the architectural implications it has: a bunch of global classes, specificy issues…
Another example, highly intelligent scss mixins:
.section {
.title {
@include size(large);
}
}
Imagine large
holds a single value with a number in pixels for the font-size
. The rest of props needed for the typography to work (line-height
, letter-spacing
, all the incremental sizes from mobile to desktop would be computed out of a single value) would be calculated in some function inside the mixin.
Again, that highly declarative solution is appropriate for all teams? This solution would compute the final values in functions. Could be interesting for example in a team with hybrid frontend and design profiles where the calculations are clear and agreed. Other teams for sure will prefer to keep the ownership of the values themselves.
Wrap up
Declarative paradigm leverages proven tools to allow developers to work on higher abstraction layers. Those higher abstraction layers can be converted into conventions to establish agreements between development and design teams.
These are my key learnings:
- Find and agree this conventions along with your team analyzing your current workflow, what concepts are already working fine, need to be improved or removed.
- Create a common language for those conventions so they have name, a purpose, features, posible values…
- Start from the most basic layers and build from there more complex ones.
- Code the abstraction layer to support the conventions.
- Test them, challenge them often.
- Iterate over those conventions in order to adapt, improve or remove.