Introduction
O.C.P : Open closed principle….
an object is open for extension but closed for modification.
So to understand what this means, let's consider the following scenario:
We have a product entity, and we can get lists of these back from our repository. The Requirement for Today: We're asked to create a filtered list of products that match a certain color, so we create the following product filter class, who’s job it is to filter the list.
public class ProductFilter{
public IEnumberable<iproduct> GetItemsMatchingTheColor
( IEnumerable<iproduct> fullList, Color color) {
foreach ( var item in fullList )
if ( item.ItsColor == color);
yield return item;
}
}
That’s fine, and it gets us what we want… but as we know, change is always around the corner…. Requirement a little in the future: we're asked to get list filtered by Product size.
public class ProductFilter{
public IEnumberable<iproduct> GetItemsMatchingTheColor
( IEnumerable<iproduct> fullList, Color color) {
foreach ( var item in fullList )
if ( item.ItsColor == color);
yield return item;
}
public IEnumberable<iproduct> GetItemsMatchingSomeSize
( IEnumerable<iproduct> fullList, ProductSize size) {
foreach ( var item in fullList )
if ( item.Size == size);
yield return item;
}
}
So what did we do here??? well we cracked open the filter object, added the new behavior and rebuilt it. Requirement a little further in the future: we're asked to get list filtered by Product size AND Color.
public class ProductFilter{
public IEnumberable<iproduct> GetItemsMatchingTheColor
( IEnumerable<iproduct> fullList, Color color) {
foreach ( var item in fullList )
if ( item.ItsColor == color);
yield return item;
}
public IEnumberable<iproduct> GetItemsMatchingSomeSize
( IEnumerable<iproduct> fullList, ProductSize size) {
foreach ( var item in fullList )
if ( item.Size == size);
yield return item;
}
public IEnumberable<iproduct> GetItemsMatchingSomeSize
( IEnumerable<iproduct> fullList, ProductSize size, Color color) {
foreach ( var item in fullList )
if ( item.Size == size && item.ItsColor==color);
yield return item;
}
}
So what did we do here??? Well AGAIN we cracked open the filter object, added the new behavior and rebuilt it. Although we managed to extend the behavior of the object, we violated the OCP.
What are the benefits of OCP? Well, by following the principle, we have 2 benefits:
- Objects are extensible which will satisfy changing requirements
- Objects are maintainable and stable, because they didn't change, there’s less chance of introducing bugs.
So how do we do this, how do we extend something without cracking it open? (one possible way)….
Let's consider the design guideline… "encapsulate what varies": in this case the filter specification and the Specification pattern:
public interface ISpecification<t>
{
bool IsSatisfiedBy(T item);
}
public interface IFilter<typetofilter>
{
IEnumerable<typetofilter> Filter(IEnumerable<typetofilter> completelist,
ISpecification<typetofilter> specification);
}
public interface IProduct
{
string Name { get; set; }
Color ItsColor { get; set; }
ProductSize Size { get; set; }
}
public enum ProductSize
{
Small, Medium, Large, ReallyBig
}
Now the refactored Filter object.
public class ProductFilter : IFilter<iproduct>
{ ?
public IEnumerable<iproduct> Filter(IEnumerable<iproduct> completelist,
ISpecification<iproduct> specification)
{
foreach (var product in completelist )
if ( specification.IsSatisfiedBy ( product ))
yield return product;
}
}
Now we have a Product filter object that accepts a complete list of products and specification that we can filter on. All we need to do now is provide the filtering specifications.
#region -[ Specifications ]-
public class ColorFilterSpecification : ISpecification<iproduct>
{
Color colorToMatch;
public ColorFilterSpecification(Color colorToMatch)
{
this.colorToMatch = colorToMatch;
}
public bool IsSatisfiedBy(IProduct item)
{
return item.ItsColor == colorToMatch;
}
}
public class SizeFilterSpecification : ISpecification<iproduct>
{
ProductSize sizeToMatch;
public SizeFilterSpecification(ProductSize sizeToMatch)
{
this.sizeToMatch = sizeToMatch;
}
public bool IsSatisfiedBy(IProduct item)
{
return item.Size == sizeToMatch;
}
}
public class AndSpecification<t> : ISpecification<t>
{
ISpecification<t> first;
ISpecification<t> second;
public AndSpecification(ISpecification<t> first, ISpecification<t> second)
{
this.first = first;
this.second = second;
}
public bool IsSatisfiedBy(T item)
{
return ( first.IsSatisfiedBy(item) && second.IsSatisfiedBy(item));
}
}
public class ORSpecification<t> : ISpecification<t>
{
ISpecification<t> first;
ISpecification<t> second;
public ORSpecification(ISpecification<t> first, ISpecification<t> second)
{
this.first = first;
this.second = second;
}
public bool IsSatisfiedBy(T item)
{
return (first.IsSatisfiedBy(item) || second.IsSatisfiedBy(item));
}
}
#endregion
And to test all this actually works, here’s a very basic set of tests using MSTest built into the Visual Studio.
using System;
using System.Text;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Drawing;
namespace _Tests_For_OOConcepts
{
[TestClass]
public class FilterTests
{
IEnumerable<iproduct> complete_list;
IEnumerable<iproduct> results;
IFilter<iproduct> System_under_test;
[TestInitialize]
public void startup()
{
complete_list = Fake_list_of_products();
System_under_test = new ProductFilter();
}
[TestMethod]
public void List_only_contains_items_of_a_specified_COLOR()
{
results = Action_the_system_under_test_with
(new ColorFilterSpecification(Color.Orange));
foreach (var item in results)
Assert.IsTrue(item.ItsColor == Color.Orange);
}
[TestMethod]
public void List_only_contains_items_of_a_specified_SIZE()
{
results = Action_the_system_under_test_with
( new SizeFilterSpecification(ProductSize.Small) );
foreach (var item in results)
Assert.IsTrue(item.Size == ProductSize.Small);
}
[TestMethod]
public void List_only_contains_items_of_a_specified_SIZE_AND_COLOR()
{
var the_matching_criteria = new AndSpecification
<iproduct>(new ColorFilterSpecification
(Color.AliceBlue),
new SizeFilterSpecification(ProductSize.Small));
results = Action_the_system_under_test_with(the_matching_criteria);
foreach (var item in results)
{
Assert.IsTrue(item.Size == ProductSize.Small);
Assert.IsTrue(item.ItsColor == Color.AliceBlue);
}
}
public IEnumerable<iproduct> Action_the_system_under_test_with
(ISpecification<iproduct> the_filter_specification_to_use)
{
return System_under_test.Filter
(complete_list, the_filter_specification_to_use);
}
public IEnumerable<iproduct> Fake_list_of_products()
{
yield return new FakeProduct("apple", Color.Red, ProductSize.Large);
yield return new FakeProduct("apple2", Color.Red, ProductSize.Medium);
yield return new FakeProduct("orange", Color.Orange, ProductSize.Medium);
yield return new FakeProduct("orange2", Color.Orange, ProductSize.Large);
yield return new FakeProduct("bananna", Color.Yellow, ProductSize.Small);
yield return new FakeProduct
("bananna2?, Color.Yellow, ProductSize.ReallyBig);
yield return new FakeProduct
("stuffed bear", Color.AliceBlue, ProductSize.ReallyBig);
yield return new FakeProduct
("stuffed bear2?, Color.Orange, ProductSize.Small);
}
}
public class FakeProduct : IProduct
{
public FakeProduct(string name, Color theColor, ProductSize itsSize)
{
this.Name = name;
this.ItsColor = theColor;
this.Size = itsSize;
}
public string Name {get;set;}
public Color ItsColor {get;set;}
public ProductSize Size { get; set; }
}
}
So now we can vary the filter specification, add new specifications and extend the filtering object without actually modifying the source code of the Filter object.
The sample was first presented on LosTechies.
Hope this helps.
fuzzelogic Solutions.com
Read the blog