•  Back
  • Defensive Programming Techniques

    Writing code resilient to changes and runtime errors (with Examples in Java)

    This blog is not about programming, it’s about Software Engineering. I’ve created this blog with the goal to share some of the material I’ve created over the course of my professional career. Some of the content will be about programming, some will be about process improvement, etc.

    I usually use these posts to share with the organizations I’m working for. However, I think that this content will be valuable to many other organizations out there.

    Defensive programming, in particular, is something that I think is extremely useful and many senior developers do at some level. I hope that by sharing these examples, other developers may also benefit from employing these techniques.

    What is Defensive Programming?

    We all know that there are multiple ways to write a code that does the exact same thing. This property is called “plasticity”. If you think about that, the first thing that comes to mind is that some are easier to maintain than others. Some will also be more resilient than others, meaning that if errors happen, the code can still execute correctly. Some will also help you identify programming mistakes during compilation time.

    Defensive programming is a collection of techniques that, when applied, won’t change the behavior of the code, but will make it either more resilient to runtime errors or to coding mistakes (faults).

    The concept is derived from defensive driving. In defensive driving, you adopt the mindset that you’re never sure what other drivers are going to do. That way, you make sure that if they do something dangerous you won’t be hurt. You take the responsibility for protecting yourself even when it might be the other driver’s fault. In defensive programming, the main idea is that if bad data is passed to your code, the application won’t be hurt, even if the bad data is another code’s fault. More generally, defensive programming is the recognition that programs will have problems and go through faulty modifications, and that a smart programmer will develop code accordingly.

    Examples

    If you have other nice examples, please share them in the comments! :-)

    When comparing objects, literals/constants come first.

    What happens if your method receives a null parameter when it expects a valid reference to an object? Will it break? Validating all parameters in all methods will make your code harder to understand and is often not necessary. Look at the example below:

    Don’t do this:

    if (variable.equalsIgnoreCase(literal)) {}
    if (variable.equals(STATIC_CONSTANT)) {}

    It is quite obvious that if the variable is null, a NullPointerException will be thrown at runtime. The equivalent solution below prevents that from happening with no side effects.

    Do this instead:

    if ("literal".equalsIgnoreCase(variable)) {...}
    if (STATIC_CONSTANT.equals(variable)) {...}

    Don’t trust the API.

    Especially with old methods. Many of them will return null as if it represents an actual return value.

    Don’t do this:

    String[] files = file.list();
    for (int i = 0; i < files.length; i++) {...}

    There is a philosophical discussion about the true meaning of null. Null is often treated as an absence of value when in fact it represents an unknown value. This is one of the many cases in the API where the decision to return null does not represent the actual result, where no valid return was found.

    Do this instead:

    String[] files = file.list();
    if (files != null) {
      for (int i = 0; i < files.length; i++) {...}
    }

    Why is it that for 3 files we get an array with 3 elements, for 2 files the array has 2 elements, for 1 file the array has 1 element but for 0 files we get null instead? Note that if the return had been an empty array (to represent 0 files), it would be a valid return (since there are no files it is expected that the array would contain 0 elements and there would be no problem.

    Do not return ‘null’.

    As we’ve seen in the topic before, null is often mistaken by a value that represents absence, while in fact it represents an unknown value, and assigning that value to any variable that can represent the state of the application will cause instability. It is one thing to say that the name of a person is not present in the system (by the use of an empty string) but it is another thing to say that it is unknown (like the result of a heuristic method that can’t converge to an answer).

    Don’t do this:

    public String method() {
      String result = null;
      if (condition) { //if the condition is not met, the result will remain null.
        result = "example";
      }
      return result;
    }

    Whenever possible, return empty strings or empty collections in your methods. This will help the caller avoid having to deal with possible NullPointerExceptions. So in the case where the caller hasn’t taken the necessary precautions, the code will still execute appropriately.

    Do this instead:

    public String method() {
      String result = "";
      if (condition) { //if the condition is not met, the result will be an empty string.
        result = "example";
      }
      return result;
    }

    If you are defining enums, create one element that represents that the value was not set. This will be especially valuable when in use with switch-case statements.

    Do this instead:

    public enum InnerPlanet {
      MERCURY,
      VENUS
      NOT_INNER_PLANNET;//Now if you need to map a UI parameter to your enum,
                        // you can default to this one and deal with it
                        // gracefully as any code should be designed to
                        // deal with this situation explicitly.
    }

    Do not ignore exceptions or other errors.

    There are several important guidelines on how to deal with exceptions, but from a defensive programming perspective, it is important to not catch-and-ignore them.

    Catch-and-ignore comes in multiple forms, not just by having empty catch blocks. Sometimes it is a block that logs the exception but doesn’t do anything about it.

    Don’t do this:

    public String method() {
      try {
      //do something
      } catch (Exception e) {
        // do not leave it blank
      }
    }

    Don’t do this:

    public String method() {
      try {
      //do something
      } catch (Exception e) {
        log.error("Messsage", e);
        // this is almost the same as leaving it blank. An error happened and it shoud affect your code execution flow.
      }
    }

    Whether something must be done or not depends on the circumstances, but if an error has occurred (for instance, you have logged it at the error level) it means that the thread cannot proceed to execute properly as if nothing has happened. For the subsequent code in the method (or the caller method), it would be like if nothing wrong has happened.

    So, if an error happened, either try to recover from it (change the context and try again), try something different, rethrow or encapsulate and throw a new exception, but do not let it continue. If an error happened, an exception was triggered and it is expected that your code won’t flow in the “happy path” anymore. It shouldn’t.

    For examples on how to handle exceptions properly, consult Exception Handling.

    Validate the input.

    This can be applied at any layer, at any level (method, class, etc.) but what is most important is to validate if the inputs are correct at the application (API, microservice, component) boundaries (API endpoints, MVC controllers, etc.). If only a set of values is valid, reject anything that is not part of the set. If only a range is allowed, reject anything out of the range. Failing to do so allows your code to execute under invalid contexts until it fails.

    Do this:

    public void method(Integer param1, String param2) throws IllegalArgumentException {
      if \(param1 <= 0 \|| param1 >= 11) {
        throw new IllegalArgumentException("Param 1 must be higher than 0 and lower than 11.");
      }
      if ("".equals(param2)) {
        throw new IllegalArgumentException("Param 2 cannot be null or empty.");
      }
      // Do something
    }

    Throw an exception in the switch default case.

    This is a bit polemic, but it is generally considered a good rule. In a switch statement, throw an exception on the default case. If the default case has an actual business logic to be executed, create a valid case for it, with a specific value. Often new enums or constants are added to a set due to a change in requirements and old switch statements are not updated. This allows for your application to identify these scenarios and handle the exceptional situation graciously, i.e., in a planned way, since the exception would be caught and treated accordingly.

    Do this:

    public static void switchMethod(ExampleEnum value) {
     switch (value) {
      case A: {
       break;
      }
      case B: {
       break;
      }
      case C: {
       break;
      }
      default: {
       throw new IllegalArgumentException();
      }
     }
    }

    If you want to take it to the next level, for every switch that uses an enum, write a test that will trigger the exception if a new value is added without a corresponding case. This will immediately inform the developer that a potential defect has been introduced and point him to any places where this change may have an impact:

    @Test
    void testEnum() {
     for (ExampleEnum value : ExampleEnum.values()) {
      object.switchMethod(value);
     }
    }

    This technique not only prevents defects but also help with the identification of unexpected impact in other areas of the application.

    Tips and tricks

    • Avoid using acronyms or single-letter variables as they may cause confusion when reading.
    • Always try to reduce the cyclomatic and cognitive complexity by removing nesting as much as you can. If needed, invert if conditions to create fast-return blocks at the beginning of the code instead of encompassing the whole method in a single if.
       The following is a very simple example, but consider a case where the method could have several lines.
    public void printElementsIfAllowed(Collection<Elements> elements) {
      if (!printAllowed()) {
        return;
      }
      elements.stream().forEach(System.out::println);
    }
    • When refactoring, try as much as you can to use automated tools present in your IDE.
    • Type safety is important to prevent defects. Minimize the use of primitive and string constants by using Enums. If constants come from other systems and APIs, convert them to Enums inside the application. The only situation where Enums won’t apply is when the values need to be dynamic (i.e., when values need to be added or removed after compile time, like in runtime or doing installations.)
    • Watch out for variable scopes. Don’t over-extend their lives. For example, in the past, it was fairly common to see the usage of iterators combined with while blocks (don’t do this):
    Iterator iterator = collection.iterator();
    while (iterator.hasNext()) {
      Object object = iterator.next();
      //...
    }
    Iterator iterator2 = otherCollection.iterator();
    while (iterator.hasNext()) {...} //ERROR! It was supposed to be iterator2.

    As a result, iterators would have references pointing to them after the while block was executed and could be mistakenly reused (not to mention delaying garbage collection). It didn’t take too long before the general usage of iterators shifted to for blocks.

    for (Iterator iterator = collection.iterator(); iterator.hasNext();) {
      Object object = iterator.next();
      //...
    }
    //"iterator" variable cannot be reused.
    //iterator object can be collected.
    for (Iterator iterator = otherCollection.iterator(); iterator.hasNext();) {
      Object object = iterator.next();
      //...
    }iterator.hasNext();) {
    Object object = iterator.next();
    //...
    }

    Of course, for classes that implement the Iterable interface, we can use the enhanced for (if you are using Java version 5 or greater — I hope you are). Regardless, this is still relevant as an example to limit the scope of variables.

    • Run FindBugs and SonarLint in your code. Eclipse has plugins that support both. Watch the recommendations closely. They check for different things, so you probably want to run both.

    If you like this post, please share it (you can use the buttons in the end of this post). It will help me a lot and keep me motivated to write more. Also, subscribe to get notified of new posts when they come out.

    Update: If you want to see a detailed theoretical discussion about Defensive Programming, check out this amazing post by Deyvidy F.S: https://deyvidyfs.medium.com/defensive-programming-or-a-study-on-how-to-become-a-better-software-engineer-9079ffdddfe5

  •  Back
  • You might also enjoy