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.