Null Object Pattern

2019-03-10 23:58发布

问题:

There seems to be a growing community of people saying that you should never return null and should always use the Null Object Pattern instead. I can see the usefullness of the NOP when using a collection/map/array or calling boolean functions such as isAuthenticated(), which is shown here.

I haven't found anything on this that is fully convincing. Bear with me here as I try to organize my thoughts.

My understanding is that instead of returning a null object, you return a valid object that has been "zeroed" out.

So for example, the client would make a call to get an object:

Car car = getCar();

If not using the NOP you would need to check if the object returned from getCar() is null before calling any methods on it:

if (car != null){
    color = car.getColor();
    doScreenStuff(color);
   }

Using the NOP, instead of getCar() returning null, it now returns an Object that has been effectively "zeroed out". So now we no longer need to do if (car != null) and can just request the color. So, I suppose that our "zeroed out" object would return "none" when we call color.

How does this help? It seems that moving forward and calling methods on an empty object causes just as much pain as just checking null. Now, when it comes time to display the information, we need to check that the color isn't "none", that the height isn't 0, or whatever other values you have. So essentially, instead of checking in the beginning of processing if the car is null, you check afterwards if the car object we have is a real car or a substitute. I.E. we don't want to display a bunch of empty objects, so we need some way to filter out all of our empty objects.

This filtering is an added step just like calling if (car != null). The only difference is that with checking null, we can stop processing as soon as we discover that the car object is null by throwing an exception, whereas with NOP we call methods on the empty object and keep chugging along until it gets to be time to display the object and at this point we filter out the empties. Furthermore, you need to know the values returned by your empty object. I.E. does getColor() return "none" or "empty".

There obviously must be something I'm overlooking. Thanks in advance.

回答1:

MattPutnam's answer is right on point, and I second it. I'd add this: the concept of "null object," when you analyze it, seems to boil down to the mathematical concept of a monoid. You can think of it this way: a monoid is a type that has both of these things:

  1. An "append," "sum" or similar operation, which needs to be associative: a.op(b).op(c) is the same as a.op(b.op(c)).
  2. An "empty," "zero" or "null" value, that acts as the neutral element or identity element of the operation.

The classic example of the null object pattern is to return an empty list or array instead of null. Well, lists are a monoid, with append as the operation and the empty list as the neutral element.

Now, the problem that you face in your Car example is that Car isn't really a monoid; there is no notion of "the empty car" or "the neutral car", and there isn't really a sensible operation that you could use to combine two Cars into one.

So the recommendation you're rightly getting is to use something like the Java 8 Optional. And the trick is that no matter what type T is, Optional<T> is a monoid:

  1. The monoid's "combine" operation is "pick the first value if it's not empty, otherwise pick the second value":
    • x || empty = x
    • empty || x = x
  2. The neutral element is Optional.empty(), because Optional.empty().orElse(anything) is the same as just anything.

So basically, Optional<T> is a wrapper that adds a null object to types like Car that don't have one. The Optional<T>.orElse(T value) method that is a slightly refactored version of the "pick first non-empty value" monoid.



回答2:

The null object pattern only makes sense when there's a reasonable, functional value for the null object to be. The purpose isn't to defer null, as you've described, but to completely eliminate the idea of null by representing the nothingness or emptiness with an actual piece of data that is still functional. For example, the natural case of holes in a tree structure, as described in the Wikipedia article.

A null car doesn't make sense. In this case, it seems like the more appropriate thing would be for getCar() to return Optional<Car>.



回答3:

If you do not see the point, then it is probably not a good paradigm to use for you. The whole idea of OO programming is to make things simpler for YOU. Don't get trapped into thinking you need to adopt someone else elaborate pattern-based system. Often it takes a significant amount of work to learn various patterns and use them effectively, so it is better to grow into them, rather than try to force yourself to use it.

As far as this particular pattern is concerned, it assumes a certain style of programming which may be inappropriate for you. I would never use it myself because I return nulls as legitimate values (missing data) which are handled differently in each case, so "centralized handling" makes no sense for me. When I return booleans, I use primitives.

The bottom line here is that if a pattern seems unnatural to you, don't use it.



回答4:

The Null Pointer Object, at least in my experience, is so you limit you null checks to one central location to avoid null pointer exceptions.

If a lot of services are using CarFactory, it's much easier to have Carfactory handle the null result, then have each individual service handle it. Plus it ensures each null result is handled the same fashion, whether that be doing nothing or some specified logic. The downside is if it isn't handled correctly, it could lead to some temporarily confusing bugs (especially since null pointer exceptions scream loud and proud).

I don't really use it much anymore. There are alternatives to using null checks, such as using the Java 8 Optional. There are people who are for and against this as well, and this is by no means a replacement for the null object pattern.

String result = Optional.ofNullable(someInteger)
   .map(Integer::valueOf)
   .map(i -> i + 1)
   .map(Object::toString)
   .orElse("number not present");

System.out.println(result);


回答5:

NULL design pattern fills an ABSENCE of an object with a DEFAULT behavior and should be used only when one object collaborates with other.

NULL design pattern is not meant to replace NULL exception handling. It’s one of the side benefits of NULL design pattern but the intention is to provide a default behavior.

For example consider the below sample pseduo code. Its a simple Customer class which is delegating discount calculation to a discount object.

class Customer {
    IDiscount dis = new NormalDiscount();
    public double CalculateDiscount() {
        return dis.Calculate();
    }
    // Code removed for simplicity
}

Now let us say we want to create customers who have defaulted for payments. So we inherit from the above class as lot of properties and behaviors are same but we do not want discount calculation because a defaulted customer is not eligible for discounts. So we make the discount object null but that is a problem because now the discount calculation will crash because of NULL discount object.

class DefaultedCustomer : Customer {
    IDiscount dis = null;
    public double CalculateDiscount() {
        return dis.Calculate(); <---- This will crash in parent
    }
    // Code removed for simplicity
}

So one of the ways developers fix this is my checking for NULLs and returning zero. If you close your eyes and think logically this is actually business scenario where there is no discounts for defaulted customer.

So rather than fixing this technically by checking NULL we can create a DEFAULT DISCOUNT BEHAVIOR class as shown below which returns zero discount and use the same for defaulted customer. This is more clean than checking NULLs.

public class DefaultDiscount : IDiscount {
    public void Calculate() {
        return 0;
    }
}

NULLs are needed but for exception handling not for fixing absence of a object in collaboration as shown above. NULL design pattern does not make sense if there is no collaboration.

Because of NULL design pattern NULL checks get avoided but that's more of after effect and side benefit but not the main intention.

All the above thoughts i have taken from this NULL Design pattern in C# which explains Do's and Donts of NULL.



回答6:

Returning Optional over Car will not take away the step of checking if car object is actually present or not.

When returning car, you would check if(car != null). Similarly when accessing Optional you SHOULD check if(car.isPresent()) then car.get().

Doing a get() without checking for presence is not acceptable and that can be easily identified and avoided using checkstyle configuration to throw errors.

You might ask the question.. What is any advantage we are getting because of this if we are checking for it's presence in both the patterns?

The answer lies in knowing that you have to do a check, before using it. If you strictly follow the practice of returning Optional wherever it can be nullable across, then you need not check for nulls whereever it's not optional.

It acts as a programmatic way of documenting methods which internally helps enforcing it's check via checkstyles.