Monday, May 25, 2009

SOLID and DRY Part 2

Interface Segregation Principle

If I don't care about it, don't make me implement it!

The interface segregation principle simply dictates that you design your interfaces with the other principles in mind. Instead of creating a bloated interface, interfaces should be segregated into cohesive units of functionality. This way the implementation of the interface can focus on what is important for that class.

To better illustrate this, consider a data access class. One common approach might be to interface that class, and declare methods for loading, saving, updating, deleting, searching, performing a list, a security check, and who-knows-what-else. You could then create a concrete class that implemented this interface. The result is a fat interface that does everything the class needs, but doesn't necessarily allow for appropriate scaling.

Another example is the use of an ORM like NHibernate. Perhaps your company decides this is useful for the basic CRUD operations (create, read, update, delete) but that LINQ to SQL or some other framework will be used for the complex searches and sorts. With one large, "IWidgetDataAccess" interface, you would have to implement all of the method signatures, even if you really only did something concrete to the CRUD pieces. A common practice is to do this:


void MethodNotUsed() 
{
   throw new NotImplementedException();
}

If you find yourself throwing this exception, it's a pretty good sign you are not following the interface segregation principle. A better approach would be to break out two different interfaces. One interface is your standard CRUD interface, call it "ICrud" and the other is the specific set of methods for the class ("IWidget"). Then you could implement your ICrud pieces with the concrete NHibernate instance, and save the IWidget for a different concrete class.

This really comes back to the Liskov substitution principle. I don't want to pollute my IUser interface with methods for an administrator, otherwise everyone has to implement the method signature (even if simply to throw a NotImplementedException). With interface segregation, I can have IUser and IAdminUser and then my AdminUser would implement both:


public class AdminUser : BaseUser, IUser, IAdminUser

In summary, the interface should be fine-grained and focused on the client class that will implement the interface, rather than fat and bloated covering every possibility.

Dependency Injection or Inversion of Control

Depend on abstraction, not concreteness.

This final principle is very popular but not well understood. I see lots of discussion about dependency injection and inversion of control, but it all seems centered on the frameworks that implement it (such as StructureMap, Unity, Spring.NET, and so forth) rather than the principle itself. Like many of the other SOLID and DRY principles, this one does not stand alone but ties into the other principles as well.

The idea is rooted back in our "Single Responsibility." Once again, let's take an example. I am writing my data access class and for troubleshooting, I want to log issues. I decide that I'm going to use log4net and so I configure everything that I need and then start writing out logging information in my class.

At this point I've violated "single responsibility" because my class isn't doing one thing (data access), it's responsible for two things (data access and logging). If I continue down this path I'll end up with dozens or even hundreds of classes with log4net logging embedded in them. Then I get the notice from the powers that be that we cannot use any type of open source in our project. We decide to shift to Enterprise Library. Now I've got my work cut out for me because I depended so much on my concrete logging implementation that I have to touch every class.

By focusing on abstractions, I could instead have allowed for an ILogger interface and coded to that. I no longer make my class responsible for logging, instead it simply takes in the abstraction of a logger interface (ILogger) and depends on something else to make it concrete. This is where "dependency injection" comes from: the dependency on the concrete implementation is injected by something else. This also explains what "inversion of control" is - instead of my class being in control of logging, I invert that control and let something higher up the chain decide.

For a more detailed explanation of dependency injection, read Simplified Mocking with Dependency Injection.

As I mention in that article, DI does not require a framework. Taking the logging example, I could easily do this at the beginning, when I think, "We're going to use log4net, but let me go ahead and follow some solid object-oriented design principles and abstract it just in case."


public class Widget
{
    private ILogger _logger;

    public Widget() 
    {
       _logger = LoggerFactory.GetLogger();
    }
 
    public Widget(ILogger logger) 
    {
       _logger = logger;
    }
    ...
}

public static class LoggerFactory 
{
    public static ILogger GetLogger()
    {
       return new Log4NetLogger();
    }
}

As you can see, no framework is required. My classes work fine without anything knowing what to inject, but they make a constructor available anyway so if I do decide to move to configuration or a framework, I can do so. I use the factory pattern to get the logger. Again, we're hardcoded right now. But now I have possibilities. If I switch to Enterprise Library, for example, I only have to go to one place to change the concrete instance of my logger. Furthermore, I might want to have my logs Debug.Print during testing, in which case I create my custom DebugLogger that uses Debug.Print and inject that for testing.

Summary

These concepts are by no means "programming law" and there are many variations. I also didn't intend this to be a comprehensive discussion of the principles, but hopefully have touched on the surface and generated enough interest and curiosity for those not familiar to begin more research and apply these methods to their own software. In my opinion, you can give complex names to certain concepts or come up with new "architectures" or "design patterns" but in the end, it's about getting back to the basics and continuing to write simple, modular, easy to read and understand, maintainable code that works as building blocks for more powerful applications.

Jeremy Likness