Refactoring conditions-Introduce Null Object introduces Null Object VII

Refactoring condition-Introduce Null Object introduces Null object seven

1. Introduce Null object

1.1. Usage scenarios

You need to double-check whether an object is null. Replace null values with null objects.

The most fundamental advantage of polymorphism is that you don’t have to ask the object “what type are you” and then call a certain behavior of the object according to the answer you get-you just call the behavior, and all other polymorphic mechanisms will do it for you well arranged. Another less intuitive (and lesser known) use of polymorphism is when a field is null. Let’s start with Ron Jeffries’ story. “

Excerpt from: [US] Martin Fowler. “Refactoring: Improving the Design of Existing Code.” Apple Books.

1.2. How to do it

  • Creates a subclass of the source class that behaves like a null version of the source class. Add the isNull() function to both the source class and the null subclass. The former’s isNull() should return false, and the latter’s isNull() should return true.
  • The following method may also help you: create a nullable interface, put the isNull() function in it, and let the source class implement this interface.
  • In addition, you can also create a test interface, which is specially used to check whether the object is null.
  • compile.
  • Find all the places where you “asked for the source object and got a null”. Modify these places so that they get an empty object instead.
  • Find all the “compare the source object to null” places. Modify these places so that they call the isNull() function.
  • You can process only one source object and its client program at a time, compile and test, and then process another source object.
  • You can put some assertions in the place where “null should not appear again” to ensure that null does not appear again. This might help you.
  • Compile, test.
  • Find out such a program point: if the object is not null, do A action, otherwise do B action.
  • For each of the above locations, override the A action in a null class to behave the same as the B action.
  • Use the overridden action above, and then remove the “is object equal to null” conditional test. Compile and test.

1.3. Example

A utility company’s system expresses a site (place) in terms of Site. Both houses and apartments use the company’s services. At any time, each location has (or corresponds to) a customer, and customer information is represented by Customer:

 class Site...
   Customer getCustomer() {<!-- -->
       return _customer;
   }
   Customer _customer;

Customer has many features, we only look at three of them

 class Customer...
   public String getName() {<!-- -->...}
   public BillingPlan getPlan() {<!-- -->...}
   public PaymentHistory getHistory() {<!-- -->...}

The system also uses PaymentHistory to represent the customer’s payment record, which also has its own characteristics

 public class PaymentHistory...
   int getWeeksDelinquentInLastYear()

The various accessor functions above allow clients to obtain various data. But sometimes the customers at a location move away, and the new customers have not yet moved in. At this time, there are no customers at this location. Since this situation is possible, we must ensure that all users of Customer can handle the “Customer object equals null” situation. Here are some sample snippets

 Customer customer = site. getCustomer();
         Billing Plan plan;
         // Check if it is an empty object
         if (customer == null) plan = BillingPlan. basic();
         else plan = customer. getPlan();
...
         String customerName;
         // Determine whether it is an empty object, repeat the code 1 time
         if (customer == null) customerName = "occupant";
         else customerName = customer. getName();
...
         int weeks Delinquent;
         // Determine whether it is an empty object, repeat the code 2 times
         if (customer == null) weeksDelinquent = 0;
         else weeksDelinquent = customer.getHistory().getWeeksDelinquentInLastYear();

There may be many places where Site and Customer objects are used in this system, and they all must check whether the Customer object is equal to null, and such checks are completely repeated.
Looks like it’s time to use a null object.
First create a NullCustomer, and modify the Customer to support the check of “whether the object is null”:

// Create an empty object class, inheriting the Customer class
 class NullCustomer extends Customer {<!-- -->
 // Create a method that handles null objects
   public boolean isNull() {<!-- -->
       return true;
   }
 }
 class Customer...
   public boolean isNull() {<!-- -->
       return false;
   }
   protected Customer() {<!-- -->} //needed by the NullCustomer

If you can’t modify Customer, you can use the method on page 266: Create a new test interface.
If you like, you can also create a new interface and tell everyone that “an empty object is used here”:

 interface Nullable {<!-- -->
   boolean isNull();
 }
 class Customer implements Nullable

I also like to include a factory function for creating NullCtomer objects. This way, the user doesn’t have to know about the existence of the empty object:

 class Customer...
   static Customer newNull() {<!-- -->
       return new NullCustomer();
   }

The next part is a little trickier. For all “return null” places, I have to change it to “return null object”, and I also need to replace foo==null checks with foo.isNull(). I’ve found it useful to find all the places that “ask for a Customer object” and modify them all so that they don’t return null and return a NullCustomer object instead.

 class Site...
   Customer getCustomer() {<!-- -->
       return (_customer == null) ?
           Customer. newNull():
           _customer;
   }

In addition, I have to modify all the “use Customer objects” to check them with the isNull() function instead of using the “== null” check.

 Customer customer = site. getCustomer();
       Billing Plan plan;
       // replace with null object
       if (customer. isNull()) plan = BillingPlan. basic();
       else plan = customer. getPlan();
 ...
       String customerName;
       if (customer.isNull()) customerName = "occupant";
       else customerName = customer. getName();
 ...
       int weeks Delinquent;
       if (customer.isNull()) weeksDelinquent = 0;
       else weeksDelinquent = customer.getHistory().getWeeksDelinquentInLastYear();

Without a doubt, this is the trickiest part of this refactoring. For every object that may be equal to null that needs to be replaced, I have to find all the places that check if it is equal to null and replace them one by one. If the object is spread to many places, it will be difficult to track. In the above example, I have to find out every variable of type Customer and where they are used. It is difficult to break this process into smaller steps. Sometimes I find that the object that may be equal to null is only used in a few places, so the replacement work is relatively simple; but most of the time I have to do a lot of replacement work. Fortunately, undoing these substitutions is not difficult, because I can find out the call to isNull() without too much difficulty, but it is messy and annoying after all.

After this step is complete, if the compilation and testing pass without a hitch, I can smile in relief. The next move is more interesting. Using the isNull() function has not yielded any benefit so far. Only after moving the relevant behavior into NullCustomer and removing the conditional expression did I get tangible benefits. I can move the various behaviors over one by one. Let’s start with the “get customer name” function. The client code at this point is roughly as follows:

 String customerName;
 if (customer.isNull()) customerName = "occupant";
 else customerName = customer. getName();

First add an appropriate function to NullCustomer, and use this function to get the customer name:

 class NullCustomer...
   public String getName(){<!-- -->
       return "occupant";
   }

Now, I can get rid of the conditional code:

String customerName = customer. getName();

Then I do the same with the other functions to make them respond appropriately to the corresponding queries. In addition, I can also do proper processing of “modifiers”. So the following client program:

 if (! customer. isNull())
     customer.setPlan(BillingPlan.special());

It becomes like this:

 customer.setPlan(BillingPlan.special());
 class NullCustomer...
   public void setPlan (BillingPlan arg) {<!-- -->}

Remember: such a behavioral move only makes sense if most of the client code expects the same response from an empty object. Note that I said “most” not “all”. Any user who needs a null object to respond differently can still use the isNull() function to test. As long as most clients expect the same response from a null object, they can invoke the default null behavior, and you get the benefit.
A slight difference from the above example is that some clients use the result of the Customer function:

if (customer.isNull()) weeksDelinquent = 0;
else weeksDelinquent = customer.getHistory().getWeeksDelinquentInLastYear();

I can create a new NullPaymentHistory class to handle this situation:

 class NullCustomer...
   public PaymentHistory getHistory() {<!-- -->
       return PaymentHistory. newNull();
   }

Then, I can also remove this line of conditional code:

int weeksDelinquent = customer.getHistory().getWeeksDelinquentInLastYear();

You can often see situations where empty objects return other empty objects.

Another way to test the interface

In addition to defining isNull(), you can also create an interface to check whether the object is null.
Using this method, you need to create a new Null interface, which does not define any functions:

interface Null {<!-- -->}

Then, let the null object implement the Null interface:

 class NullCustomer extends Customer implements Null... ?

Normally I try to avoid using the instanceof operator, but in this case it’s fine to use it. And this approach has another advantage: there is no need to modify the Customer. In this way, even if the Customer source code cannot be modified, I can use the empty object.

other special circumstances
When using this refactoring, you can have several different kinds of empty objects, for example you can say “no customers” (new houses and temporarily unoccupied houses) and “unknown customers” (occupants, but we don’t know who) the two situations are different. If so, you can create different empty object classes for different situations. Sometimes empty objects can also carry data, such as the usage records of unknown customers, so we can send the bill to him after finding out the customer’s name.

Essentially, this is a bigger pattern than the Null Object pattern: the Special Case pattern. The so-called special case (special case), that is, a special case of a class, has a special behavior. Therefore, UnknownCustomer, which means “unknown customer”, and NoCustomer, which means “no customer”, are special cases of Customer. You can often see such “special case classes” in classes that represent quantities. For example, Java floating point numbers have special cases such as “positive infinity”, “negative infinity” and “not a number” (NaN). The value of special case classes is: they can reduce your “error handling” overhead, eg floating point operations will never throw exceptions. If you do floating point arithmetic on NaN, the result will be NaN too. This is the same as “the access function of an empty object usually returns another empty object”.