Friday, June 8, 2012

Object Design Principles - Part 4

Interface Segregation Principle (ISP)

The ISP says that an interface should not become too 'fat' but be split into smaller and more specific interfaces so that only methods that pertain to a client need to be implemented.  You should focus on designing abstractions that have a very small, focused and sleek design.  Basically, no client should be forced to implement methods or properties it does not need to use.  We should always start at the most granular level and extend interfaces as needed.  You can create new interfaces that are extensions, or groupings, of other interfaces to form classes and objects desired.  Rather than forcing everyone to consume the entirety of a polluted interface, you are allowing them to implement an array of more focused interfaces.  I like analogies for getting my hands around design principles.  I think of this one much like eating at a restaurant.  If you sit down and order a burger, how do you react if you are given soup; then a burger, fries, and a coke; then an ice cream sundae?  And you are expected to pay for all of it.  Apparently, the only way to get a burger is to implement the IThreeCourseMeal interface.  If it doesn't make sense in life, it doesn't make sense in design. 

During application design we should pay close attention to how we abstract modules that contain several sub-modules. Granularity is always the side on which to err when doing interface design.  It is much easier to couple interfaces than to decouple a polluted interface (just as it would be very difficult to 'un-salt' your food.)  As I look through existing interfaces, it seems that this principal is easily and repeatedly violated, probably without intent.  In reviewing for this piece, I saw that months ago I created an interface that flagrantly violated this principle as a quick means to an end.  I plan to decouple it as part of this mental exercise.

Using the restaurant analogy above, I created some interfaces that, while silly, illustrate the principle.  First, I would go for the most granular interface that could logically be shared for all iterations of this type.  I landed on a menu item:

public interface IMenuItem
{
    String Item { get; set; }
    String Price { get; set; }
}


Then, I started creating logical abstractions of a menu item into various aspects of a meal:

public interface IEntree : IMenuItem
{
    String CookingInstructions { get; set; }
}
 
public interface ISide : IMenuItem
{
    Boolean IsVeggie { get; set; }
}
 
public interface IDrink : IMenuItem
{
    Int16 CupSize{ get; set; }
}
 
public interface IAppetizer : IMenuItem
{
    //...
}
 
public interface IDessert : IMenuItem
{
    //...
}


Next, I moved onto large interfaces that would have enough real world implementations to justify a single abstraction:

public interface IValueMeal : IEntree, ISide, IDrink
{
    //...
}
 
public interface IThreeCourseMeal : IAppetizer, IEntree, ISide, IDrink, IDessert
{
    //...
}

The advantage is that I can always come in at the implementation level and create my own dynamic abstraction.  Notice which properties' objects are created when I choose to implement the interface:

public class MyCustomMeal : IEntree, IDrink
{
 
    #region IEntree Members
 
    public string CookingInstructions
    {
        get
        {
            throw new NotImplementedException();
        }
        set
        {
            throw new NotImplementedException();
        }
    }
 
    #endregion
 
    #region IMenuItem Members
 
    public string Item
    {
        get
        {
            throw new NotImplementedException();
        }
        set
        {
            throw new NotImplementedException();
        }
    }
 
    public string Price
    {
        get
        {
            throw new NotImplementedException();
        }
        set
        {
            throw new NotImplementedException();
        }
    }
 
    #endregion
 
    #region IDrink Members
 
    public short CupSize
    {
        get
        {
            throw new NotImplementedException();
        }
        set
        {
            throw new NotImplementedException();
        }
    }
 
    #endregion
}

Hopefully, you can see how this principle allows for much scalability and dynamism of types without the inherent bloat from linear bulge.  This is a great principle to keep in focus during design.