© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2023
V. SarcarSimple and Efficient Programming with C# https://doi.org/10.1007/978-1-4842-8737-8_11

11. Special Attention to the Null Values

Vaskaran Sarcar1  
(1)
Kolkata, West Bengal, India
 
In the previous chapter, I told you about runtime exceptions. These are dangerous. Most often, they come in the form of the NullReferenceException in C#. Similarly, Java has the NullPointerException. The exception names can be different, but at the core, they arise when you try to access a member of an object whose value is null. Michael Feathers wrote the following in the book Clean Code:

Returning null from methods is bad, but passing null into methods is worse. Unless you are working with an API which expects you to pass null, you should avoid passing null in your code whenever possible.

In short, these null values can kill an application prematurely. So, you need to handle them properly. This chapter focuses on them and provides you with some useful suggestions.

Initial Program

Suppose that IVehicle is an interface that has a method called ShowStatus(). Two concrete classes, Bus and Train, inherit from this interface and provide the implementation for this interface method. Here is the code segment that reflects this:
interface IVehicle
{
    void ShowStatus();
}
class Bus : IVehicle
{
    public void ShowStatus()
    {
        Console.WriteLine("One bus is ready to travel.");
    }
}
class Train : IVehicle
{
    public void ShowStatus()
    {
        Console.WriteLine("One train is ready to travel.");
    }
}

For simplicity, I want clients of this program to create three IVehicle instances and invoke the corresponding ShowStatus method. Let’s assume clients of this program can type b and t (in a console application) to create a Bus object and a Train object, respectively. Can you write this program? I know that you can. But before you write your program, let’s investigate the following program. This program has a potential bug, and we’ll improve it for sure, but you can see the importance of using null checks to avoid runtime errors in a program.

Demonstration 1

Here is the complete demonstration:
Console.WriteLine("Chapter 11.Demo-1.");
Console.WriteLine("This program has a potential bug.");
IVehicle[] vehicles = new IVehicle[3];
int vehicleCount = 0;
while (vehicleCount < 3)
{
    Console.WriteLine("Enter your choice(Type 'b' for
     a bus, 't' for a train.");
    string input = Console.ReadLine();
    switch (input)
    {
        case "b":
            vehicles[vehicleCount] = new Bus();
            break;
        case "t":
            vehicles[vehicleCount] = new Train();
            break;
        default:
            Console.WriteLine("Invalid input");
            break;
    }
    vehicleCount++;
}
Console.WriteLine("**Checking the vehicle's status sequentially:**");
foreach (IVehicle vehicle in vehicles)
{
    vehicle.ShowStatus();
}
interface IVehicle
{
    void ShowStatus();
}
class Bus : IVehicle
{
    public void ShowStatus()
    {
        Console.WriteLine("One bus is ready to travel.");
    }
}
class Train : IVehicle
{
    public void ShowStatus()
    {
        Console.WriteLine("One train is ready to travel.");
    }
}

Output

This program does not show any errors if you pass valid inputs. Here is a sample:
Chapter 11.Demo-1.
This program has a potential bug.
Enter your choice(Type 'b' for a bus, 't' for a train.)
b
Enter your choice(Type 'b' for a bus, 't' for a train.)
t
Enter your choice(Type 'b' for a bus, 't' for a train.)
t
**Checking the vehicle's status sequentially:**
One bus is ready to travel.
One train is ready to travel.
One train is ready to travel.
Let’s assume that, by mistake, the user has supplied a different character, say e, as shown here:
Chapter 11.Demo-1.
This program has a potential bug.
Enter your choice(Type 'b' for a bus, 't' for a train.)
b
Enter your choice(Type 'b' for a bus, 't' for a train.)
t
Enter your choice(Type 'b' for a bus, 't' for a train.)
e

This time you will see the following runtime exception:

System.NullReferenceException: ‘Object reference not set to an instance of an object’.

Analysis

To avoid the previous runtime error, an immediate fix may come into your mind, as shown here:
if (vehicle != null)
{
  vehicle.ShowStatus();
}
This solution will work in this case. But think of an enterprise application. When you do null checks for each possible scenario, if you place if conditions like this, you make your code “dirty.” At the same time, you may notice the side effect of the code being difficult to maintain. Is there a better solution? I think, yes. You can use the null conditional operator (for member access) as follows:
vehicle?.ShowStatus();
This operator is available in C# 6 and later versions only. It provides you with a lot of help. Still, using the previous code segment, you can avoid the NullReferenceException, but you may not see the status of all the vehicles. To understand this better, consider the following output now:
Chapter 11.Demo-1.
This program has a potential bug.
Enter your choice(Type 'b' for a bus, 't' for a train.)
b
Enter your choice(Type 'b' for a bus, 't' for a train.)
t
Enter your choice(Type 'b' for a bus, 't' for a train.)
e
Invalid input
**Checking the vehicle's status sequentially:**
One bus is ready to travel.
One train is ready to travel.

So, there are situations where instead of doing nothing, you want to provide a default behavior that suits your application. For example, in this case, instead of showing nothing, you may like to show that the vehicle is not ready yet. This is helpful because as per the requirements, clients need to see the status of all vehicles, but if they cannot see one (or more) of them, they will be clueless about this. In a situation like this, you’ll want to know about the Null Object pattern, which is covered next.

Better Programs

Using the Null Object pattern, you can implement a “do-nothing” relationship, or you can provide a default behavior when an application encounters a null object instead of a real object. The core aim is to make a better solution by avoiding a “null objects check.” Figure 11-1 shows a basic structure of this pattern.
Figure 11-1

The basic structure of the Null Object pattern

Let’s modify the faulty program that you saw in demonstration 1. This time you’ll tackle an invalid input using a IncompleteVehicle object. Whenever the user supplies invalid data, the application will ignore it, and instead of creating a Bus or Train object, it’ll use an IncompleteVehicle object. The class is defined as follows:
class IncompleteVehicle : IVehicle
{
    public void ShowStatus()
    {
        Console.WriteLine("This vehicle is incomplete.");
    }
}
POINTS TO REMEMBER

For any null object method, you need to return whatever seems sensible as a default. In our example, you cannot travel with an IVehicle object that is not created with valid input. So, it makes sense that the ShowStatus() method of the IncompleteVehicle class shows that it is not ready yet.

Let’s take a look at the complete demonstration.

Demonstration 2

This is a modified version of demonstration 1. I have marked important changes in bold.
Console.WriteLine("Chapter 11.Demo-2.");
IVehicle[] vehicles = new IVehicle[3];
int vehicleCount = 0;
while (vehicleCount < 3)
{
    Console.WriteLine("Enter your choice(Type 'b' for
      a bus, 't' for a train.)");
    string input = Console.ReadLine();
    switch (input)
    {
        case "b":
            vehicles[vehicleCount] = new Bus();
            break;
        case "t":
            vehicles[vehicleCount] = new Train();
            break;
        default:
            Console.WriteLine("Invalid input");
            vehicles[vehicleCount] = new IncompleteVehicle();
            break;
    }
    vehicleCount++;
}
Console.WriteLine("**Checking the vehicle's status sequentially:**");
foreach (IVehicle vehicle in vehicles)
{
    vehicle.ShowStatus();
}
interface IVehicle
{
    void ShowStatus();
}
class Bus : IVehicle
{
    public void ShowStatus()
    {
        Console.WriteLine("One bus is ready to travel.");
    }
}
class Train : IVehicle
{
    public void ShowStatus()
    {
        Console.WriteLine("One train is ready to travel.");
    }
}
class IncompleteVehicle : IVehicle
{
    public void ShowStatus()
    {
        Console.WriteLine("This vehicle cannot travel.");
    }
}

Output

This program does not show any error for valid input. Also, it does not halt whenever you pass an invalid input by mistake. Here is a sample:
Chapter 11.Demo-2.
Enter your choice(Type 'b' for a bus, 't' for a
 train.)
b
Enter your choice(Type 'b' for a bus, 't' for a
 train.)
gh
Invalid input
Enter your choice(Type 'b' for a bus, 't' for a
 train.)
t
**Checking the vehicle's status sequentially:**
One bus is ready to travel.
This vehicle cannot travel.
One train is ready to travel.

Analysis

The problem of demonstration 1 is gone! Also, you can see the status of all vehicles using the Null Object pattern too. Truly, these were the key intentions of this chapter.

Still, demonstration 2 can be improved. Why?
  • In this application, for each invalid input, you create an IncompleteVehicle object. Now think about an enterprise application where users can pass many invalid inputs. In every case, if you create the same type of objects to tackle them, a large number of objects will be created unnecessarily, and they can affect the application performance.

So, what is the solution? Following the Singleton design pattern, you can create only one object, and from that time onward, you’ll reuse it. The detailed discussion of the Singleton pattern is beyond the scope of this book, but I’ll show you a modified version of the IncompleteVehicle class that you can use in this program to avoid unnecessary object creations. In the following demonstration, you’ll see this class and its usage.

Demonstration 3

This is a modified version of demonstration 2. I have marked important changes in bold.
Console.WriteLine("Chapter 11.Demo-2(Modified version).");
IVehicle[] vehicles = new IVehicle[3];
int vehicleCount = 0;
while (vehicleCount < 3)
{
    Console.WriteLine("Enter your choice(Type 'b' for
     a bus, 't' for a train.)");
    string input = Console.ReadLine();
    switch (input)
    {
        case "b":
            vehicles[vehicleCount] = new Bus();
            break;
        case "t":
            vehicles[vehicleCount] = new Train();
            break;
        default:
            Console.WriteLine("Invalid input");
            vehicles[vehicleCount] = IncompleteVehicle.Instance;
            break;
    }
    vehicleCount++;
}
Console.WriteLine("**Checking the vehicle's status sequentially:**");
foreach (IVehicle vehicle in vehicles)
{
    vehicle.ShowStatus();
}
interface IVehicle
{
    void ShowStatus();
}
class Bus : IVehicle
{
    public void ShowStatus()
    {
        Console.WriteLine("One bus is ready to travel.");
    }
}
class Train : IVehicle
{
    public void ShowStatus()
    {
        Console.WriteLine("One train is ready to travel.");
    }
}
class IncompleteVehicle : IVehicle
{
    private static readonly IncompleteVehicle instance = new();
    public static IncompleteVehicle Instance
    {
        get
        {
            return instance;
        }
    }
    public void ShowStatus()
    {
        Console.WriteLine("This vehicle cannot travel.");
    }
}

Analysis

This program produces the same output that you saw in demonstration 2. I have not repeated those lines here. The difference is that this program can create at most one IncompleteVehicle instance. If needed, it’ll reuse this instance again.

POINTS TO NOTE
In this demonstration, I talked about the Null Object pattern. It is useful to remember that there is a pattern called the Special Case pattern. These two concepts are very close. Why? A special case is made when a subclass provides special behavior for a particular case. In fact, the book Patterns of Enterprise Application Architecture says the following:

I haven’t seen Special Case written up as a pattern yet, but Null Object has been written up in [Woolf]. If you’ll pardon the unresistable pun, I see Null Object as special case of Special Case.

To describe the intent of the Null Object pattern, Bobby Woolf writes the following (see https://www.cs.oberlin.edu/~jwalker/refs/woolf.ps):

Provide a surrogate for another object that shares the same interface but does nothing. The Null Object encapsulates the implementation decisions of how to “do nothing” and hides those details from its collaborators.

Now you may think that since I have implemented a special behavior instead of a “do-nothing” behavior, I could call it a Special Case pattern instead of a Null Object pattern. Probably you are correct. But the overall philosophy is the same because for me, doing nothing also depicts a special kind of behavior. This is why instead of memorizing a pattern name, let’s focus on why we see different pattern names. The idea behind this is that there are situations where a null can mean different things. For example, consider the cases such as “incomplete data versus wrong data” or “a missing customer versus an invalid customer.” Instead of treating everything as a null customer, you can use the concept of a special customer.

Summary

Runtime errors are dangerous, and often they come in the form of NullReferenceException in C#. So, handling the null values is an important aspect of programming. This chapter discussed them using some simple code fragments and provided a better solution.

In short, this chapter answered the following questions:
  • How can a program raise NullReferenceException in C#?

  • How can you avoid repeated null checks using a traditional if-else chain?

  • How can you provide default behavior (or a do-nothing behavior) for the null values using the Null Object pattern?

  • How can you avoid unnecessary object creations when you deal with the null values?

Before I finish writing this chapter, I want to mention the following points too:
  • The Null Object pattern basically helps you to implement a default behavior when you unconsciously deal with an object that is not present at all. But trying to supply such a default behavior may not be appropriate always. There are situations when you like to fix the root cause of a failure. In that case, throwing a NullReferenceException can make more sense to you. In this case, you can handle the exception in a try-catch block or a try-catch-finally block and update the log information accordingly.

  • Incorrect implementations of the Null Object pattern can suppress the true bug that may appear normal in program execution.

So, in the end, it heavily depends on your mindset about how to handle the null values in an application.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.188.53.32