The Open/Closed Principle – A real world example

Out of the five SOLID principles the Open/Closed Principle is probably the one that I’ve had the hardest time understanding. However, a while ago though I found some code that I had written years ago that made me think to myself “Hey, this is clearly violating the Open Closed Principle!”. I had been thinking of reusing this code in the project I was working on, this blog, but realized that I would have to rewrite it to abide by OCP to satisfy my ambition of writing a flexible and well designed application.

The case – protecting the blog from spam bots

The case was this: I was writing a (ASP.NET MVC) controller that handled adding new comments from visitors to a blog post. Naturally I wanted to stop spam bots from posting comments but I didn't want to force visitors to fill in some CAPTCHA text. So, I decided to check commenting visitors IP address’ against Project Honey Pot’s IP black list. While that strategy doesn’t give bulletproof protection against spam bots it’s very easy to implement so I thought I’d start with that and then add other protection, such as Akismet, later on should I need to.

As I had implemented spam bot protection with Project Honey Pot before I dug up that old piece of code. It consisted of a single method that was called when a new comment was about to be saved. My old implementation looked something like this:

public class EntryController
{
    public void AddComment()
    {
        if(ValidateNotSpam())
        {
             //Save the comment to the database
        }
    }
    
    private bool ValidateNotSpam(string comment)
    {
        //Check if the IP-address that made the request
        //is known as a spammer by Project Honey Pot
    }
}

This code has a few problems. First of all it is responsible for doing two things, validating the request and persisting the comment, which isn't optimal. It's dependency on an external service renders it almost impossible to test. It is also clearly violating the Open/Closed principle.
Let's say I wanted to extend it so that it also used another service for validating the comment. I would then have no other choice but to modify the ValidateNotSpam method. And if I where to ship the compiled code as a product the users of it would have no way to add another service. Hence, it's not possible to extend the class, that is it isn't open for extension, and to modify behavior that isn't at the core of what the class should do (persisting comments) requires the actual class to be changed, it isn't closed for modification.

A better solution – abiding by the Open Closed Principle

Instead of using that old class I decided to come up with a better solution, which (I think) abides by OCP and which is much easier to extend. First of all I created an interface called ICommentValidator with a single method.

public interface ICommentValidator
{
    bool Validate(string authorIP, string content);
}

I then let my controller require an array of instances of ICommentValidator as a constructor parameter.

public class EntryController : Controller 
{
    private ICommentValidator[] _commentValidators;

    public EntryController(ICommentValidator[] commentValidators)
    {
        _commentValidators = commentValidators;
    }
}

I also modified the ValidateNotSpam method so that it invoked the Validate method on all of the ICommentValidators supplied to the constructor.

public class EntryController
{
    private bool ValidateNotSpam(string comment)
    {
        foreach (ICommentValidator validator in _commentValidators)
        {
            if (!validator.Validate(Request.ServerVariables["REMOTE_ADDR"], comment))
                return false;
        }

        return true;
    }
}

Finally I created a new class, ProjectHoneyPotCommentValidator that implemented the ICommentValidator interface and supplied an array with an instance of that class to the EntryControllers constructor. Actually I didn’t do that last part myself though, but instead let StructureMap instantiate the EntryController with instances of all ICommentValidator that it could find in the application domain, but that’s sort of beside the point.

public class ProjectHoneyPotCommentValidator : ICommentValidator
{
    public bool Validate(string authorIP, string content)
    {
        //Check if authorIP is blacklisted with 
        //Project Honey Pot
    }
}

Conclusion

In order to satisfy the Open/Closed Principle I had to make quite a few changes and add some extra abstraction but as a result my EntryController class is now much more flexible while at the same time it’s more stable as it’s less likely that it will have to be changed in the future. Choosing this implementation might have cost me about 15 minutes extra development time, but it didn’t take me long until that paid off when I had to add more validation functionality which was now a breeze. I also think that this code is much easier to maintain.

Any feedback is much appreciated! Did this example make sense? Was it a good example of the Open Closed Principle? Have I perhaps still misunderstood the principle?

Comments

  1. Jeff Doolittle's avatar

    Jeff Doolittle 3 months ago

    Good example of OCP, Joel. Although, to clarify for those learning OCP, you do not necessarily need the parameter to be an array in order to apply OCP. Suppose the parameter simply accepted ICommentValidator rather than ICommentValidator[]. If in the future you needed to apply multple comment validators, you could create a CompositeCommentValidator that applied rules from multiple validation providers.

    [Pro]: I like the cleaner constructor for the EntryController. Requiring an array feels ugly to me. Of course since StructureMap will scan for all implementors, it certainly makes the hook up easy. But I'm not so sure I want my APIs to be affected by my DI container choice.

    Which leads to a [Con]: You could no longer wireup StructureMap by simply scanning for all implementors of ICommentValidator. If CompositeCommentValidator received an array of validators in its constructor, you'd get a StructureMap error since CompositeCommentValidator implements ICommentValidator and this self-reference would be problematic.

    My main point is that you don't have to accept array parameters in order to comply with OCP.

  2. Joel Abrahamsson's avatar

    Joel Abrahamsson 3 months ago

    Good point Jeff. Thanks for the feedback!

  3. Daniel Berg's avatar

    Daniel Berg 3 months ago

    Great post, Joel! Love the framework mindset. :)

  4. Ted Nyberg's avatar

    Ted Nyberg 3 months ago

    Great post, I'd love to see more writings on patterns! In this case I totally agree with your architectural approach!

    I can also see that your approach for comment validation is fairly similar to that of BlogEngine.NET (which I still run on since I don't have a slick EPiServer Community blog like you) ! Oh, the envy... :)

  5. Bob Santos's avatar

    Bob Santos 3 months ago

    I am not a .Net fan as I am more of an open source guy, mainly developing using Java but I would like to say you got one good and simple to understand example there Joel. Good job!

    Moreover, I agree with Jeff on the con of using arrays as parameters since its better to use interfaces polymorphically.

    My take on OCP is that more than usual you will get this if you code against an interface rather than a concrete class.

    Again, good post! Hoping for more posts on patterns.

Add a comment

Allowed tags: <b>, <em>, <quote cite="">, <code>, <c-sharp-code>, <css-code>, <sql-code>, <xml-code>, <javascript-code>. If you want to display code examples, please remember to write &lt; for < and &gt; for >.

Follow me on Twitter

  1. @tathamoddie Sadly, no :( 5 hours ago
  2. @tathamoddie So far so good. Might get back to you though :) 14 hours ago
  3. @fredriktberg Turned out it was an issue with assembly trust levels. Upping them helped. Never experienced it before though. 16 hours ago
follow me

Latest comments

  1. Joel Abrahamsson wrote "Hi Rajesh! As you say, given that you use only GetPage an..." on How EPiServer CMS caches PageData objects
  2. Rajesh Shelar wrote "Hi Joel, What you have mentioned is DATA caching and we can..." on How EPiServer CMS caches PageData objects
  3. Adeel wrote "Hi, no theyre saved as normal, save and publish, and the..." on How EPiServer CMS caches PageData objects

About this site

This blog is built with EPiServer Community, EPiServer CMS, ASP.NET MVC and a bunch of other great products. The source code is available for download at the projects page, where you also can read more about this site and my other projects.

read more