π What is the Null Object Pattern?
The Null Object Pattern is a behavioral design pattern where:
- Instead of returning
null
(which can cause errors if used without checking), - You return a special object that does nothing but still behaves like a real object.
β
This avoids if (obj != null)
checks everywhere in your code
β
It helps follow Open/Closed Principle (extend behavior without changing logic)
π¨ Real-World Examples (Simple to Visualize)
π€ 1. Unknown User (Guest)
-
Instead of: Returning
null
for unauthenticated users -
Do this: Return a
GuestUser
object that has safe defaults (no login, read-only)
π§ 2. Empty Cart
-
Instead of:
if (cart) { cart.checkout() }
-
Do this: Use
EmptyCart
object with.checkout()
method that logs "No items"
ποΈ 3. Logger
- A
ConsoleLogger
logs to the console - A
NullLogger
just does nothing (used in production or testing)
π§ Why Use It?
β
Avoid runtime errors from null
β
Reduce code clutter from if (obj !== null)
β
Improve readability & testability
β
Follow Polymorphism instead of conditionals
π§± TypeScript Example β Logger Pattern
Let's build a real use case: a logger.
1. Define Logger Interface
interface Logger {
log(message: string): void;
}
2. Real Logger (Console)
class ConsoleLogger implements Logger {
log(message: string): void {
console.log(`[LOG]: ${message}`);
}
}
3. Null Logger (does nothing)
class NullLogger implements Logger {
log(message: string): void {
// do nothing
}
}
4. Service Using Logger
class UserService {
constructor(private logger: Logger) {}
createUser(name: string) {
// ... create logic
this.logger.log(`User created: ${name}`);
}
}
5. Use It Without Null Check
const consoleLogger = new ConsoleLogger();
const nullLogger = new NullLogger();
const service1 = new UserService(consoleLogger);
service1.createUser("Alice"); // logs: User created: Alice
const service2 = new UserService(nullLogger);
service2.createUser("Bob"); // logs nothing, no error
β
No need to check if (logger != null)
β just inject the correct behavior.
π‘ Better Than Null Check
Instead of:
if (logger) {
logger.log("something");
}
With Null Object:
logger.log("something"); // always safe!
π Real-World Use Cases
Context | Null Object |
---|---|
Logging |
NullLogger (test env, prod) |
Payment |
NullPaymentProcessor (free plan) |
User |
GuestUser (not signed in) |
Cart |
EmptyCart (user hasn't added anything) |
Strategy |
NoOpStrategy for default behavior |
π― Pro Tips for Mid-to-Senior Devs
β
Replace null
/undefined
returns with Null Object classes
β
Let Null Object implement the same interface
β
Makes code cleaner, testable, and follows polymorphic behavior
π§ͺ Testability Bonus
In unit tests, instead of mocking a logger:
const service = new UserService(new NullLogger());
No mocks needed β just use the Null Object!
π Final Summary
"The Null Object Pattern replaces null with an object that safely does nothing β reducing checks and avoiding errors."
Perfect! Letβs now apply the Null Object Pattern in a NestJS service layer β a common place where it adds a lot of clarity and safety. Iβll walk you through a practical example slowly and clearly.
β Use Case: Optional Logger in a NestJS Service
Letβs say you have a UserService
that logs events (like user creation), but sometimes you donβt want logging (e.g., during tests or in certain environments). You donβt want to check if (logger)
everywhere.
π§± Step-by-Step: Null Object Pattern in NestJS
πΉ 1. Create a Logger Interface
// logger/logger.interface.ts
export interface LoggerService {
log(message: string): void;
}
πΉ 2. Real Logger Implementation
// logger/console-logger.service.ts
import { Injectable } from '@nestjs/common';
import { LoggerService } from './logger.interface';
@Injectable()
export class ConsoleLoggerService implements LoggerService {
log(message: string): void {
console.log(`[LOG] ${message}`);
}
}
πΉ 3. Null Logger (does nothing)
// logger/null-logger.service.ts
import { Injectable } from '@nestjs/common';
import { LoggerService } from './logger.interface';
@Injectable()
export class NullLoggerService implements LoggerService {
log(message: string): void {
// do nothing
}
}
πΉ 4. Inject Logger into Your Service
// user/user.service.ts
import { Injectable } from '@nestjs/common';
import { LoggerService } from '../logger/logger.interface';
@Injectable()
export class UserService {
constructor(private readonly logger: LoggerService) {}
createUser(username: string) {
// ... your real user creation logic
this.logger.log(`User created: ${username}`);
}
}
πΉ 5. Provide Either Logger in Your Module
// app.module.ts
import { Module } from '@nestjs/common';
import { UserService } from './user/user.service';
import { ConsoleLoggerService } from './logger/console-logger.service';
import { NullLoggerService } from './logger/null-logger.service';
import { LoggerService } from './logger/logger.interface';
@Module({
providers: [
UserService,
{
provide: LoggerService,
useClass:
process.env.NODE_ENV === 'test' ? NullLoggerService : ConsoleLoggerService,
},
],
})
export class AppModule {}
β Now:
- In test env, it uses
NullLoggerService
(no logs, no noise) - In prod/dev, it uses
ConsoleLoggerService
(full logs)
π― Why This Works Well
Benefit | How It Helps |
---|---|
β
Avoid null checks |
No if (logger) needed anywhere |
β Clean DI | Swap behaviors easily at runtime |
β Safe by default |
NullLogger never throws |
β Open/Closed Principle | Add new loggers without changing service logic |
β Testing-friendly | Inject NullLoggerService in tests, no mocking needed |
π§ͺ Test Usage (Easy)
const module = await Test.createTestingModule({
providers: [
UserService,
{ provide: LoggerService, useClass: NullLoggerService },
],
}).compile();
const service = module.get(UserService);
service.createUser('Alice'); // runs silently
π§ You Can Extend This Pattern To:
-
AnalyticsService β
NullAnalyticsService
-
EmailService β
NullEmailService
-
CacheService β
NullCacheService
for dev mode -
NotificationService β
NullNotificationService
π§΅ Final Summary
In NestJS, use the Null Object Pattern by creating default "no-op" services that follow the same interface as real ones β this simplifies logic, removes conditionals, and improves testing and runtime flexibility.
Top comments (0)