by Zoran Horvat
In the previous article we have highlighted issues that arise from use of optional method parameters (see Avoiding Optional Parameters ).
Most prominent issue is possible misunderstanding between the caller and the callee. Whenever the class makes decision based on arguments, the decision may differ from what the caller intended.
In this article we will cover similar scenario, this time when the method allows some arguments to be null as an indicator that some operation should take alternate course.
We will continue working on an e-commerce application example which was demonstrated in previous articles from this series. Right now, we are interested in methods that allow registered user to purchase an item on the website.
By this point, users would register with the site and then place an order. All buying was done by registered users. But now suppose that the selling network has expanded and some land-based shops have been open.
The same website is used by sellers in the shop. This time there are no registered users anymore, but buyers are actually physical people coming in right from the street.
To support this new selling model, we are adding one more method to the application services:
public interface IApplicationServices
{
...
IReceiptViewModel LoggedInUserPurchase(string itemName);
IReceiptViewModel AnonymousPurchase(string itemName);
}
The first method will be invoked by the website front-end. It will perform a buy in the name of the currently logged in user.
The second method will be invoked by the land-based shop application. A buy will be performed in the name of anonymous customer who just popped up in the shop, carrying cash or whatever method of payment she might find fit.
Now how do we implement these two scenarios in the domain model? Here is one possibility:
public class DomainServices: IDomainServices
{
private readonly IUserRepository userRepository;
private readonly IProductRepository productRepository;
private readonly IAccountRepository accountRepository;
public IReceiptViewModel Purchase(string userName, string itemName)
{
User user = null;
Account account = null;
if (userName != null)
{
user = this.userRepository.Find(userName);
if (user == null)
return new InvalidUser(userName);
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);
if (user == null)
return new ReceiptDto(null, item.Name, item.Price);
ReceiptDto receipt = user.Purchase(item);
MoneyTransaction transaction = account.Withdraw(receipt.Price);
if (transaction == null)
return new InsufficientFunds(user.UserName, receipt.Price, itemName);
return receipt;
}
...
}
The first Purchase method is the public one and it is invoked by the application service. This method is doing both things – it lets both registered and anonymous buyer to purchase an item. To support both scenarios, this method allows userName parameter to be null, and null reference here has a special meaning. Whenever userName is null, the method knows that it is working with the anonymous buyer, so it doesn’t search for the user in the user repository, nor does it look up the user’s account.
The second Purchase method is private and it accepts model objects – User and Account objects are expected by this method. But this time, user object is allowed to be null, and once again null reference has a special meaning. It means that user is not registered and that the purchase should be performed in the name of a physical buyer which carries money with herself.
The first thing this private Purchase method does is to test whether the requested item is available. Once that is done, it immediately tests the user object against null. If user is null, that indicates this magical scenario about the physical buyer rather than the web user. Therefore, the Purchase method immediately returns the receipt as soon as it discovers that it is dealing with the physical buyer.
Only if the user object is non-null, the rest of the domain logic gets executed. This includes withdrawing money from the user’s account and potentially returning the insufficient funds error. Notice how we assume that physical user has brought in sufficient amount of money to perform the purchase.
The problem with code shown above is the same as in the previous article about optional method parameters: Method implementation is complicated. Control is branching based on whether the optional parameter is null or not. Null reference has a special meaning and certain segments of the domain logic are mapped to null references.
Instead of polluting the code with if-then-else logic around null references, we should rather think of a better domain model, which more closely relates to the real system we are modelling.
For example, why would the null reference to registered user (User class) indicate that we are dealing with the physical buyer? It would be much more convenient if we just created two user classes – one for registered users, and the other one for physical, anonymous buyers.
The same stands for the Account class. We said before that the business assumes that physical buyers are carrying cash with them. Nothing prevents us from creating two kinds of accounts – one for the registered users, and another one for anonymous buyers. Registered user account would have to check the balance before paying the bill. Physical user’s account would be just a placeholder and it would assume that the cashier has counted the money before issuing the receipt.
In other words, the message from this example is that you should not model certain natural things as null references, but rather as specialized objects. When done right, no null references would be allowed to enter your methods. As a consequence, all the if-then-else logic based on null checks will go away, meaning that the overall complexity of the code would decrease.
The first step towards solving the problem is to recognize that there are two kinds of buyers – registered users and anonymous buyers. So we need something that makes them the same in terms of buying stuff. That will be the common interface they will both implement:
public interface IBuyer
{
IReceiptViewModel Purchase(IProduct item);
}
public interface IRegisteredUser: IBuyer
{
string UserName { get; }
void SetReferrer(IRegisteredUser referrer);
void ReferralAdded();
}
Observe that now we have two levels of interfaces. IBuyer interface deals with purchases, and that is what all buyers can do. IRegisteredUser interface extends IBuyer and adds registration-related features. This lets us neatly segregate registration from buying in the domain services.
Now the User class will become the registered user implementation:
public class User: IRegisteredUser
{
...
public IReceiptViewModel Purchase(IProduct item)
{
IProduct discountedItem = item.ApplyDiscounts(this.discounts);
this.RegisterPurchase(discountedItem.Price);
return new ReceiptDto(this, discountedItem.Name, discountedItem.Price);
}
public void SetReferrer(IRegisteredUser referrer)
{
this.referrer = referrer;
referrer.ReferralAdded();
}
private void RegisterPurchase(decimal price)
{
this.totalPurchases += price;
if (!hasReceivedLoyaltyDiscount && this.totalPurchases > 100.0M)
{
this.discounts.Add(new Discount(0.05M));
this.hasReceivedLoyaltyDiscount = true;
}
}
public void ReferralAdded()
{
this.discounts.Add(new Discount(.02M));
}
}
As you can see, registered user executes quite a bit of domain logic, primarily related to discounts. On the other hand, anonymous buyer would not be eligible for these kinds of discounts, and also cannot deal with referrers. Anonymous buyer is just a buyer:
public class AnonymousBuyer: IBuyer
{
public IReceiptViewModel Purchase(IProduct item)
{
return new ReceiptDto(this, item.Name, item.Price);
}
}
Anonymous buyer has no features. It just buys stuff and goes away.
The next thing we have to settle is the account. Registered users come with registered accounts. Anonymous buyers come with cash (or cards).
To make both payment methods the same, we can just define an interface they will both implement:
public interface IAccount
{
IMoneyTransaction Deposit(decimal amount);
IMoneyTransaction Withdraw(decimal amount);
}
Now, the registered account contains quite some logic, primarily to deal with the account balance:
public class Account: IAccount
{
public IMoneyTransaction Deposit(decimal amount)
{
MoneyTransaction transaction = new MoneyTransaction(amount);
this.transactions.Add(transaction);
Log(string.Format("{0} deposited ${1:0.00} - balance ${2:0.00}",
this.UserName, amount, this.Balance));
return transaction;
}
public IMoneyTransaction Withdraw(decimal amount)
{
if (this.Balance >= amount)
{
MoneyTransaction transaction = new MoneyTransaction(-amount);
this.transactions.Add(transaction);
this.Log(string.Format("{0} withdrew ${1:0.00} - balance ${2:0.00}",
this.UserName, amount, this.Balance));
return transaction;
}
return null;
}
private decimal Balance
{
get
{
return
this.transactions
.Select(tran => tran.Amount)
.DefaultIfEmpty(0.0M).Sum();
}
}
...
}
This implementation accepts all deposits, but withdrawals may be rejected if balance is lower than the requested amount.
On the other hand, anonymous buyer would deal with infinite supply of cash:
public class InfiniteCash: IAccount
{
public IMoneyTransaction Deposit(decimal amount)
{
return new MoneyTransaction(amount);
}
public IMoneyTransaction Withdraw(decimal amount)
{
return new MoneyTransaction(-amount);
}
}
This is all it does – all deposits and withdrawals are just accepted.
In this way, we have made the definition of a buyer and an account more abstract. Domain services should not bother with details such as whether the buyer has previously registered or not. Instead, domain services should keep focused on the process of buying things – searching for an item, withdrawing money from the buyer, producing a receipt.
All the operations are now exposing polymorphic behavior and every particular kind of user is free to provide specific implementation. Domain services will be fine as long as they can carry on the purchase using only the abstract interface of the underlying domain model.
Now we can finally get back to the Purchase method in the domain services. This method used to make distinction between registered and anonymous buyers by letting the username be null. This time, we have developed a proper domain model for both kinds of buyers. All it takes is to use them in context of buying things.
Therefore, we will define two methods in the domain services:
public interface IDomainServices
{
...
IReceiptViewModel Purchase(string userName, string itemName);
IReceiptViewModel AnonymousPurchase(string itemName);
}
The first method will receive a mandatory username and it will obviously deal with registered users. The second method omits username, but through its name it communicates that it deals with anonymous buyers.
Implementation in the domain services is straight-forward:
public class DomainServices: IDomainServices
{
private readonly IUserRepository userRepository;
private readonly IProductRepository productRepository;
private readonly IAccountRepository accountRepository;
public IReceiptViewModel Purchase(string userName, string itemName)
{
IRegisteredUser user = this.userRepository.Find(userName);
if (user == null)
return new InvalidUser(userName);
IAccount account = this.accountRepository.FindByUser(user);
return this.Purchase(user, account, itemName);
}
public IReceiptViewModel AnonymousPurchase(string itemName)
{
return this.Purchase(new AnonymousBuyer(), new InfiniteCash(), itemName);
}
private IReceiptViewModel Purchase(IBuyer buyer, IAccount account, string itemName)
{
Product item = this.productRepository.Find(itemName);
if (item == null)
return new OutOfStock(buyer, itemName);
IReceiptViewModel receipt = buyer.Purchase(item);
IMoneyTransaction transaction = account.Withdraw(item.Price);
if (transaction == null)
return new InsufficientFunds(buyer, item.Price, itemName);
return receipt;
}
...
}
There is no more if-then-else logic to select the buying scenario. All buyers are the same when it comes to acquiring things – that is what the private Purchase method is responsible for. Buying an item has just become one polymorphic operation. Different kinds of buyers behave differently, but they buy stuff the same way.
Making all operations unconditional means that we have reduced cyclomatic complexity in the domain services. Operations have been moved out from branching statements into specialized classes, which are then invoked unconditionally.
In this article we have identified a problem with branching based on null method parameters. Whenever we encounter an if-then-else statement asking whether a parameter is null, we can be sure that we have missed a class.
Branch when parameter is non-null is supposedly executed on the object pointed to by the non-null reference. Alternate branch, taken when the parameter is null, mimics the previous operation but this time without the object. Code used to mimic the operation in negative case is actually the code that should fit into another class.
If-then-else would then boil down to asking whether to invoke the method on one object or the other. But since we already have the object, the branching statement can easily be dropped and replaced with unconditional call to the method of the object at hand.
In that way, creeping polymorphism embodied in the if-then-else statement is raised above and displayed explicitly. There are no more positive and negative branches. There are only objects of one class or the other.
Remember – branching upon null method parameter means that there is certain polymorphic behavior in our domain which was not recognized. Need a solution? Recognize polymorphism and make it explicit.
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.