The Open/Closed Principle in C# with Filters and Specifications
Tags: dotnet, designpatterns, csharp
Published: 2024-07-11 01:34:25


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:

```csharp
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.

```csharp
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.

```csharp
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.

```csharp
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.

```csharp
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:

```csharp
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.