by Zoran Horvat
In the previous article in this series we have discussed use of factory methods in the domain class (see How to Reduce Cyclomatic Complexity: Switchable Factory Methods for details). Example shown there exhibits one feature which was not mentioned at that point, and that is precisely what we will discuss in this article. Namely, factory methods contained a bit of domain logic.
Was that a good decision, or should we pay more attention to such detail? That question will be addressed in this article.
Abstract factory design pattern says that the client should be decoupled from creation of a concrete product. Instead of relying directly on a concrete product, client depends on abstract factory which returns abstract product. Concrete product merely implements the interface defined by the abstract product.
Basically, abstract factory defines an interface which tells us which data we have to provide in order to get an instance that implements the abstract product interface. Admittedly, there is a certain quantity of domain logic in that. Abstract product interface is taken from the domain. Arguments to the factory method are also meaningful in the domain.
Therefore, we can say that abstract factory interface is tied to the domain. But interface is more general than its implementation.
As an experiment, take one concrete decision that could be made by the factory. For instance, abstract factory creates a bank account object, but some bank accounts could be frozen due to inactivity. Who makes the decision to freeze the account object?
Is it a concrete factory which does that? That would mean that the concrete factory knows the business rule about account freezing. Concrete factory would have to know after how many days the account is frozen.
Or is the concrete factory supposed to just return an account object and then the caller is free to freeze the object? In this case, the caller would have to be informed about this particular business rule.
Both solutions seem to be equally appealing. And that is not the real issue - we choose one way or the other and just implement it. The problem is that abstract factory interface does not tell us which design decision was made when the concrete factory was coded.
Therefore, we should be cautious when putting domain logic into concrete factory. Main target here is to properly define interface of the abstract factory. When done right, callers will not be confused about what object the factory will return to them. We will see this on an example.
We will return to the e-commerce application we were refactoring along this series of texts on cyclomatic complexity. One of the requirements for the application is that the registered user receives a discount after spending more than $100. In all subsequent purchases, the user would receive a 5% loyalty discount.
Not only that the discount object used to be created inside the User object, User object also had to keep evidence and make sure that the loyalty discount is assigned exactly once. Here is the implementation of the User class:
namespace Store.Domain.Implementation
{
public class User: IRegisteredUser
{
public string UserName { get; private set; }
private decimal totalPurchases;
private IList<IDiscount> discounts = new List<IDiscount>();
private IBuyer referrer;
private Func<Option<IDiscount>> tryCreateLoyaltyDiscount;
public User(string userName)
{
this.UserName = userName;
this.tryCreateLoyaltyDiscount = () => this.CreateLoyaltyDiscountIfFulfilled();
}
...
private void RegisterPurchase(decimal price)
{
this.totalPurchases += price;
this.TryAssignLoyaltyDiscount();
}
private void TryAssignLoyaltyDiscount()
{
this.tryCreateLoyaltyDiscount()
.Each(discount =>
{
this.discounts.Add(discount);
this.tryCreateLoyaltyDiscount = () => Option<IDiscount>.CreateEmpty();
});
}
private Option<IDiscount> CreateLoyaltyDiscountIfFulfilled()
{
if (this.totalPurchases > 100.0M)
return Option<IDiscount>.Create(new Discount(0.05M));
return Option<IDiscount>.CreateEmpty();
}
}
}
This is quite a lot of code to deal with a single type of discount. We want to move it out to a separate class in order to relieve the User class from having to maintain this logic. User class should focus on primary responsibilities.
We can move the loyalty discount logic out of the User class. One possibility is to define an abstract factory for that purpose:
public interface IDiscountFactory
{
IDiscount Create();
}
Concrete factory would then deal with loyalty discounts:
public class LoyaltyDiscountFactory : IDiscountFactory
{
public IDiscount Create()
{
return new Discount(0.05M);
}
}
This is by-the-book solution and it would definitely work in our application. But there is a problem with this approach. Since this concrete factory is devoid of domain logic, it doesn't know whether the user is eligible for the discount or not.
If we wanted to use this concrete factory, we would still have to implement the domain logic in the same way as it is implemented now. Introducing such a simple abstract factory to the code base brings only a minor value. To really simplify the code, we need something that to contain the domain logic.
We can enhance the discount factory to include eligibility tests. If user is eligible for the discount, the factory would create the discount object. Otherwise, the factory would not create the object.
But wait a minute. Abstract factory has an interface which indicates that it creates an object whenever called. Typical abstract factory does not support the feature to not create an object when asked to create one.
So here we face the first trace of domain logic. Concrete factory will be responsible to test eligibility. On the other hand, abstract factory must communicate that feature:
public interface ILoyaltyDiscountFactory
{
void RegisterPurchase(decimal amount);
Option<IDiscount> TryCreate();
}
public class LoyaltyDiscountFactory : ILoyaltyDiscountFactory
{
private decimal totalPurchases = 0;
private Func<Option<IDiscount>> tryCreateLoyaltyDiscount;
public LoyaltyDiscountFactory()
{
this.tryCreateLoyaltyDiscount = () => this.CreateLoyaltyDiscountIfFulfilled();
}
public void RegisterPurchase(decimal amount)
{
totalPurchases += amount;
}
public Option<IDiscount> TryCreate()
{
return this.tryCreateLoyaltyDiscount();
}
private Option<IDiscount> CreateLoyaltyDiscountIfFulfilled()
{
if (this.totalPurchases > 100.0M)
{
this.tryCreateLoyaltyDiscount = () => Option<IDiscount>.CreateEmpty();
return Option<IDiscount>.Create(new Discount(0.05M));
}
return Option<IDiscount>.CreateEmpty();
}
}
This time, factory is responsible to keep track of purchases and then to decide whether to create the discount object or not. This added responsibility is reflected in the abstract factory interface as well. What we have here is the example of abstract factory which has to be changed when domain logic is moved to the factory.
Client to this factory class then gets significantly simpler:
public class User : IRegisteredUser
{
public string UserName { get; private set; }
private IList<IDiscount> discounts = new List<IDiscount>();
private IBuyer referrer;
private ILoyaltyDiscountFactory loyaltyDiscountFactory;
public User(string userName)
{
this.UserName = userName;
this.loyaltyDiscountFactory = new LoyaltyDiscountFactory();
}
...
private void RegisterPurchase(decimal price)
{
this.loyaltyDiscountFactory.RegisterPurchase(price);
this.loyaltyDiscountFactory
.TryCreate()
.Each(discount => this.discounts.Add(discount));
}
}
This time, all the testing is performed inside the concrete factory. The only requirement for the User class is to call the RegisterPurchase on the concrete factory before letting it create the discount object.
When abstract factory is used in this way, we quickly get into problems. One problem is that the client (User class in this case) has to know the calling protocol before receiving the loyalty discount. User must first call the RegisterPurchase method on the factory object and only after that to call its TryCreate method, which optionally delivers the discount.
Calling protocols are generally a bad thing. If programmer forgets to make a call or just misunderstands the interface, the code will contain a bug. It would be much better if we could design such factory class which leads the caller through the process so that it cannot skip this mandatory step. The best protection from protocol violations is the compiler. We want to have a class which fails to build if it didn't follow the mandatory protocol.
The next drawback of this approach becomes apparent when another discounts comes into play. User object supports referral discounts as well. Whenever a user brings in another customer, she receives a 2% discount for all further purchases.
Unfortunately, abstract factory which creates the loyalty discount is tightly bound to that kind of discounts. It looks like we would have to define another abstract factory for the purpose of assigning referral discount. But that defies the idea of abstract factories - in that way we would have as many abstract factories as concrete products, which totally misses the point.
Bottom line is that it looks like domain logic should not be pushed into factories as-is. Something else should be done to prepare the solution.
One way to resolve issues discussed above is to let the abstract factory remain simple and generally applicable. On the other hand, rules such as those that decide whether a user is eligible for a discount, should not be returned back into the client. Remember, simplifying the client was the original goal of this refactoring.
The idea then comes by itself. We need a class which only holds business rules. This class would then let the client use appropriate concrete factory to obtain a concrete product it desires.
Abstract factory would still be able to choose whether to create the concrete product or not. That is the last remnant of domain logic in the factory. In the end, interface will look like this:
namespace Store.Application
{
public interface IDiscountFactory
{
Option<IDiscount> TryCreate();
}
}
On the other hand, domain layer will define concrete factories which can be used in all scenarios where discounts are generated.
namespace Store.Domain.Implementation
{
internal class LoyaltyDiscountFactory: IDiscountFactory
{
public Option<IDiscount> TryCreate()
{
return Option<IDiscount>.Create(new Discount(0.05M));
}
}
}
This factory class can be used to create a loyalty discount. It possesses a bit of domain knowledge, namely that this is a 5% discount with no other constraints. Notice that this concrete factory is declared as internal class. Nobody outside the domain layer needs it - everyone else are supposed to depend on abstract factory interface only.
namespace Store.Domain.Implementation
{
internal class ReferralDiscountFactory: IDiscountFactory
{
public Option<IDiscount> TryCreate()
{
return Option<IDiscount>.Create(new Discount(0.02M));
}
}
}
This is the referral discount factory. Once again, we have a fixed implementation with only a tiny bit of domain knowledge. The class is declared internal because it will never be referred directly outside of the domain layer.
These two concrete factories are simple and straight-forward, but they do not justify the decision to return Option type from the factory method. Here comes the third implementation, which tells the other side of the story:
namespace Store.Domain.Implementation
{
internal class NoDiscountFactory: IDiscountFactory
{
public Option<IDiscount> TryCreate()
{
return Option<IDiscount>.CreateEmpty();
}
}
}
With this third concrete factory defined, we possess the ability to return empty option when business rules say that the discount should not be assigned to a user.
At this point we have abstract and concrete factories defined. These are also manageable, because they are letting us return an option which does or does not contain a discount. Finally, we only need a class which would choose appropriate concrete factory for each of the business situations. This class will be called DiscountRules and here is its complete listing:
namespace Store.Domain.Implementation
{
internal class DiscountRules
{
private decimal totalPurchases;
private Func<IDiscountFactory> loyaltyFactorySelector;
public DiscountRules()
{
this.loyaltyFactorySelector = this.LoyaltySelectorUntilFulfilled;
}
public IDiscountFactory MoneySpent(decimal amount)
{
this.totalPurchases += amount;
return this.loyaltyFactorySelector();
}
public IDiscountFactory ReferralAdded()
{
return new ReferralDiscountFactory();
}
private IDiscountFactory LoyaltySelectorUntilFulfilled()
{
if (this.totalPurchases > 100.0M)
{
this.loyaltyFactorySelector = () => new NoDiscountFactory();
return new LoyaltyDiscountFactory();
}
return new NoDiscountFactory();
}
}
}
This class has two public methods, and both of them are returning a discount factory (abstract factory, of course).
The first method is invoked when user makes a purchase and it keeps track of all purchases made by the user. Once the user reaches the purchase limit, LoyaltyDiscountFactory is returned and then the user will be able to produce itself a real 5% discount. In all other cases, this method ends up returning a NoDiscountFactory. In that way, two goals are met. User will not receive a discount until he spends enough money. The other goal is that the user will not receive another loyalty discount after one has been issued to him.
The second public method of this class is ReferralAdded. This method will be invoked whenever the user brings in another customer. Note that this method always returns concrete factory which creates referral discounts. But, should the requirement in this respect change in the future, this would be exact spot where the new requirement would be implemented. For example, it could turn up that the user can obtain referral discounts at most three times. Then the counter would be implemented in this particular method.
In the end, it only remains to use this DiscountRules class. The class will be instantiated inside the User class and it would take effect whenever a purchase is made or when a referral appears. Here is the code:
namespace Store.Domain.Implementation
{
public class User : IRegisteredUser
{
public string UserName { get; private set; }
private IList<IDiscount> discounts = new List<IDiscount>();
private IBuyer referrer;
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);
}
public void SetReferrer(IRegisteredUser referrer)
{
this.referrer = referrer;
referrer.ReferralAdded();
}
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));
}
public override string ToString()
{
return this.UserName;
}
}
}
This is the complete source code of the User class. As you can see, the user is now responsible to receive notifications when purchase is attempted and when referrer is reported, and that is all.
All the business rules about summing up purchases and issuing referral discounts are wrapped in another class - DiscountRules. User class only delegates calls to that class and uses the abstract factory returned to issue an appropriate discount.
In this article we have tackled the idea of externalizing the domain logic which creates an object. It may look attractive to move such domain logic to a factory class. But this turns to be a problem, because such factory classes would be tightly bound to particular business rules. There seems to be no way to declare an abstract factory, because abstract factory interface then depends on the scenario in which it is used.
Another approach, which is more promising, is to lock business rules inside a class which only implements the rules. This class would then delegate execution further down the stream to abstract factories. The benefit from this approach is that abstract factory interface would remain simple and generally applicable. It would not depend much on domain logic.
When looking at the implementation, class implementing business rules would make selection of concrete factories. In that way, the factor which varies, and that is creation of objects depending on business rules, would be coded as the varying selection of concrete factories. Once business rules change, only this code which selects concrete factory would change. Client and the factories would remain intact.
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.