The Open/Closed Principle states that modules (classes, methods etc.) should be open for extension but closed for modification. What does it mean? It means that new behavior can be added in the future and no changes to the existing source code will be required.

How to change behavior without changing the source code? Rely on abstractions!
Example
Let’s consider a Cart class with TaxAmount method. Tax is calculated on the basis of a product category: 25% for alcohol, 5% for books and a special rate of 30% but not less than $10 per item for chosen products.
Implementation of this class that breaks OCP could look like this:
public class Cart
{
private readonly List OrderItem _items = new List OrderItem ();
public IEnumerable Items => _items;
public void Add(OrderItem orderItem)
{
_items.Add(orderItem);
}
public decimal TaxAmount()
{
decimal totalTax = 0m;
foreach (OrderItem orderItem in Items)
{
switch (orderItem.Category)
{
case Categories.Alcohol:
totalTax += orderItem.PriceNet * TaxRates.Alcohol;
break;
case Categories.Books:
totalTax += orderItem.PriceNet * TaxRates.Books;
break;
case Categories.Special:
//tax rate but not less than MinimumSpecialTax
decimal itemTax = orderItem.PriceNet * TaxRates.Special;
totalTax += Math.Max(itemTax, TaxRates.MinimumSpecialTax);
break;
default:
throw new ArgumentOutOfRangeException();
}
// more rules are coming!
}
return totalTax;
}
}
What if some new tax rates come up? TaxAmount method will have to be modified every time! Each change can introduce bugs and needs retesting. It is much better to introduce new classes for new functionalities than to modify existing code. New classes have nothing depending on them yet and no legacy coupling.
What to do?
We create an abstraction- the ITaxRule interface and extract each tax rule to a separate class. Every tax rule class has its own implementation of IsMatch and CalculateTax methods. (See also: Strategy Pattern)
public interface ITaxRule
{
bool IsMatch(OrderItem item);
decimal CalculateTax(OrderItem item);
}
[TaxRuleActivatorAttribute(IsActive = true)]
public class BooksTaxRule : ITaxRule
{
public bool IsMatch(OrderItem item)
{
return item.Category == Categories.Books;
}
public decimal CalculateTax(OrderItem item)
{
return item.PriceNet * TaxRates.Books;
}
}
[TaxRuleActivatorAttribute(IsActive = true)]
public class SpecialTaxRule : ITaxRule
{
public bool IsMatch(OrderItem item)
{
return item.Category == Categories.Special;
}
public decimal CalculateTax(OrderItem item)
{
//tax rate but not less than MinimumSpecialTax
return Math.Max(item.PriceNet * TaxRates.Special, TaxRates.MinimumSpecialTax);
}
}
We create TaxCalculator class which calculates tax for a given cart item according to its category. In the constructor we use reflection to find all non- abstract classes in the assembly that implement ITaxRule interface. That will be the set of tax rules used for the calculation.
public class TaxCalculator : ITaxCalculator
{
private readonly List _taxRules = new List();
public TaxCalculator()
{
//EDITED: see comments under the post
//_taxRules = new List {new BooksTaxRule(), new AlcoholTaxRule(), new SpecialTaxRule()};
Type[] typesInThisAssembly = Assembly.GetExecutingAssembly().GetTypes();
List[Type] activeTaxRuleTypes = (from t in typesInThisAssembly
where t.GetInterface(typeof(ITaxRule).ToString()) != null
&& !t.IsAbstract
let attrs = t.GetCustomAttributes(true).OfType[TaxRuleActivatorAttribute]
where attrs.Any(a => a.IsActive == true)
select t).ToList();
activeTaxRuleTypes.ForEach(t => _taxRules.Add(Activator.CreateInstance(t) as ITaxRule));
}
public decimal CalculateTax(OrderItem item)
{
ITaxRule taxRule = _taxRules.FirstOrDefault(r => r.IsMatch(item));
if (taxRule == null) throw new ArgumentException("Order item of unknown tax category");
return _taxRules.First(r => r.IsMatch(item)).CalculateTax(item);
}
}
In case some class is added to the solution but is not to be included in the calculation we will take into account only those types that are marked active. To do this we use the custom attribute TaxRuleActivatorAttribute and its IsActive property.
[AttributeUsage(AttributeTargets.Class)]
class TaxRuleActivatorAttribute : Attribute
{
public bool IsActive { get; set; }
}
Finally, we modify Cart class. Note the difference between the previous TaxAmount method and CalculateTotalTaxAmount below.
public class Cart
{
private readonly List _items = new List();
private readonly ITaxCalculator _taxCalculator;
public Cart() : this(new TaxCalculator())
{
}
public Cart(ITaxCalculator taxCalculator)
{
_taxCalculator = taxCalculator;
}
public IEnumerable Items => _items;
public void Add(OrderItem orderItem)
{
_items.Add(orderItem);
}
public decimal CalculateTotalTaxAmount()
{
return Items.Sum(orderItem => _taxCalculator.CalculateTax(orderItem));
// more rules are coming!
}
}
Now, if some new tax rates are added CalculateTotalTaxAmount stays the same! New tax rules come up as additional classes.
References
SOLID Principles of Object Oriented Design – Pluralsight Online Training – Steve Smith
“Clean Code: A Handbook of Agile Software Craftsmanship” – Robert C. Martin
The Open-Closed Principle Clean Code, Episode 10 By Uncle Bob
But the problem has only been moved to TaxCalculator class where you have to add new ITaxRules. So even if you don’t change the Cart class, you have to update TaxCalculator. Is this ok with OCP?
LikeLiked by 1 person
You are right, Romek. Thanks for the comment. 🙂 What I think we can do here instead of
_taxRules = new List {new BooksTaxRule(), new AlcoholTaxRule(), new SpecialTaxRule()};
in the TaxCalculator constructor is to:
– use reflection to find all non-abstract ITaxRule types,
– use attributes to mark if particular ITaxRule class is included in the calculation,
– fill the _taxRules list with instances of types found.
I edited the post and added this part of code. I omitted try-catch blocks for brevity. Take a look.
LikeLike
The reflection is not really suitable in this place either – the code is still strongly linked to types existing in an assembly. Semantically your assembly is your dependency container and the code in constructor is your dependency resolver. As such it should be moved outside of the class and TaxCalculator constructor should have IEnumberable in parameter.
LikeLiked by 1 person
* IEnumerable of ITaxRule
LikeLike
Hmm, indeed good point. Then probably a TaxRulesFetcher class could contain the reflection part. TaxCalculator class would then have two constructors: one with IEnumerable of ITaxRule parameter and the default one:
public TaxCalculator () : this(TaxRulesFetcher.FetchTaxRules())
{
}
What do you think about a static method here? Static methods are again some kind of dependency 🙂 although it will not affect tests.
LikeLike
Well, I don’t approve parameterless constructors initializing default implementation of something that should be injected – after all it is nothing else but hardcoded binding to a specific implementation. Initialization should be exclusive responsibility of dependency resolver or anything playing its part.
If creation of instances can’t be configured in dependency container, e.g. because it depends on some service, then you should have some Factory/Provider that can be configured:
public TaxRuleProvider: ITaxRuleProvider
{
public TaxRuleProvider(ISomeService someService)
{
}
public IEnumerable GetTaxRules()
{
if (someService.IsSomething()) {
yield return new Rule3()
} else {
yield return new Rule1()
yield return new Rule2()
}
}
}
public TaxCalculator (ITaxRuleProvider taxRuleProvider)
{
}
LikeLike
We can remove the offending constructor but without reflection your GetTaxRules method breaks OCP.
LikeLike
I don’t agree it breaks OCP ;). If you need you can always replace TaxRateProvider with a different implementation of ITaxRateProvider.
In the simplest scenario (use all tax rules in assembly), you should have only this:
public TaxCalculator (IEnumerable taxRules)
No reflection is needed, but your DC should be configured to provide list of tax rules when required to construct TaxCalculator. It can be a hardcoded list or IoC container can internally use reflection to get all implementations found in assembly (most IoCC can do this), but you don’t really care about this when you implement TaxCalculator.
The need for ITaxRateProvider arises only when you require more complicated initialization logic, that can’t be handled by DC. When you implement such logic you usually have to know exact class name, but if you don’t you can still have:
public TaxRuleProvider(ISomeService someService, IEnumerable allTaxRules)
and have some generic implementation of GetTaxRules().
In either case you shouldn’t need to write reflection code unless you’re writing your own IoCC.
LikeLike