In Search of the Meaning in Imperative Code
Tags: tdd, functional, testing, javascript
Published: 2022-01-10 15:15:30

TL;DR 
Imperative code tells you how, declarative code tells you what, good tests tell you why.

On the backend, I frequently come across imperative code that looks kind of like this:
```javascript
async function saveUserSettings(companyId, userData) {
  if (userData.userId) {
    const existingUser = await userRepository.findById(
      userData.userId
    );
    userData.settings = mergeExistingUserSettings(
      existingUser.settings,
      userData.settings
    );
  }
  if (companyId) {
    const company = await companyRepository.findById(companyId);
    userData.settings = mergeCompanyAndUserSettings(
      company.settings,
      userData.settings
    );
  }
  if (session.settings) {
    userData.settings = mergeSessionAndUserSettings(
      session.settings,
      userData.settings
    );
  }
  await encryptSensitiveDataForDb(companyId, userData);
  const savedUser = await userRepository.save(userData);
  await decryptSensitiveDataForUi(companyId, createdUser);

  session.settings = undefined;

  return savedUser;
}
```
While you can probably figure out that it's saving a user in the db, it's difficult to skim. You can't really know what each if branch does without reading it fully. And even after you do that, unless you're the one who wrote the method in the first place, you likely won't know why each of the steps is necessary.

As a functional programming fanboy it's tempting to think that declarative, promise chains (or pipes in other languages) hold the answer. Here is the same function written in that style:

```javascript
async function saveUserSettings(companyId, userData) {
  const savedUser = await (
    maybeMergeExistingUserSettings(userData.userId)
      .then(maybeMergeCompanyAndUserSettings(companyId))
      .then(maybeMergeSessionAndUserSettings(session))
      .then(encryptSensitiveDataForDb)
      .then(saveUser)
      .then(decryptSensitiveDataForUi)
  );

  session.settings = undefined;

  return savedUser;
}
```
The steps of our algorithm are now clearer and the whole thing is skimable. If we want to signify that the order of certain steps doesn't matter, we can easily refactor so that they execute in parallel. In short, this gives us a good sense of _what_ is happening, instead of just _how_ it is happening.

If most of the world's code was written like this, I'd be happy, and it's tempting to stop here. But from a business standpoint, this still leaves us asking why questions. _Do we need so many separate merging steps? What user-facing or business requirements do these cover? Did we miss any? Could we remove some to clean up the code?_ At a startup where requirements change often, these are always relevant questions.

# Tests can help

The only real way to specify business requirements in code (without getting too deep into DDD) is to use tests:
```javascript
describe('saveUserSettings', () => {
  it("can save a preexisting user's settings", () => {});
  it("can save a new user's settings", () => {});
  it("can save a user's settings who belongs to a company", () => {});
  it("can save a user's settings who does not belong to a company", () => {});
  it("can save a user's settings when the user has settings cached in-session", () => {});
  it("can save a user's settings when the user does not have settings cached in-session", () => {});
  it("can save a user's settings when some of them are sensitive", () => {});
});
```
If we had initially read a test suite like this before trying to understand the method, it would have helped a lot. We can see that the method's branches come from its business use cases. If the requirements ever change we can first change the tests before refactoring.

Most importantly, now that the burden of meaning is being borne by the test rather than the implementation, we can write the original method in whatever style our coworkers find most legible, no functional programming needed!