install
source · Clone the upstream repo
git clone https://github.com/Intense-Visions/harness-engineering
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/Intense-Visions/harness-engineering "$T" && mkdir -p ~/.claude/skills && cp -r "$T/agents/skills/codex/gof-null-object" ~/.claude/skills/intense-visions-harness-engineering-gof-null-object-8915c3 && rm -rf "$T"
manifest:
agents/skills/codex/gof-null-object/SKILL.mdsource content
GOF Null Object
Eliminate null checks by providing default no-op implementations of interfaces.
When to Use
- You have many
guards before calling methods on an optional dependencyif (thing !== null) - You want a "do nothing" default when an optional feature is absent (e.g., optional logger, optional cache, optional analytics)
- You're wiring up optional dependencies that are configured at startup
- You want tests to work without providing real implementations of optional dependencies
Instructions
Basic null object for an optional logger:
interface Logger { info(message: string, context?: object): void; warn(message: string, context?: object): void; error(message: string, error?: Error): void; } // Real implementation class ConsoleLogger implements Logger { info(message: string, context?: object): void { console.log('[INFO]', message, context ?? ''); } warn(message: string, context?: object): void { console.warn('[WARN]', message, context ?? ''); } error(message: string, error?: Error): void { console.error('[ERROR]', message, error); } } // Null object — safe no-ops class NullLogger implements Logger { info(_message: string, _context?: object): void {} warn(_message: string, _context?: object): void {} error(_message: string, _error?: Error): void {} } // Consumer — never needs to check if logger is null class OrderService { constructor( private readonly repo: OrderRepository, private readonly logger: Logger = new NullLogger() // optional, defaults to null object ) {} async createOrder(data: CreateOrderInput): Promise<Order> { this.logger.info('Creating order', { userId: data.userId }); const order = await this.repo.create(data); this.logger.info('Order created', { orderId: order.id }); return order; } } // Production const service = new OrderService(repo, new ConsoleLogger()); // Test / minimal setup — no logging noise const testService = new OrderService(repo); // uses NullLogger by default
Null object for an optional cache:
interface Cache { get<T>(key: string): Promise<T | null>; set<T>(key: string, value: T, ttlMs?: number): Promise<void>; delete(key: string): Promise<void>; } class RedisCache implements Cache { constructor(private readonly client: RedisClient) {} async get<T>(key: string): Promise<T | null> { const val = await this.client.get(key); return val ? JSON.parse(val) : null; } async set<T>(key: string, value: T, ttlMs = 60_000): Promise<void> { await this.client.set(key, JSON.stringify(value), 'PX', ttlMs); } async delete(key: string): Promise<void> { await this.client.del(key); } } // Null object — always miss, never store class NullCache implements Cache { async get<T>(_key: string): Promise<T | null> { return null; } // always cache miss async set<T>(_key: string, _value: T, _ttlMs?: number): Promise<void> {} // no-op async delete(_key: string): Promise<void> {} // no-op } // Service uses cache without null checks class UserService { constructor( private readonly db: UserRepository, private readonly cache: Cache = new NullCache() ) {} async findUser(id: string): Promise<User | null> { const cached = await this.cache.get<User>(`user:${id}`); if (cached) return cached; const user = await this.db.findById(id); if (user) await this.cache.set(`user:${id}`, user, 300_000); return user; } }
Typed null object factory:
// Create a null object automatically from an interface (advanced) function createNullObject<T extends object>(methods: (keyof T)[]): T { const obj = {} as T; for (const method of methods) { (obj as Record<string | symbol, unknown>)[method as string] = () => {}; } return obj; }
Details
Null Object vs. Optional chaining: Optional chaining (
obj?.method()) is fine for occasional null checks. Null Object is better when a dependency is consistently optional across many methods — it removes ALL null checks at once, not one at a time.
Null Object vs. TypeScript optional types:
Logger | undefined as a parameter type forces callers to guard. Logger (defaulting to NullLogger) is simpler — callers provide a real logger or nothing; the service always has a logger.
Null Object is not the same as
: A undefined
NullLogger is a real object that does nothing. undefined throws when you call methods on it. The Null Object makes the absence of a feature safe and explicit.
Anti-patterns:
- Null object that returns null/undefined from methods — the calling code then needs null checks again; return safe defaults (empty arrays, zero, empty string)
- Null object that tracks calls for assertions in tests — use a mock/spy instead; null objects should be passive
- Using null objects to mask missing required dependencies — null objects should be for genuinely optional features, not required ones that haven't been wired yet
For analytics / telemetry (common use case):
interface Analytics { track(event: string, properties?: object): void; identify(userId: string, traits?: object): void; } class NullAnalytics implements Analytics { track(_event: string, _properties?: object): void {} identify(_userId: string, _traits?: object): void {} }
Source
refactoring.guru/introduce-null-object
Process
- Read the instructions and examples in this document.
- Apply the patterns to your implementation, adapting to your specific context.
- Verify your implementation against the details and edge cases listed above.
Harness Integration
- Type: knowledge — this skill is a reference document, not a procedural workflow.
- No tools or state — consumed as context by other skills and agents.
Success Criteria
- The patterns described in this document are applied correctly in the implementation.
- Edge cases and anti-patterns listed in this document are avoided.