Start with the Null Object

How to use the Null Object to keep your code releasable

Georgios Chinis
4 min readMar 24, 2018

A newer version of this post along with more content is available in my site: https://blog.gchinis.com/posts/start-with-the-null-object/

TL/DR

When developing a new feature, I found that the use of the Null Object pattern can help keep the code releasable and naturally leads to the creation of a feature toggle.

Introduction

Every time I need to add some new functionality in my application, I face the same challenges, I need to make sure that my every commit does not introduce any regressions and also that I commit often so that my teammates and I always work on the latest code.

I have a special interest in DevOps, so keeping the pipeline green while I keep pushing often is very important for me. Push often and push early is something I take seriously.

Lately, I’ve notice that when I try to introduce some new functionality I follow roughly the same steps.

Disclaimer, I spent most of my professional life programming in Object Oriented languages, so the terms I use in the blog come from that domain. The ideas thought can be applied to any other style of programming that has the notion of an interface. In fact, I’ve used this technique recently in a Golang project. It could be that in your own favorite language some of the steps can be implemented differently or that there are libraries that take care of the heavy lifting for you, if so I encourage you to use them.

The feature

First of all, let’s frame the problem. There is a new feature in the Backlog! Our amazing application needs to integrate with a 3rd party weather service to read the current temperature for our location.

The steps

The Interface

I start by deciding the interface for the operation. The priority in this step is to create an interface that makes sense for the callers and has less to do with how the implementation would look like.

type Weather interface {
Temperature(location string) (temp int, err error)
}

The Mock Object

Then I focus that testing the existing code interacts with the interface as I would expect. I extend the unit tests of the code to ensure that it interacts properly with the interface. At this point I am forced to create a Mock object for the interface and also wire the interface in the existing code (using dependency injection), in order to get the tests green again.

Ok, at this point the unit tests tell us that the existing code is calling the interface as expected and it reacts appropriately in the return value. In terms of new code, we only have the interface and the Mock object.

import (
"testing"
"github.com/stretchr/testify/mock"
)
type Mock struct{
mock.Mock
}

func (m *MockWeather) Temperature(location string) (int, error) {
args := m.Called(location)
return args.String(0), args.Error(1)
}

When do we push? Soon but not just yet!

You see the mock object is good for testing but we need an implementation of the interface to use with our application.

The Null Object

I use the Null object pattern to get to my code in a releasable state ASAP. I create the simplest implementation ever my interface. It usually is just one line where I return a hard coded response. Remember, the intention of this step is not to provide new functionality, it is to ensure that our application remain releasable and free of regressions.

The Null Object pattern is great for this job:

  • It implements the interface, thus I can build the application.
  • It models the state of “the feature is not there”, which is correct for my application at the moment.
  • It is so simple it gives me confidence that it will not introduce any nasty surprises in my code.
type NoWeather struct{}

func (w *NoWeather) Temperature(location string) (int, error) {
return 0, errors.New("not implemented yet!")
}

Alright, at this point my existing code uses the interface of my new functionality. There is a mock implementation for the unit tests and an Null object implementation that does nothing. Not much code right? That’s good, it gives me confidence to push and deploy.

The Implementation

At this point, I can start developing my actual implementation. TDD first right? At any point in time I can just push and deploy, since this implementation is not used yet.

type Weather struct{}

func (w *Weather) Temperature(location string) (int, error) {
// real implementation
}

Unit tests are important but as the implementation progresses I want to see my implementation in action!

The Feature Flag

I use an environment variable to decide between using the Null Object or the actual implementation in my code. At first, the flag is set only on my development machine. This allows me test locally my new object, and still be able to push and deploy.

As my confidence grows, I set the flag in the staging environment and later in production.

If something goes wrong and my new implementation causes some unexpected problems in production, I still have the option to unset the feature flag and disable the feature.

var weather Weather
_, weatherFlagExists := os.LookupEnv("ENABLE_WEATHER")
if weatherFlagExists {
weather = &Weather{}
} else {
weather = &NoWeather{}
}

Conclusion

Starting with a Null Object before the real implementation has a few advantages:

  • It allows you to quickly go in a state that you can push your code again with confidence.
  • Combined with a feature flag, I can control when to enable the feature in each environment.
  • It can work as a fallback, if at any point you want to turn the feature off I can just switch to the Null Object implementation.

--

--