4

Background

Tony Hoare's billion dollar mistake was the invention of null. Subsequently, a lot of code has become riddled with null pointer exceptions (segfaults) when software developers try to use (dereference) uninitialized variables.

In 1989, Wirfs-Brock and Wikerson wrote:

Direct references to variables severely limit the ability of programmers to refine existing classes. The programming conventions described here structure the use of variables to promote reusable designs. We encourage users of all object-oriented languages to follow these conventions. Additionally, we strongly urge designers of object-oriented languages to consider the effects of unrestricted variable references on reusability.

Problem

A lot of software, especially in Java, but likely in C# and C++, often uses the following pattern:

public class SomeClass {
  private String someAttribute;

  public SomeClass() {
    this.someAttribute = "Some Value";
  }

  public void someMethod() {
    if( this.someAttribute.equals( "Some Value" ) ) {
      // do something...
    }
  }

  public void setAttribute( String s ) {
    this.someAttribute = s;
  }

  public String getAttribute() {
    return this.someAttribute;
  }
}

Sometimes a band-aid solution is used by checking for null throughout the code base:

  public void someMethod() {
    assert this.someAttribute != null;

    if( this.someAttribute.equals( "Some Value" ) ) {
      // do something...
    }
  }

  public void anotherMethod() {
    assert this.someAttribute != null;

    if( this.someAttribute.equals( "Some Default Value" ) ) {
      // do something...
    }
  }

The band-aid does not always avoid the null pointer problem: a race condition exists. The race condition is mitigated using:

  public void anotherMethod() {
    String someAttribute = this.someAttribute;
    assert someAttribute != null;

    if( someAttribute.equals( "Some Default Value" ) ) {
      // do something...
    }
  }

Yet that requires two statements (assignment to local copy and check for null) every time a class-scoped variable is used to ensure it is valid.

Self-Encapsulation

Ken Auer's Reusability Through Self-Encapsulation (Pattern Languages of Program Design, Addison Wesley, New York, pp. 505-516, 1994) advocated self-encapsulation combined with lazy initialization. The result, in Java, would resemble:

public class SomeClass {
  private String someAttribute;

  public SomeClass() {
    setAttribute( "Some Value" );
  }

  public void someMethod() {
    if( getAttribute().equals( "Some Value" ) ) {
      // do something...
    }
  }

  public void setAttribute( String s ) {
    this.someAttribute = s;
  }

  public String getAttribute() {
    String someAttribute = this.someAttribute;

    if( someAttribute == null ) {
      someAttribute = createDefaultValue();
      setAttribute( someAttribute );
    }

    return someAttribute;
  }

  protected String createDefaultValue() { return "Some Default Value"; }
}

All duplicate checks for null are superfluous: getAttribute() ensures the value is never null at a single location within the containing class.

Efficiency arguments should be fairly moot -- modern compilers and virtual machines can inline the code when possible.

As long as variables are never referenced directly, this also allows for proper application of the Open-Closed Principle.

Question

What are the disadvantages of self-encapsulation, if any?

(Ideally, I would like to see references to studies that contrast the robustness of similarly complex systems that use and don't use self-encapsulation, as this strikes me as a fairly straightforward testable hypothesis.)

3
  • If I'm reading this correctly, you're asserting that nulls should be considered harmful and never used? Would self-encapsulation prevent the perceived need to test for if getAttribute() == defaultValue? Nov 12, 2013 at 19:41
  • The hypothesis is that using class-scoped variables directly results in code that crashes more often than using variables indirectly (and consistently) via accessor methods that force all uninitialized variables to be initialized immediately prior to use. In the example shown, calling setAttribute( null ) will cause the attribute's value to be re-initialized prior to use (e.g., makes a database call to find the current value). Nov 12, 2013 at 21:35
  • Why allow setAttribute(null) if it's not valid for the attribute to be null? If non-null was asserted there (and an appropriate value set in the constructor), then the attribute can be used directly with no chance of being null.
    – Errorsatz
    Apr 4, 2019 at 21:34

2 Answers 2

9

The disadvantages are the inefficiency of the extra indirection, as you pointed out, and the fact that the compiler doesn't enforce it. All it takes is your worst programmer using one unencapsulated reference to destroy the benefits.

Also, the right way to solve a null pointer problem isn't to replace it with a non-null default value with essentially the same characteristics. The problem with null pointer dereferences isn't that they cause a segfault. That's just a symptom. The problem is that the programmer might not always handle an unexpected default/uninitialized value. That problem still must be handled separately with your self-encapsulation pattern.

The right way to solve a null pointer problem is to not create the object until a semantically valid non-null value can be put into the attribute, and to destroy the object before it is necessary to set any of its attributes to null. If there is never the possibility for a pointer to be null, there is never a need to check it.

Usually when people think an attribute must be null, they are trying to do too much in one class. It often makes the code much cleaner to split it into two classes. You can also split functions to avoid null assignments. Here's an example from another question where I refactored a function to avoid a problematic null assignment.

5
  • +1. I neglected to mention that the createDefaultValue() method is used, lazily, to provide a valid non-null value -- this method abstracts how the value is initialized while allowing subclasses to override how the initial value is obtained. This provides reusability through the Open-Closed Principle. Nov 12, 2013 at 21:19
  • While developers must handle unexpected values, eliminating null pointer dereferences would help produce crash-free software (other flaws would still be present). You mention needing to handle uninitialized values, but self-encapsulation ensures all values are initialized (i.e., never null), which means that no extra code is required to handle the null (i.e., uninitialized) case. Nov 12, 2013 at 21:38
  • 2
    In the case of data not being initialized properly, I'd much rather have a crash at the time of the bad data being encountered rather than silently using an incorrect value. The incorrect value will manifest itself later as a problem that's located away from the original source. This makes things much harder to debug.
    – 17 of 26
    Nov 12, 2013 at 21:52
  • I disagree about making issues harder to debug. By consistently applying self-encapsulation, it makes the entire system easier to debug. Add a stack trace in the accessor to show exactly when the unexpected value was set and how. Combined with Aspect Oriented Programming, debugging the system becomes trivial. I cannot imagine a manager saying, "The system should crash for our users." Nov 12, 2013 at 22:02
  • 1
    A crash that a developer can't ignore is better than a subtle data corruption issue he doesn't notice. Nov 13, 2013 at 1:26
0

There are times when it would be helpful to be able to define a data type that would hold a reference to an immutable object, but would behave as an immutable object, rather than a reference, such that code written as:

thing.foo(bar);

would compile as a call to a static method:

classOfThing.do_foo(thing, bar);

where the static method could then handle the case of the first argument being null in whatever manner it saw fit. A lot of string-handling code in Java could have been cleaner if String were such a type; an uninitialized variable of type string could then behave as an empty string rather than a null reference. Conversions between such types and Object might have been a little tricky [each such type could perhaps define a singleton object to represent the default value for instances of its type, so converting a default-valued string to type Object or String would yield a reference to String.defaultInstance, conversion of a null reference to String would yield an NPE] but such types could have made some things much cleaner.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.