If you don't know what this means, Then you should read this article. If you think this is a good article, Then you should share it with friends or colleagues. If you understand what this is, Then you should comment below with additional information if anything was missed. If you think we should move forward, Then continue reading.
What is the difference between Declarative vs Imperative programming?
First off, let's talk about Imperative programming. It is most likely the one that most people inherently understand when first learning how to write the simplest of scripts.
Define: Imperative Programming
Imperative programming is a programming paradigm that uses statements that change a program's state. ~ Wikipedia
The first paragraph of this article was written in a form of imperative statements. If-This-Then-This type statements in code are imperative. Any time you end up asking a question, you have entered the realm of being imperative.
Define Declarative Programming
Declarative programming is a non-imperative style of programming in which programs describe their desired results without explicitly listing commands or steps that must be performed. ~ Wikipedia
The best example of declarative programming are as-code paradigms. For example Docker-Compose/Kubernetes configuration files or terraform and ansible. All of these applications take in a configuration file and create a series of steps to construct infrastructure to satisfy the declared configuration.
So why is one better than the other?
I wouldn't say that one is necessarily better than the other, but I try to prefer Declarative programming over imperative for these reasons.
Imperative programming is verbose and tedious. Especially in applications where a lot of different conditions can occur. The event-driven architecture is the thing that comes to mind that may have such conditions. An event comes in and the event can have multiple actions,
deleted. If you were you write some code to handle the different cases it might look something like this.
Implementing the code above with declarative programming in mind may look like more code, and in some cases, it can be, but the number of additional cases can be added with minimal lines of code and sometimes even just 1 line of code.
Where it starts breaking down
You might have noticed, or maybe not, that if you follow this kind of pattern you can quickly let the code get out of hand for edge cases in your controller functions. Say that the delete function now requires you to pass along a
groupId, where create or update don't require it. This could be a code smell that that kind of dependency should be moved out of the controller. But for sake of the argument let's press forward.
You could start implementing a contract where the controller will take in the arguments of (
update would just not use the
groupId argument. Then say some time in the future,
create needs a
role for some reason. Now all of them would have (
role). This is a terrible example but I hope you start to see where certain requirements may need to provoke a change.
Prioritize configuration over imperative programming
There are a lot of examples I can give on this particular topic. I see it all the time. You should aim to prioritize application configuration over needing to ask how this particular code should run.
First of all, don't do this because it invalidates the 12-factor app. Your application code should run the same way in every environment regardless of configuration. The only thing that should change is the configuration values or what those libraries/applications point to.
How do you fix this?
Generally, you would do this by moving your configuration into something that could be referenced on deployment and app start. Using something like environment variables so your application can pull from those to set itself up. You could also use libs like
dotenv. These allow you to create configuration based on environments at which point your application should pull those in and just run with that configuration instead of asking what environment it is in. This will reduce the number of bugs that are only reproducible in a single environment. Ironically it will make all bugs show up in all environments but that is the point! At least you will be able to reproduce it on your machine and not just stage or prod.
Here is an example. First, let me show you what the config files might look like per environment:
They don't look much different than the configuration that was being passed to the lib above, now does it? And it really shouldn't. We are merely pulling out the configuration values.
Next, this is what your new lib would look like:
As you can see, there is a lot less code here. This is because we have moved the conditional logic of determining what lane we are in, and instead to application start. In both examples you are specifying the configs the same way, the difference is depending on the lane. Only one config is used by the app at any given time. How succinct and declarative 😉.
That's a wrap
So that's it. There isn't much to this pattern but it can be extremely powerful. Powerful, yet simple. I love this pattern because it promotes commanding your application to act. Instead of needing to constantly be asking what state you're in and then realizing you need to run something.
This thing has happened.
Go. Do the things I say.
Keeping this pattern in mind can also help you set yourself up to move over to things like containers with docker. You've set yourself up to be able to start utilizing environment variables or applications like Consul by the very nature of pulling the configuration out of your code.