In the previous part of this series we looked at what are basically a lot of guidelines for achieving "clean code". In this part I'd like to take a closer look at something we call null. Our main goal will be: to get rid of it.

THE MEANING OF NULL

Actually, there's not one meaning we can ascribe to null. This is why it complicates our code quite a lot. In the case of null as a return value, it could mean:

  • "I don't know what I should return to you, so I'll return null."
  • "What I'd like to return to you doesn't make sense anyway, so I'll return null."
  • "What you were expecting to find isn't there, so I'll return null."
  • "I never return anything meaningful (because I'm a void function), so I'll returnnull."

I'm sure there are even more situations you can think of.

In the case of null being passed for function arguments, it could mean:

  • "Sorry, I don't know what to provide, so I'll provide null."
  • "It seems this argument is optional, so I'll provide null."
  • "I got this value from someone else (it's null), I'll just pass it along now."

All of this may seem funny, but it's not if your code is actually having these kinds of conversations. It's certainly a good idea to get rid of the uncertainty and vagueness that null brings to your code. Besides, most of the time when you encounter an actual null value in your program, you probably weren't expecting it. You just call a method on it, thinking that it is an object and PHP will rightfully let your program crash.

SOLUTIONS

Every particular null situation requires a different solution, but at least I'll list several common solutions for you. The next time you feel the need for using a null value in your code, try to pick one of the following strategies. The general guideline is to make sure that a value can always be used in the same way.

USE "NULL" EQUIVALENTS

In the case of primitive variables (of type integer, string, boolean, etc.) see if you can use their "null equivalents" as an alternative for returning actual null values:

  • When your function usually returns an array, return an empty array instead ofnull.
  • When your function usually returns an integer, return 0 instead of null.
  • ...

You get the idea. See what makes sense in your case (maybe 0 makes no sense but 1does). If you only supply values of the expected types you can always use the same operators, functions, etc. on any value no matter what. For example, instead of:

if (is_array($values)) {
    foreach ($values as $value) {
        ...
    }
}

you can always do:

foreach ($values as $value) {
    ...
}

PHP (before version 7) only allows you to enforce the correct types of function arguments if their expected types are objects or arrays, in which case passing a nullwon't work. This means that primitive types should still be validated in your function body:

function (ClassName $a, array $b, $c) {
    // $a and $b can't be null, but $c can:
    if (!is_int($c)) {
        throw new \InvalidArgumentException(...);
    }
}

This will of course lead to a lot of extra (duplicate) code, so again I recommend you to use an assertion library, like beberlei/assert:

function (ClassName $a, array $b, $c) {
    Assertion::integer($c);
}

This will prevent many mistakes or implicit type conversions down the road.

USE "NULL" OBJECTS

If you expect a value to be of type array, then retrieving a null value is basically a violation of the contract of that value, since null can't be used in all the ways in which an array can be used. The same is true for objects. If a certain variable was expected to be of a certain class, and instead the variable contains null, the contract was broken. You'll notice this when you call a method on null and PHP crashes with a Fatal error.

In many cases you can prevent this problem by introducing so-called "null objects". Where you would normally return null instead of an object of a given type, you now return another object of the same type (or subtype). This often (but not always) requires the introduction of an interface which serves as the shared type between actual objects and null objects.

Null services

A much seen example is that of optionally injecting a logger in a service-like object or method:

function doSomething(Logger $logger = null) {
    if ($logger !== null) {
        $logger->debug('Start doing something');
    }
    ...
}

The provided logger can be any concrete implementation of the Logger interface. Providing null would be valid, but forces you to check for null anywhere you want to log something. Introducing a "null object" would mean providing another implementation of Logger which does nothing:

class NullLogger implements Logger
{
    public function debug($message)
    {
        // do nothing
    }
}

This works very well since the implementation of all of the methods of such a collaborating service can be left empty: these methods are so-called commandmethods with a void type return value.

Null data objects

It can be a bit more tricky to replace null values in objects that hold interesting data, like domain or view model objects. These objects have (almost) no commandmethods, only query methods, meaning they have an informational return value. In these cases you'll need to provide default or "null" implementations which follow the same contract and offer useful data. Consider the following code, which asks a factory to create a view model for the current user and prints its name:

$userViewModel = $userViewModelFactory->createUserViewModel();
if ($userViewModel === null) {
    echo 'Anonymous user';
} else {
    echo $userViewModel->getDisplayName();
}

We can get rid of the null case by letting the createUserViewModel() always return an object which has the same contract as the user view model for a logged in user:

// shared interface
interface UserViewModel
{
    /**
     * @return string
     */
    public function getDisplayName();
}
// view model for logged in users
class LoggedInUser implements UserViewModel
{
    public function getDisplayName()
    {
        return $this->displayName;
    }
}
// "null object" implementation
class AnonymousUser implements UserViewModel
{
    public function getDisplayName()
    {
        return 'Anonymous user';
    }
}
class UserViewModelFactory
{
    public function createUserViewModel()
    {
        if (/* user is logged in */) {
            return new LoggedInUser(...);
        }
        return new AnonymousUser();
    }
}

Now UserViewModelFactory::createUserViewModel will always return an instance of UserViewModel and client code doesn't have to compensate for a possible null value being returned:

$userViewModel = $userViewModelFactory->createUserViewModel();
echo $userViewModel->getDisplayName();

If the "null object" is a special case of the normal object you may define a base class and extend it, instead of letting both classes implement a shared interface.

THROW EXCEPTIONS

In some cases it isn't possible or rational to return a null-equivalent value or null object. Returning anything else than what the client expects would be a lie. For example in the following class:

class UserRepository
{
    public function getById($id) {
        if (/* user was not found */) {
            return null;
        }
        return new User(...);
    }
}

The null return value here means "I couldn't find what you were looking for". However, the client calling getById() expected to receive a User object (because it expected the provided identifier to be valid). In case that user can't be found, this is exceptional and we need to let the user know what went wrong. In other words, we should throw an exception:

class UserRepository
{
    public function getById($id) {
        if (/* user was not found */) {
            throw UserNotFound::withId($id);
        }
        return new User(...);
    }
}
class UserNotFound extends \RuntimeException
{
    public static function withId($id)
    {
        return new self(
            sprintf(
                'User with id "%s" was not found', $id
            )
        );
    }
}

(See Formatting Exception Messages by Ross Tuck for more information about the above practice of using named constructors and encapsulated message formatting for exceptions.)

ENCAPSULATE NULL

I don't think we can go so far as to completely exterminate null. Reducing the amount of nulls as much as possible is something that benefits your code quality very much already. Whenever you do need to have null values (like in uninitialized attributes), make sure you hide that fact well behind the public interface of your class. In other words: encapsulate. For example, convert a meaningless null into a meaningful boolean:

class Value
{
    private $value;
    public function isDefined() {
        return $this->value !== null;
    }
}

Or make sure that clients don't have to worry about a middle name that's null:

class Name
{
    private $firstName = 'Matthias';
    private $middleName = null;
    private $lastName = 'Noback';
    public function fullName()
    {
        return implode(
            ' ',
            array_filter([
                $this->firstName,
                $this->middleName,
                $this->lastName
            ])
        );
    }
}

When you want to allow your objects to be constructed without certain information, you can hide the fact that you're using nulls in that case by offering alternative named constructors:

class Person
{
    private $name;
    public static function fromName(Name $name)
    {
        $person = new self();
        $person->name = $name;
        return $person;
    }
    public static function anonymous()
    {
        $person = new self();
        // $person->name will remain null
        return $person;
    }
}

CONCLUSION

In this article we've seen several ways in which you can get rid of nulls in your code. When you apply these techniques combined with the ones from the previous article, the complexity of your code will be greatly reduced.

So far we've discussed statements and expressions in function bodies. In the next article we'll take a look at different types of objects and their lifecycles. We'll discuss object creation, references and state changes.

https://www.ibuildings.nl/blog/2016/01/programming-guidelines-php-developers-part-2-getting-rid-null

PROGRAMMING GUIDELINES – PART 2: GETTING RID OF NULL
标签: