Поиск  
Always will be ready notify the world about expectations as easy as possible: job change page
Nov 22, 2023

How returning NULL leads to snitchy bugs and how to prevent them

Автор:
Источник:
Просмотров:
3216

There is a simple solution just around the corner

Returning NULL horror

Null as a return value is so easy to implement, but it brings many problems. So people make mistakes. That is part of being human. Sometimes it turns out to be a billion-dollar mistake.

“I call it my billion-dollar mistake. It was the invention of the null reference in 1965.” — Tony Hoare — British Turing Award winner Tony Hoare at a conference in 2009.

Every developer will probably have had a NullReferenceException or something similar blows up in their face. Now you know whom to thank.

So if using NULL has so many drawbacks, why are many of us, including me, also using it so recklessly? And are there better alternatives? Yes, you will see.

The Null Problem

I am sure your problem won’t be worth a billion dollars. 😉 But many do it since Null as a return value is so easy to implement. This is also my guilty pleasure.

Unfortunately, it does not provide any information why no more object was delivered. Imagine calling the API for the database and getting a null return. I instantly think of 3 scenarios that might be wrong:

  • Did the database access fail?
  • Was there a problem with authentication?
  • Is there a missing reference anywhere else my return relied on?

You can never be sure what has happened.

This is a big disadvantage.

Easy to implement. Easy to check. But the handling is the worst!

The consuming code must always include so-called guard clauses. That means any statement that wants to use a potential null object must perform the null check. This results in many if-else branches spread across the entire code base, which increases the cyclomatic complexity.

Your code gets worse and worse to comprehend,

and I dare you to forget only a single null check.

Pulp Fiction Scene — “Say What again, I dare you, I double dare motherfu**er”
Pulp Fiction Scene — “Say What again, I dare you, I double dare motherfu**er”

This brings us back to the null check on returned references.

How to use the Null Object Design Pattern

Ironically, introducing a null object is the first way to avoid null checking.

Consider this as a simple example:

Incoming messages are routed depending on their message type.

The class Message includes an enum to determine their type:

public class Message
{
   public enum Types
   {
      Sms,
      Unknown
   }

   private readonly Types _type;

   public Types Type => _type;

   public Message(Types type)
   {
      _type = type;
   }
}

The routers, which take over the further processing, are implemented by the interface IRouter:

interface IRouter
{
   void Route(Message message);
}

This interface can be inferred to determine concrete implementations of all desired types:

class SmsRouter : IRouter
{
   public void Route(Message message)
   {
      Console.WriteLine("Implementation details for SMS");
   }
}

class MailRouter : IRouter
{
   public void Route(Message message)
   {
      Console.WriteLine("Implementation details for Mail");
   }
}

class ChatRouter : IRouter
{
   public void Route(Message message)
   {
      Console.WriteLine("Implementation details for Chat");
   }
}

The following factory generates the router that matches the type of the Message or returns null in the initial implementation if a message type is unknown.

class RouterFactory
{
    public IRouter GetRouter(Message.Types messageType)
    {
        if (messageType == Message.Types.Sms) return new SmsRouter();
        if (messageType == Message.Types.Mail) return new MailRouter();
        if (messageType == Message.Types.Chat) return new ChatRouter();

        return null;
    }
}

So this is the typical code that quickly makes problems. You will see what happens when you write the calling code.

I defined an array of Messages and looped through them to get the matching router for each message type:

var messages = new Message[]
{
   new(Message.Types.Sms),
   new(Message.Types.Mail),
   new(Message.Types.Chat),
   new(Message.Types.Unknown),
};

foreach (var msg in messages)
{
   new RouterFactory().GetRouter(msg.Type).Route(msg);
}

And this stuff throws the console at me for the unknown type:

Implementation details

This is a runtime error nightmare and would be easily avoidable by using the Null Conditional Operator:

new RouterFactory().GetRouter(msg.Type)?.Route(msg);

But then you end up in the same situation again.
I dare you always remember to implement just a single one of these.

So what now?

Your goals are:

  • Avoid checking for null actively.
  • In case of a null return — the code runs smoothly without crashing.

You can achieve this by creating an automatic RoutingHandler that returns a NullObjectRouter that you define:

public class NullObjectRouter : IRouter
{
    private static readonly NullObjectRouter _instance = new NullObjectRouter();
    public static NullObjectRouter Instance => _instance;

    private NullObjectRouter()
    {
    }

    public void Route(Message message)
    {
        Console.WriteLine("No router for message type of {0}. Do nothing.",
        message.Type);
    }
}

Since you have your failsafe NullObjectRouter, you can quickly build a Handler on top of your adapted factory from before. Simply ensure to return NullObjectRouter.Instance instead of null:

class RouterFactory
{
   public IRouter GetRouter(Message.Types messageType)
   {
      if (messageType == Message.Types.Sms) return new SmsRouter();
      if (messageType == Message.Types.Mail) return new MailRouter();
      if (messageType == Message.Types.Chat) return new ChatRouter();

      return NullObjectRouter.Instance;
   }
}

Then implement the wrapper for the factory, and you can handle every type of Message automatically without any null checks:

public class RoutingHandler
{
   private readonly RouterFactory _factory = new RouterFactory();

   public void Process(IEnumerable<Message> messages)
   {
      foreach (var msg in messages)
      {
         _factory.GetRouter(msg.Type).Route(msg);
      }
   }
}

Your possible calling code:

var messages = new Message[]
{
   new(Message.Types.Sms),
   new(Message.Types.Mail),
   new(Message.Types.Chat),
   new(Message.Types.Unknown),
};

new RoutingHandler().Process(messages);

Implementation details

This adaptation eliminates null checks in the implementation of the RoutingHandler, which is the Factory supplies by this adjustment, in each case, an instance of IRouter.

Identify Code Smells With Ease and Replace Them With Best Practice Patterns. Get the “All Code Smells As One-Liner”-Cheatsheets.

References

Похожее
Mar 1, 2023
Author: Alper Ebiçoğlu
In this article, I’ll show you all the object mapping strategies of EF Core and especially the new one: TPC Inheritance Mapping. TPC mapping is the new feature introduced with Entity Framework Core 7. It’s called Table Per Concrete type...
Aug 5
Author: Zeeshan Wazir
C# is a powerful programming language, widely used for software development across various domains. One crucial aspect of application development is dealing with PDFs, whether it's generating PDFs, extracting information from existing PDF documents, or manipulating PDF files. In this...
Jan 1, 2023
Author: Walter Guevara
By software engineering standards, ASP.NET Web Forms can be considered old school. Perhaps I’m aging myself with that statement, but its true. From a software perspective, even 10 years begins to show its age. I started to use Web Forms...
Aug 29
Author: Juldhais Hengkyawan
In some scenarios, we need to use different database providers for development and production environments. For instance, consider a scenario where we use SQLite in the development environment and SQL Server for the production environment. This article will guide us...
Написать сообщение
Тип
Почта
Имя
*Сообщение
RSS
Если вам понравился этот сайт и вы хотите меня поддержать, вы можете
Soft skills: 18 самых важных навыков, которыми должен владеть каждый работник
Performance review, ачивки и погоня за повышением грейда — что может причинить боль сотруднику IT-компании?
GraphQL решает кучу проблем — рассказываем, за что мы его любим
Разбираемся с middleware в ASP.NET Core
Как избавиться от прокрастинации до того, как она разрушит вашу карьеру
Функции и хранимые процедуры в PostgreSQL: зачем нужны и как применять в реальных примерах
Разрабы работают медленно и дорого — и люди считают нас лентяями. Просто в разработке всё сложно
Using a сustom PagedList class for Generic Pagination in .NET Core
Четыре типажа программистов
Интернет вещей — а что это?
LinkedIn: Sergey Drozdov
Boosty
Donate to support the project
GitHub account
GitHub profile