DEV Community

Cover image for In Search of the Meaning in Imperative Code
David Woodward
David Woodward

Posted on

In Search of the Meaning in Imperative Code

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:

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;
}
Enter fullscreen mode Exit fullscreen mode

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:

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;
}
Enter fullscreen mode Exit fullscreen mode

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:

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", () => {});
});
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (0)