Code is littered with `if (x !== null) x.do()` checks around the same collaborator. Each new caller adds another check; missing one ships a NullPointerException to production; the absence-handling logic duplicates across callers.
Null-check branches the agent must trace at every collaborator-using call site. The agent cannot statically verify all check sites stay consistent; new callers risk omitting the check and shipping NullPointerExceptions.
A Null Object implementing the same interface stands in for the missing collaborator. Callers invoke methods unconditionally; the Null Object returns sensible defaults (return the input unchanged, return zero, do nothing).
One Null Object class the agent verifies once; all call sites unconditionally invoke the collaborator interface. The agent's edit budget on a collaborator-using method drops because there's no per-call branch to reason about.
Before the refactoring
// Every consumer of Customer.discount and Customer.loyaltyProgram must check for null first.class Order {constructor(customer) {this.customer = customer;}finalPrice() {let price = this.subtotal();if (this.customer.discount !== null) {price = this.customer.discount.applyTo(price);}if (this.customer.loyaltyProgram !== null) {price -= this.customer.loyaltyProgram.pointsValue();}return price;}subtotal() { /* ... */ }}
After the refactoring
// Null Objects implement the same interface with safe defaults; callers stop branching.class NoDiscount {applyTo(price) {return price;}}class NoLoyaltyProgram {pointsValue() {return 0;}}class Order {constructor(customer) {// customer.discount and customer.loyaltyProgram are never null;// they are NoDiscount/NoLoyaltyProgram when absent.this.customer = customer;}finalPrice() {let price = this.subtotal();price = this.customer.discount.applyTo(price);price -= this.customer.loyaltyProgram.pointsValue();return price;}subtotal() { /* ... */ }}
Null checks duplicate at every call site; the rules for what 'absent' means duplicate too. Adding a new caller is one more place to remember the check; forgetting one is a runtime bug.
N call sites × M methods × the null check = the agent's per-edit verification cost. Static analysis catches some missing checks but not all (especially after refactors that change which fields can be null).
Null Objects hide absence; downstream code may mistake the silent no-op for a real operation, especially when the Null Object's behaviour drifts from the originally-intended default. Debugging 'why did nothing happen?' requires recognizing that a Null Object is in hand.
A Null Object collapses two distinct runtime states (absent / present) into one type at the call site. The agent loses the ability to statically distinguish a real collaborator from its null stand-in; debugging requires reading the constructor / factory to know which is which.
Call sites simplify to plain method calls; absence-handling lives in one place (the Null Object); the rules for what 'no discount' or 'no loyalty program' means are visible and testable as a single class.
Branch count at call sites drops to zero; the agent reads the Null Object's body once to know what 'absent' does, and trusts the type system thereafter. Adding a new caller is one method call, not one method-call-plus-branch.
A Null Object with subtly-wrong defaults silently corrupts results without throwing. The pattern pays when 'absent' has a well-defined neutral semantics; for collaborators where absence should actually halt the operation, Null Object hides the error the caller expected to see.
A Null Object that quietly returns the wrong neutral element (zero where one was needed, identity where blocking was needed) produces silent failures the agent cannot detect from static reading. The pattern requires careful semantic alignment between 'absent' and the chosen default.