Imagine you are building a CMS (content-management system) for publishing articles. And in that CMS, you want to build your own analytics tool for the published posts—to see how many page views an article got, for example.
You want the authors using your CMS to be able to see how an article is doing through the analytics page for that article.
Let's say you are storing these analytics in their own table in the database. So to get the analytics data for a post, you have to fetch these analytics separately based on the post id.
So you would write something like this:
function fetchAnalyticsForPostId(postId) {
// code to fetch the analytics data for postId
return new PostAnalytics(analyticsData)
}
function displayAnalytics(analyticsObject) {
console.log(`
Page views: ${analyticsObject.pageViews},
Users: ${analyticsObject.users},
New Users: ${analyticsObject.newUsers}
`)
}
const analyticsObject = fetchAnalyticsForPostId(5)
displayAnalytics(analyticsObject)
This code would work as along as the fetched analytics exists in the database. If it doesn't (draft articles, for example), then I have to check for null everywhere it's used.
function displayAnalytics(analyticsObject) {
console.log(`
Page views: ${analyticsObject === null ? 0 : analyticsObject.pageViews},
Users: ${analyticsObject === null ? 0 : analyticsObject.users},
New Users: ${analyticsObject === null ? 0 : analyticsObject.newUsers}
`)
}
It's not a big deal if I'm doing these null checks only in this function. But in real-world cases, I will likely do these checks many times throughout my code base.
It would be much cleaner if I can just use the analyticsObject
directly without checking for null each time I'm using it. Actually I can do this using the Null Object Pattern.
The idea of the Null Object Pattern is simple: it's a special version of an object that knows how to handle null cases.
In this example, it means I just need to define a new class with the same interface as PostAnalytics
and return zeros from each getter—assuming I have three getters in it: pageViews
, users
, and newUsers
.
Let's say that the PostAnalytics
class looks like this:
class PostAnalytics {
#analyticsData
constructor(analyticsData) {
this.#analyticsData = analyticsData
}
get pageViews() {
return this.#analyticsData.pageViews
}
get users() {
return this.#analyticsData.users
}
get newUsers() {
return this.#analyticsData.newUsers
}
}
To create a null version for it, I would create this new class:
class NullPostAnalytics {
get pageViews() {
return 0
}
get users() {
return 0
}
get newUsers() {
return 0
}
}
So its interface (in other words, its public methods) should be exactly the same as the real one, PostAnalytics
, but it should return empty values instead of real values. In this example, I'm returning 0
because they are numbers and that's what I want for null analytics to be. But if they are strings, I might return empty strings or whatever the logic should be.
Now the last step is to return this object instead of the real one in the creation step.
function fetchAnalyticsForPostId(postId) {
// code to fetch the analytics data for postId
if (!analyticsData) {
return new NullPostAnalytics()
}
return new PostAnalytics(analyticsData)
}
Notice how I have the creation logic of the analytics object in that function. In real-world apps, I would move that logic into a factory function for the post analytics object.
Now after this change, I can remove all the null checks for the analyticsObject from my code.
Top comments (6)
Nice. I've seen this pattern before, but haven't really used it. Would you say there are any advantages of using the Null Object Pattern over code like the following?
Thanks, Anthony!
If it was only doing what you're showing here, I would say there's no main difference. However, in most cases a class would also contain methods to do something. With the null object pattern, you can easily handle each one in the null case without introducing any conditionals.
Another reason I prefer the Null class over the above implementation is that I can clearly see what all the code in the class is for (no need to see any checks like
if (!analyticsData)
), and in this case it's for the null case—which would most likely contain more logic and code as I add more features to it. In other words, your code will be more cohesive because all null related code are put in a single, dedicated place.Right. So by having a null-object, you end up with two implementations:
In that way, there is no need for guard statements or conditionals when implementing them; the conditional occurs when choosing which implementation to return.
Exactly!
This is nice.
Thanks!