Calling optional dependencies such as logging, tracing and notifications should be fast and reliable.
The Null Object pattern can be used for reducing code complexity by managing optional dependencies with default behaviour as discussed in the Reject the Null Checked Object post.
This post aims to illustrate the problem with the Null Object pattern and how to resolve it using a simple lambda expression.
The problem is that the null check pattern can potentially lead to performance degradation and unnecessary failure.
The solution is to avoid executing unnecessary operations especially for default behaviour.
Download Source Code
Setup
The intent of the sample application is to read and parse XML documents for a book store. The document reader is responsible for logging the book titles using the code below.
public interface ILogger
{
void Log(string message);
}
public class ConsoleLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine(message);
}
}
public class DocumentReader
{
private readonly ILogger _logger;
public DocumentReader(ILogger logger)
{
if (logger == null) throw new ArgumentNullException("logger");
_logger = logger;
}
public void Read(XmlDocument document)
{
if (document == null) throw new ArgumentNullException("document");
var books = document.SelectNodes("catalog/book");
if (books == null) throw new XmlException("Catalog/book missing.");
_logger.Log(string.Format("Titles: {0}.",
string.Join(", ", GetBookTitles(document))));
// Implementation
}
private static IEnumerable<string> GetBookTitles(XmlNode document)
{
Console.WriteLine("Retrieving the book titles");
var titlesNodes = document.SelectNodes("catalog/book/title");
if (titlesNodes == null) yield break;
foreach (XmlElement title in titlesNodes)
{
yield return title.InnerText;
}
}
}
Note: The GetBookTitles method adheres to the null check pattern. When the titlesNode is null on line 40, an empty IEnumerable will be returned instead of a null. This means that clients don’t have to check for a null and it reduces the likelihood of potential null reference exceptions.
Problem
Here is an example that illustrates the execution of the application.
static void Main(string[] args)
{
var document = new XmlDocument();
document.LoadXml(@"<catalog>
<book><title>Developer's Guide</title></book>
<book><title>Tester's Guide</title></book>
</catalog>");
var logger = new ConsoleLogger();
var docReader = new DocumentReader(logger);
docReader.Read(document);
Console.ReadLine();
}
Retrieving the book titles
Book titles: Developer's Guide, Tester's Guide.
The solution works well when an actual logger is used but what happens if we replace the logger with the NullLogger as shown below?
public class NullLogger : ILogger
{
public void Log(string message)
{
// Purposefully provides no behaviour
}
}
var logger = new NullLogger();
var docReader = new DocumentReader(logger);
docReader.Read(document);
Console.ReadLine();
Retrieving the book titles
Oops! The application parsed the book titles unnecessarily. What if the application crashed when the GetBookTitles method was called? This means that the application can potentially crash due to logging, which is not a good design.
Solution
Here is an example that illustrates the improved version. The logger was modified to accept two methods. The first method takes a string for simple logging operations and the second method takes a lambda function that will produce a string.
public interface ILogger
{
void Log(string message);
void Log(Func<string> messageFunc);
}
public class NullLogger : ILogger
{
public void Log(string message)
{
// Purposefully provides no behaviour
}
public void Log(Func<string> messageFunc)
{
// Purposefully provides no behaviour
}
}
public class ConsoleLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine(message);
}
public void Log(Func<string> messageFunc)
{
try
{
Console.WriteLine(messageFunc.Invoke());
}
catch (Exception ex)
{
Console.WriteLine("Failed to log the result. Error: {0}", ex);
}
}
}
public class DocumentReader
{
public void Read(XmlDocument document)
{
_logger.Log(() => string.Format("Titles: {0}.",
string.Join(", ", GetBookTitles(document))));
...
}
}
Note: The console logger reliability was improved on line 29 to 36 since exceptions can be caught instead of interrupting the application when a log building operation fails.
Running the example using the Console Logger will produce the same result as the original example.
Let's run the Null Logger example again.
var logger = new NullLogger();
var docReader = new DocumentReader(logger);
docReader.Read(document);
Console.ReadLine();
Success! The output is empty since the GetBookTitles method was never called. Therefore, the null logger did not perform unnecessary work.
Summary
The null object checking pattern simplifies solutions by removing the need to check for null. The drawback is the potential risk of impeding performance and causing failure. Passing around lamba expression functions can be a subtle solution to overcome the problem without overcomplicating the code.