by Zoran Horvat
In the previous article we have discussed Null Object pattern and its effect on avoiding null reference returned to the caller (see How to Reduce Cyclomatic Complexity: Null Object Pattern ).
The whole issue boils down to the statement the client has to implement when calling a method which just might return null to it:
public class HomeController
{
...
private ActionResult SelectView(ReceiptDto receipt)
{
if (receipt == null)
return new FailedPurchaseView();
else
return new SuccessfulPurchaseView(receipt);
}
}
It is always like this. If null, do one thing, otherwise do the other thing.
Null Object pattern helps us remove this branching logic and treat all results uniformly. In the same example above, if the result of the operation which produces purchase receipt is guaranteed to be non-null, the controller could select appropriate view for any object it gets.
Obvious benefit brought in by the Null Object is that control flow becomes the same for positive and negative branch. There are no branches based on null tests. If client wants to invoke some behavior on the result, it just invokes it. Real object performs real behavior. Null Object typically performs nothing.
On the flip side, Null Object carries no more information than null reference itself. If we had an e-commerce application in which purchase request returned Null Object receipt, we are left clueless about what went wrong. Is it insufficient funds in the user’s account, or the item is out of stock?
Negative consequences of Null Object pattern are recognized and treated by the Special Case pattern, and that is what we will demonstrate in this article.
As the business operation progresses, more and more things may go wrong and prevent it from successfully producing a resulting object. In each of the steps along the way, we may decide to give up and return an object which indicates failure.
But not all failures are the same. Early on, user might not be eligible to perform a purchase. But if eligible, the desired item might be in stock. If in stock, though, user’s account might not have enough funds.
Instead of returning Null Object in all these cases, we could refine the result and basically return a different object every time. Those would still be a kind of null objects, but they would carry different meanings with them. One would be “insufficient funds”. The other one “site is down for maintenance”. Yet another one would be “out of stock”.
Such objects are referred to as Special Cases. We can construct and return special case objects as actual results of the operation. Only if all the business checks have passed and operation completed successfully all the way through, we return the real result object.
Let’s get back to the e-commerce example. This is part of the interface implemented by the application services:
public interface IApplicationServices
{
...
IReceiptViewModel LoggedInUserPurchase(string itemName);
}
Presentation layer expects application layer to produce certain view model. Now we have a successful scenario, in which receipt view model contains actual data from the purchase, and a couple of failure scenarios.
Purchase may fail if any of these conditions is met:
For each of these cases we will create one specific class that will implement the IReceiptViewModel interface.
public class DownForMaintenance: IReceiptViewModel
{
}
This is the view model which indicates that site is down for maintenance. It carries no additional information right now, though we could add some features later. For example, this view model might carry the estimated time when the site will be up again. Such information is strictly tied to maintenance and makes no sense in any other failed purchase.
public class InvalidUser: IReceiptViewModel
{
public string UserName { get; private set; }
public InvalidUser(string userName)
{
this.UserName = userName;
}
}
And here is the second Special Case implementation, this time dealing with an attempt to purchase an item and charge an inactive or nonexistent user for it. This time, the Special Case object carries additional information – username under which the purchase has failed.
Notice that InvalidUser Special Case may be generated after the test for DownForMaintenance has passed successfully. This is precisely the moment in which our application knows who the logged in user is. Therefore, we are using additional information to construct a more informative Special Case.
That is the general rule in Special Case design pattern. As you progress through the domain logic, more and more information is collected. All that can be used to produce a more informative Special Case object.
public class OutOfStock: IReceiptViewModel
{
public string UserName { get; private set; }
public string ItemName { get; private set; }
public OutOfStock(string userName, string itemName)
{
this.UserName = userName;
this.ItemName = itemName;
}
}
And here is out of stock view model. This time, we know both the (valid) username and the item name. This object carries the message that specified item requested by the registered user does not exist in stock.
public class InsufficientFunds: IReceiptViewModel
{
public string UserName { get; private set; }
public decimal Amount { get; private set; }
public string ItemName { get; private set; }
public InsufficientFunds(string userName, decimal amount, string itemName)
{
this.UserName = userName;
this.Amount = amount;
this.ItemName = itemName;
}
}
Finally, this is the Special Case view model which carries the information that certain user did not have sufficient money to pay the requested item which costs as much as it costs. Once again, you can see that the amount of information piles up as we progress through the business transaction.
Returning this particular Special Case object instead of plain Null Object, which contains no additional information, lets us build much more informative view in response to user’s action.
By this point, we have implemented a number of Special Case classes, one for each of the negative scenarios in the application. Using Special Cases is then straight-forward. In each case where we used to return null or Null Object instance, we just create and return appropriate Special Case instance.
public class ApplicationServices: IApplicationServices
{
...
public IReceiptViewModel LoggedInUserPurchase(string itemName)
{
if (IsDownForMaintenance())
return new DownForMaintenance();
return this.domain.Purchase(Session.LoggedInUserName, itemName);
}
private bool IsDownForMaintenance()
{
return File.Exists("maintenance.lock");
}
}
This is the application service implementation which can return DownForMaintenance Special Case in cases when application is down. If application is not down, application services just forward the call to the domain services and return whichever object was produced there.
Down the stream, inside the domain services, things may be much more complicated. Here is the implementation:
public class DomainServices: IDomainServices
{
...
public IReceiptViewModel Purchase(string userName, string itemName)
{
User user = this.userRepository.Find(userName);
if (user == null)
return new InvalidUser(userName);
Account account = this.accountRepository.FindByUser(user);
return this.Purchase(user, account, itemName);
}
private IReceiptViewModel Purchase(User user, Account account, string itemName)
{
Product item = this.productRepository.Find(itemName);
if (item == null)
return new OutOfStock(user.UserName, itemName);
ReceiptDto receipt = user.Purchase(item);
MoneyTransaction transaction = account.Withdraw(receipt.Price);
if (transaction == null)
return new InsufficientFunds(user.UserName, receipt.Price, itemName);
return receipt;
}
}
In this case, domain services are returning Special Case instances whenever something goes wrong. Only at the very end of execution, if everything went fine, the domain service will return an actual receipt.
In this way, application and domain services have joined their efforts to tell the presentation layer what really happened when the user has requested a purchase. If purchase has failed, presentation layer will receive an object carrying all the available data about reasons of the failure. If purchase went fine, presentation layer will receive a receipt and will be able to present it to the user.
Special Case design pattern is an extension of the Null Object pattern idea. In many cases, Null Object is really not applicable, because it lacks information about why no more specific object could be produced. Special Case is a more specific object and it carries additional information.
Ultimate caller doesn’t have to treat Null Object or Special Case instances in any way special. Both of these design patterns are meant to provide a non-null object which fully implements the interface expected by the caller.
Consequence on code complexity is that the caller will not have to perform an if-then-else based on whether the object reference is null or non-null. References will always be non-null. The only significant difference between Null Object and Special Case design patterns is that the latter may carry more complicated behavior.
Hence, the Special Case pattern can be applied to more complicated business scenarios and still protect the caller from unnecessary if-then-else logic.
If you wish to learn more, please watch my latest video courses
In this course, you will learn the basic principles of object-oriented programming, and then learn how to apply those principles to construct an operational and correct code using the C# programming language and .NET.
As the course progresses, you will learn such programming concepts as objects, method resolution, polymorphism, object composition, class inheritance, object substitution, etc., but also the basic principles of object-oriented design and even project management, such as abstraction, dependency injection, open-closed principle, tell don't ask principle, the principles of agile software development and many more.
More...
In this course, you will learn how design patterns can be applied to make code better: flexible, short, readable.
You will learn how to decide when and which pattern to apply by formally analyzing the need to flex around specific axis.
More...
This course begins with examination of a realistic application, which is poorly factored and doesn't incorporate design patterns. It is nearly impossible to maintain and develop this application further, due to its poor structure and design.
As demonstration after demonstration will unfold, we will refactor this entire application, fitting many design patterns into place almost without effort. By the end of the course, you will know how code refactoring and design patterns can operate together, and help each other create great design.
More...
In four and a half hours of this course, you will learn how to control design of classes, design of complex algorithms, and how to recognize and implement data structures.
After completing this course, you will know how to develop a large and complex domain model, which you will be able to maintain and extend further. And, not to forget, the model you develop in this way will be correct and free of bugs.
More...
Zoran Horvat is the Principal Consultant at Coding Helmet, speaker and author of 100+ articles, and independent trainer on .NET technology stack. He can often be found speaking at conferences and user groups, promoting object-oriented and functional development style and clean coding practices and techniques that improve longevity of complex business applications.