DEV Community

mohamed Tayel
mohamed Tayel

Posted on • Edited on

Specification Pattern in .NET: Applying the Open/Closed Principle

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode
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;
    }
}
Enter fullscreen mode Exit fullscreen mode
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);
    }
}
Enter fullscreen mode Exit fullscreen mode
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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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");
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (1)

Collapse
 
moh_moh701 profile image
mohamed Tayel