by Zoran Horvat
In the previous article in this series we have demonstrated one example in which the code could be simplified and its cyclomatic complexity reduced, by removing the collection from a class and wrapping it in a dedicated class. Composite design pattern provided means of moving the collection into another object (see How to Reduce Cyclomatic Complexity: Working With Collections for details). That was more of an academic example, and in that is why we will revisit the Composite design pattern idea in this article again, this time in a more realistic setting.
In this series of texts on techniques to reduce cyclomatic complexity we have been refactoring one e-commerce application. One of the requirements for this application was that users can achieve discounts. When user spends more than $100, she is eligible for a 5% discount on all subsequent purchases. When user brings in another user, she receives a 2% discount for all subsequent purchases.
In the first implementation, User class was responsible to keep record of the discounts:
namespace Store.Domain.Implementation
{
public class User: IRegisteredUser
{
private decimal totalPurchases;
private bool hasReceivedLoyaltyDiscount;
private IList<IDiscount> discounts = new List<IDiscount>();
public IReceiptViewModel Purchase(IProduct item)
{
IProduct discountedItem = item.ApplyDiscounts(this.discounts);
this.RegisterPurchase(discountedItem.Price);
return new ReceiptDto(this, discountedItem.Name, discountedItem.Price);
}
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 from this implementation, almost all the logic regarding discounts is concentrated inside the User class. The only part of the logic that remains is applying discounts to reach a lower price of an item. That piece of code was put in the Product class:
namespace Store.Domain.Implementation
{
public class Product: IProduct
{
public decimal Price { get; private set; }
...
public IProduct ApplyDiscounts(IEnumerable<IDiscount> discounts)
{
decimal price = this.Price;
foreach (IDiscount discount in discounts)
price = discount.Apply(price);
return new Product(this.Name)
{
Price = price
};
}
}
}
ApplyDiscounts method iterates through the discounts and applies them in a row. The price reached in the end is the discounted price.
Note one important detail in this implementation. Product class takes care about the case when discounts collection is empty. This is extremely important detail. We certainly wouldn’t like to have Product class explode when user is not eligible for any discount. What this method does is to return actual price of the product when no discount is available.
This part of code, both in the User and in the Product classes, can now be subjected to a refactoring. Goal of this refactoring is to move collection of discounts into a separate class which implements IDiscount abstract discount interface.
Very often in practice, it is not easy to just pull the collection out and replace it with the composite element. Some infrastructure work needs to precede it to make composite fit more naturally.
In the e-commerce application, the first refactoring step was to move out the logic which decides when the user is eligible for a certain discount (see How to Reduce Cyclomatic Complexity: Domain Logic in Factories for details.) After this logic was moved out, the User class became much easier to follow:
namespace Store.Domain.Implementation
{
public class User : IRegisteredUser
{
public string UserName { get; private set; }
private IList<IDiscount> discounts = new List<IDiscount>();
private DiscountRules discountRules;
public User(string userName)
{
this.UserName = userName;
this.discountRules = new DiscountRules();
}
public IReceiptViewModel Purchase(IProduct item)
{
IProduct discountedItem = item.ApplyDiscounts(this.discounts);
this.RegisterPurchase(discountedItem.Price);
return new ReceiptDto(this, discountedItem.Name, discountedItem.Price);
}
private void RegisterPurchase(decimal price)
{
this.discountRules
.MoneySpent(price)
.TryCreate()
.Each(discount => this.discounts.Add(discount));
}
private void ReferralAdded()
{
this.discountRules
.ReferralAdded()
.TryCreate()
.Each(discount => this.discounts.Add(discount));
}
...
}
}
After this modification, we are ready to use the Composite design pattern to simplify the code.
The first step is to make the Product class expect a single abstract discount instead of the collection:
namespace Store.Domain.Implementation
{
public class Product: IProduct
{
public decimal Price { get; private set; }
...
public IProduct Apply(IDiscount discount)
{
return new Product(this.Name)
{
Price = discount.Apply(this.Price)
};
}
}
}
This is serious simplification, because the product doesn’t have to worry about how the discounts are supposed to be combined. Some discounts could exclude each other, for example. Product class didn’t take this kind of business rules into account. With abstract discount defined, Product is free to deal with its own responsibilities and leave discounts logic to someone else.
The next step is to extend the IDiscount interface to support adding more discounts to the collection:
namespace Store.Application
{
public interface IDiscount
{
decimal Apply(decimal price);
IDiscount CombineWith(IDiscount discount);
}
}
This is an example where domain logic finds its way to the abstract interface. Abstract discount is now aware of such concept as combining two discounts into one. Previously, while User and Product classes used to control discounts, discount itself was oblivious of the fact that it could be combined with some other discounts.
Final preparation step is to make sure that User only contains one discount:
namespace Store.Domain.Implementation
{
public class User : IRegisteredUser
{
private IDiscount discount;
private DiscountRules discountRules;
public IReceiptViewModel Purchase(IProduct item)
{
IProduct discountedItem = item.Apply(this.discount);
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.AddDiscount(
this.discountRules
.MoneySpent(price));
}
private void ReferralAdded()
{
this.AddDiscount(
this.discountRules
.ReferralAdded());
}
private void AddDiscount(IDiscountFactory discountFactory)
{
this.discount =
discountFactory
.TryCreate()
.Select(newDiscount => this.discount.CombineWith(newDiscount))
.DefaultIfEmpty(this.discount)
.Single();
}
}
}
With this change, all elements in the system are aware of only one discount. This discount is responsible to combine simple discounts when it comes to combining them.
We are finally ready to implement the concrete composite element.
Now it only remains to provide concrete implementations of the IDiscount interface. These concrete implementations should cover all the use cases, including the one when there are multiple discounts assigned to the same user.
The simplest case we have to support is when the user doesn’t have any discounts:
namespace Store.Domain.Implementation
{
internal class NoDiscount: IDiscount
{
public decimal Apply(decimal price)
{
return price;
}
public IDiscount CombineWith(IDiscount discount)
{
return discount;
}
}
}
This class makes no changes to the product to which it is applied. Price remains the same. When combined with any other discount, that other discount is the sole result. This means that this empty discount doesn’t interfere with other discounts either.
The second case we have to cover is a simple discount, one that just does the real work of reducing the price:
namespace Store.Domain.Implementation
{
public class Discount: IDiscount
{
private decimal relativeDiscount;
public Discount(decimal relativeDiscount)
{
this.relativeDiscount = relativeDiscount;
}
public decimal Apply(decimal price)
{
return price * (1.0M - this.relativeDiscount);
}
public IDiscount CombineWith(IDiscount discount)
{
return new ComplexDiscount(new IDiscount[] { this, discount });
}
}
}
When it comes to combining the simple discount with another discount, this object augments itself to a ComplexDiscount instance. This is how we can add more than one discount to the system. Simple discount would just return a complex discount as the result.
Final class which concludes the Composite design pattern implementation in this project is the ComplexDiscount:
namespace Store.Domain.Implementation
{
internal class ComplexDiscount : IDiscount
{
private IEnumerable<IDiscount> discounts;
public ComplexDiscount(IEnumerable<IDiscount> discounts)
{
this.discounts = new List<IDiscount>(discounts);
}
public decimal Apply(decimal price)
{
return this.discounts
.Aggregate(price, (curPrice, discount) => discount.Apply(curPrice));
}
public IDiscount CombineWith(IDiscount discount)
{
return new ComplexDiscount(
this.discounts
.Union(new IDiscount[] { discount }));
}
}
}
This class doesn’t apply discounts to prices. But it does all the work of combining multiple discounts to a single price. Net result is that now we have three classes that cover three features – having no discounts, having one discount, and having several discounts that have to be combined to produce a final price.
Final touch in this solution is to properly initialize the User class:
namespace Store.Domain.Implementation
{
public class User : IRegisteredUser
{
public string UserName { get; private set; }
private IDiscount discount;
private IBuyer referrer;
private DiscountRules discountRules;
public User(string userName)
{
this.UserName = userName;
this.discount = new NoDiscount();
this.discountRules = new DiscountRules();
}
...
}
}
In the constructor, User class initializes its discount to NoDiscount instance. This perfectly models the business rule that user initially has no discounts. Any discounts that could be added to the user are combined with this initial discount to produce any list or even hierarchy of discounts that comes to be.
In this article we have seen one realistic applications of the Composite design pattern. When Composite is applied, scaffolding code is wrapped in a class which we normally never read after it is written. Domain-related code remains in domain classes, relieved from the scaffolding code, and therefore easier to read and to understand.
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.