Software design principles are fundamental in ensuring our code remains maintainable, scalable, and robust. One of the key principles in the SOLID design principles is the Open/Closed Principle (OCP). This principle states that software entities should be open for extension but closed for modification. Let’s explore how we can adhere to this principle through a practical example involving product filtering.
Initial Implementation: The Problem
Imagine we have a simple product catalog where each product has a name, color, and size. We need a way to filter these products based on various criteria. A straightforward implementation might look like this:
public enum Color
{
Red, Green, Blue
}
public enum Size
{
Small, Medium, Large, Yuge
}
public class Product
{
public string Name;
public Color Color;
public Size Size;
public Product(string name, Color color, Size size)
{
Name = name ?? throw new ArgumentNullException(paramName: nameof(name));
Color = color;
Size = size;
}
}
public class ProductFilter
{
public IEnumerable<Product> FilterByColor(IEnumerable<Product> products, Color color)
{
foreach (var p in products)
if (p.Color == color)
yield return p;
}
public static IEnumerable<Product> FilterBySize(IEnumerable<Product> products, Size size)
{
foreach (var p in products)
if (p.Size == size)
yield return p;
}
public static IEnumerable<Product> FilterBySizeAndColor(IEnumerable<Product> products, Size size, Color color)
{
foreach (var p in products)
if (p.Size == size && p.Color == color)
yield return p;
}
}
While this implementation works, it’s easy to see how it can quickly become unmanageable. Each new filter criterion or combination of criteria requires a new method. This approach violates the Open/Closed Principle because the ProductFilter
class needs to be modified each time a new filtering requirement is introduced.
Refactoring with OCP: The Solution
To adhere to the Open/Closed Principle, we need a way to extend our filtering functionality without modifying the existing code. We can achieve this by using the Specification pattern, which allows us to define criteria in a reusable and combinable way.
Step 1: Define Interfaces
First, we define two interfaces: one for specifications and one for filters.
public interface ISpecification<T>
{
bool IsSatisfied(T item);
}
public interface IFilter<T>
{
IEnumerable<T> Filter(IEnumerable<T> items, ISpecification<T> spec);
}
Step 2: Implement Specifications
Next, we implement concrete specifications for color and size.
public class ColorSpecification : ISpecification<Product>
{
private Color color;
public ColorSpecification(Color color)
{
this.color = color;
}
public bool IsSatisfied(Product p)
{
return p.Color == color;
}
}
public class SizeSpecification : ISpecification<Product>
{
private Size size;
public SizeSpecification(Size size)
{
this.size = size;
}
public bool IsSatisfied(Product p)
{
return p.Size == size;
}
}
Step 3: Combine Specifications
We can also create composite specifications to combine multiple criteria.
public class AndSpecification<T> : ISpecification<T>
{
private ISpecification<T> first, second;
public AndSpecification(ISpecification<T> first, ISpecification<T> second)
{
this.first = first ?? throw new ArgumentNullException(paramName: nameof(first));
this.second = second ?? throw new ArgumentNullException(paramName: nameof(second));
}
public bool IsSatisfied(T item)
{
return first.IsSatisfied(item) && second.IsSatisfied(item);
}
}
Step 4: Implement the Better Filter
Finally, we implement the BetterFilter
class that uses the specifications to filter products.
public class BetterFilter : IFilter<Product>
{
public IEnumerable<Product> Filter(IEnumerable<Product> items, ISpecification<Product> spec)
{
foreach (var i in items)
if (spec.IsSatisfied(i))
yield return i;
}
}
Demonstration: Putting It All Together
Here’s a demonstration of how to use the refactored filtering system:
public class Demo
{
static void Main(string[] args)
{
var apple = new Product("Apple", Color.Green, Size.Small);
var tree = new Product("Tree", Color.Green, Size.Large);
var house = new Product("House", Color.Blue, Size.Large);
Product[] products = { apple, tree, house };
var pf = new ProductFilter();
WriteLine("Green products (old):");
foreach (var p in pf.FilterByColor(products, Color.Green))
WriteLine($" - {p.Name} is green");
var bf = new BetterFilter();
WriteLine("Green products (new):");
foreach (var p in bf.Filter(products, new ColorSpecification(Color.Green)))
WriteLine($" - {p.Name} is green");
WriteLine("Large products:");
foreach (var p in bf.Filter(products, new SizeSpecification(Size.Large)))
WriteLine($" - {p.Name} is large");
WriteLine("Large blue items:");
foreach (var p in bf.Filter(products, new AndSpecification<Product>(new ColorSpecification(Color.Blue), new SizeSpecification(Size.Large))))
WriteLine($" - {p.Name} is big and blue");
}
}
Conclusion
By applying the Open/Closed Principle through the Specification pattern, we created a flexible and maintainable filtering system. The BetterFilter
class is open for extension through new specifications but closed for modification, as we no longer need to change its implementation to add new filtering criteria.
This approach not only adheres to SOLID principles but also enhances the scalability and readability of our code, making it easier to maintain and extend in the future.
Source link
lol