"Checklist" Programming

Writing functions as if they were a checklist that needs to be completed one step at a time can help when writing tests.

The topic of this post is entirely personal preference. As I mention in The Constructor (an introduction post), I am not an expert at anything. This style of code structure is something I have been using for a few years now and it has not been perfected. I started using it in an effort to help write more Unit Tests and get better Code Coverage.

This style of code structure is intended to address a few aspects of Clean Coding:

  • Early Exit - if there is no reason to continue, get out

  • Keep it short - People have a short attention span, do not ask them to remember more than one or two things

  • Readability - Tell the reader exactly what is going to happen

  • Avoid "arrow" code - Reduce the levels of indentation in a function/file

  • Avoid switch statements - I do not know why we want to avoid them, but we can

Examples

A "perfectly fine" function:

public bool IsAValidObject(SomeThing st) {
  bool isValid = true;
  if (st is null || st.Id <= 0 || string.IsNullOrWhiteSpace(st.Name)) {
    isValid = false;
  }
  else
  {
    isValid = true;
  }
  return isValid;
}

A shorter, but not cleaner nor straightforward, function would be something like this:

public bool IsAValidObject(SomeThing st) {
  return (st is not null && st.Id > 0 && !string.IsNullOrWhiteSpace(st.Name));
}

The above functions do exactly what they say. But they could be "cleaner":

public bool IsAValidObject(SomeThing st) {
  if (st is null) return false;
  if (st.Id <= 0) return false;
  if (string.IsNullOrWhiteSpace(st.Name)) return false;
  return true;
}

This function does the exact same logic, reads like a checklist (if this then that), and shows you how many tests should probably be written. In the previous function, someone may have written tests just to get a true and get a false result. It is entirely possible that someone could do the same thing (test wise) with the new structure.

Here is another example:

public ActionEnum DetermineNextAction(SomeThing st) {
  ActionEnum nextAction;
  switch (st.Status) {
    case StatusEnum.New:
      nextAction = ActionEnum.Initiate;
      break;
    case StatusEnum.Initiated:
      nextAction = ActionEnum.Approve;
      break;
    case StatusEnum.Approved:
      nextAction = ActionEnum.Complete;
      break;
    case StatusEnum.Completed;
      nextAction = ActionEnum.Archive;
      break;
    default:
      nextAction = ActionEnum.Unknown;
      break;
  }
  return nextAction;
}

Obviously, there are probably other better ways to determine what the next action should be. Just using it for an example... If this were a function that needed to not change in result, here is how I would clean it.

public ActionEnum DetermineNextAction(SomeThing st) {
  if (st.Status == StatusEnum.New) return ActionEnum.Initiate;
  if (st.Status == StatusEnum.Initiated) return ActionEnum.Approve;
  if (st.Status == StatusEnum.Approved) return ActionEnum.Complete;
  if (st.Status == StatusEnum.Completed) return ActionEnum.Archive;
  return ActionEnum.Unknown;
}

Does the exact same thing, is shorter, straightforward, zero levels of internal indentation versus the previous two, does not require the knowledge of a switch statement structure...

Notes

You cannot avoid all indentation, and that is not the goal. The goal is not to reduce lines of code, but it is nice when you can. Some people may want to add empty lines between some sections of logic, their choice. Some people do not like multiple returns in a function - ok.

The Tests

For me, the biggest goal of this code structure is to show as many possible tests that can be written to validate thoroughly the behavior of the function. It has been my experience that the number of tests written is the number of return statements plus one. If a function returns a boolean, one test to return true and one test to return false. If a function returns a string, one test to return the desired string and maybe another test to return a different string.

In the first example above, there was a single return statement and likely two tests would have been written to confirm a false result and a true result. In the "cleaned" version of the function, we see that there are at least four functions that can be written. The string.IsNullOrWhiteSpace could be three tests alone - null, string.Empty, and spaces (" "). That would be a total of six tests.

There are some who would argue that that many tests cause the code to be rigid. They are not wrong. I am ok with the rigidity. I think it provides a level of defensive programming that should protect the code.

Conclusion

This code structure may not be for everyone. That is perfectly fine. I think the benefit it provides to writing Unit Tests, the readability for new and seasons developers (and even non-developers), and the conciseness of the code are all better pros than the potential cons.

I would love to hear your thoughts on other potential pros or cons to this code structure. In my limited experience, it has served me well. But there may be other situations where this becomes more of a hassle than a benefit.