DEV Community

mohamed Tayel
mohamed Tayel

Posted on • Edited on

11

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.

Heroku

Amplify your impact where it matters most — building exceptional apps.

Leave the infrastructure headaches to us, while you focus on pushing boundaries, realizing your vision, and making a lasting impression on your users.

Get Started

Top comments (1)

Collapse
 
moh_moh701 profile image
mohamed Tayel

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

👋 Kindness is contagious

Explore a trove of insights in this engaging article, celebrated within our welcoming DEV Community. Developers from every background are invited to join and enhance our shared wisdom.

A genuine "thank you" can truly uplift someone’s day. Feel free to express your gratitude in the comments below!

On DEV, our collective exchange of knowledge lightens the road ahead and strengthens our community bonds. Found something valuable here? A small thank you to the author can make a big difference.

Okay