In my experience working on many UI code bases, I observe a lot of business logic is still being programmed imperatively instead of declaratively.
When programming imperatively, business requirements often get pushed to the background and implementation details are in the foreground.
Contrasting with declarative code, the business requirements are the foreground and implementation details, background.
While imperative programming is both fundamental and essential, we should leverage declarative patterns to maximize the connection between business requirements and the development process.
Imperative Programming - is a style of programming where the developer describes HOW each step of the program should execute to achieve the desired result.
Declarative Programming - is a programming style where a developer describes WHAT the program is supposed to do without detailing every exact step of how it is done.
With imperative programming, the code is written as detailed step by step instructions.
As an example, let’s consider a very simple list of UI requirements for submitting a form that shows a spinner during form submission and a response message.
Form UI Requirements
The form has:
A spinner.
Response message after submission.
When the user submits a form:
If the form submission succeeds:
Hide the spinner.
Show Success Message.
If the form submission fails:
Hide spinner.
Show Error Message.
Below is an example of how it can be programmed imperatively - I have attached comments to describe each step of the code.
// STEP 1.
// Describe the initial state of the form
const formState = {
showSpinner: false,
submissionResponse: '',
};
// STEP 2. Submit the form
function submitForm = (formValues) => {
// STEP 3. Make the spinner show
formState.showSpinner = true;
// STEP 4. Call the API to submit the form
apiService.submitForm(formValues).then(
() => {
// STEP 5a. If the submission is successful,
// hide the spinner and show success message
formState.showSpinner = false;
formState.submissionResponse = 'Success!';
},
// STEP 5b. If the submission is unsuccessful,
// hide the spinner and show error message
() => {
formState.showSpinner = false;
formState.submissionResponse = 'Error!';
})
}
A developer reviewing the code has to follow step by step to the end in order to validate the expected behaviour.
This is not a big deal for this very simple example. But suppose we add more complexities such as:
Automatic submission retries on error.
A counter to keep track of the number of retries.
As complexity increases, some consequences arise:
More Development Time and Mental Load on Implementation. The developer has to spend more time figuring HOW the code works, and whether it is behaving as expected.
Higher Risk of Bugs. Since the program has a sequence of interdependent steps, adding new requirements and changes in the code has a greater chance of breaking the logic in the sequence.
Now let’s see what declarative code would look like with the same requirements.
We will even add the extra complexity of submission retries on errors and keeping count of the retries.
const RxFormSubmission = () =>
RxBuilder({
initialState = {
showSpinner: false,
submissionResponse: '',
submissionRetries: 0,
},
reducers: {
retryingSubmission: (state) => ({
showSpinner: true,
submissionResponse: '',
submissionRetries: state.submissionRetries + 1,
}),
submitForm: () => ({
showSpinner: true,
submissionResponse: '',
}),
submissionSuccess: () => ({
showSpinner: false,
submissionResponse: 'Success!',
}),
submissionFailure: () => ({
showSpinner: false,
submissionResponse: 'Error!',
}),
},
effects: [handleFormSubmission] // Handles the communication and retries with the server API
});
The code mirrors the requirements’ bullet points. Any developer can look at the code right away, know what it does and whether it is in line with business requirements.
It explicitly declares WHAT the behaviour should be on each action/event.
The benefits of declarative code:
Faster Development Time. Developers can focus on the business logic vs detailed step by step implementation details.
Fewer Bugs. New requirements can be implemented with less chance of side effects (i.e. breaking a sequence of program steps)
Imperative programming will always be essential for optimization and fine-grained control for customizing implementation.
However, programming declaratively as much as practicable will help keep business requirements in the forefront and create better synergies between the development process and business needs.