I'm retroactively reviewing MartinFowler's seminal RefactoringImprovingTheDesignOfExistingCode here.
The segment on NullObjects misses a DeprecationRefactor. One could test (hence integrate and ship) more often than the Mechanics permit.
The first step of the refactor: Derive from your non-Null class a new class with Null in its name, and no difference from the parent class except a new method - IsNull(). Make this return true. Underride it in the parent class, and make that one return false.
Now take one of the methods that formerly could return null, and make it return the null object in this situation instead.
Martin says, "There's no doubt this is the trickiest part of the refactoring. For each source of a null I replace, I have to find all the times it is tested for nullness and replace them. If the object is widely passed around, these can be hard to track." That's where he misses the deprecation pass.
Here's the original code:
Customer customer = site.getCustomer(). BillingPlan plan; if (customer == null) plan = BillingPlan.basic(); else plan = customer.getPlan(); ... String CustomerName; if ( customer == null) customerName = "occupant"; else customerName = customer.getName(); ... int weeksDelinquent; if (customer == null) weeksDelinquent = 0; else weeksDelinquent = customer.getHistory().getWeeksDelinquentInLastYear();So here's Martin's attempt, after all that "find each time it is tested".
Customer customer = site.getCustomer(); BillingPlan plan; if (customer.IsNull()) plan = BillingPlan.basic(); else plan = customer.getPlan(); ... String CustomerName; if (customer.IsNull()) customerName = "occupant"; else customerName = customer.getName(); ... int weeksDelinquent; if (customer.IsNull()) weeksDelinquent = 0; else weeksDelinquent = customer.getHistory().getWeeksDelinquentInLastYear();This adds risk, and it breaks the rule "1 to 20 edits between tests". It's an arbitrary, open-ended number of edits, with no way to check intermediate edits work.
Here's my version, half-way thru the DeprecationRefactor:
Customer nullableCustomer = site.getCustomer(); Customer customer = nullableCustomer; if (nullableCustomer.IsNull()) customer = null; BillingPlan plan; if (nullableCustomer.IsNull()) plan = BillingPlan.basic(); else plan = nullableCustomer.getPlan(); ... String CustomerName; if (nullableCustomer.IsNull()) customerName = "occupant"; else customerName = nullableCustomer.getName(); ... int weeksDelinquent; if (customer == null) weeksDelinquent = 0; else weeksDelinquent = customer.getHistory().getWeeksDelinquentInLastYear();I pretended I did not yet locate the last test for nullity. It could have been in some far-flung function, after customer got passed around.
DeprecationRefactor, at this point, is more stable & interruptible because one tests more often. Recall that, under ExtremeProgramming, one can integrate whenever tests pass, and one can ship any integration.
My next step, after I think I found every test for null, is to fold this duplication...
Customer nullableCustomer = site.getCustomer(); Customer customer = nullableCustomer; if (nullableCustomer.IsNull()) customer = null;...back into this:
Customer customer = site.getCustomer();...then simply fix all the missing references to nullableCustomer.
Eventually, if I missed a null, (and maybe with harsher environments such as CeePlusPlus), I could get the program to reliably explode. That's a good technique for locating the next place to perform some incrementing refactor. ;-)
The refactor continues to merge the methods on the nullable class, so this...
String CustomerName; if (customer.IsNull()) customerName = "occupant"; else customerName = customer.getName();...becomes this...
String CustomerName; customerName = customer.getName();...where the null object has a trivial implementation of getName() that returns "occupant".
-- PhlIp