The Open-Closed Principle

This tip is the first in a series of articles dealing with the best practices of Object Oriented Design (OOD). In these articles, we will discuss several design techniques that lay the foundation of solid OOD practices. To begin, we will first introduce the cornerstone principle of OOD. All areas of study have such a principle; the core behind the entire science. For calculus, this is the Fundamental Theorem of Calculus. Newton's Three Laws of Motion lay the groundwork in physics. For OOD, the basis principle is the open-closed principle.

One thing is certain in all software development, all software changes during its life cycle. So, it is imperative that developers design software that is stable (i.e. does not change) in the face of ever changing functional requirements. To aid in this development, Bertrand Meyer gave us the open-closed principle, which states:

Software entities should be open for extension, but closed for modification.

Software modules that satisfy the open-closed principle have two huge benefits. First, they are "open for extension". Meaning, the behavior of the modules can be altered to satisfy the changing requirements. Second, these modules are "closed for modification". The modules, themselves are not allowed to change.

At first, these benefits seem impossible to experience at the same time. Normally, one would change the module if they wanted its behavior to change. How can an immutable module exhibit anything but fixed behavior?

The Answer is Abstraction

In object-oriented programming languages, it is possible to create abstractions that have a fixed design yet represent a limitless group of behaviors. With Java, the abstraction with a fixed design comes from an abstract base class or interface and, the endless group of classes that can be derived from the abstraction realizes the limitless group of behaviors. So, any module that manipulates solely abstractions will never need to change since the abstractions are fixed; the module is closed for modification. Also, deriving a new class from the abstractions can change the behavior of the module. This makes the module open for extension.

A Simple Example

Suppose you are a programmer for an on-line book seller whose current system allows for searching their collection of books on their web site. For the new system, you have the responsibility of developing a search filter that is to be added to the current search engine. The requirements of your filter modules is to take a vector of Book objects and return an new vector of the books published in a given year. Through some careful thought, you come up with:

import java.util.Enumeration;
import java.util.Vector;

public class Filter
{
    public Vector filterOnYearOfPublication(Vector books, int year)
    {
        Vector newBooks = new Vector();
        Enumeration enum = books.elements();
        while(enum.hasMoreElements()){
            Book book = (Book)enum.nextObject();
            if(book.getYearOfPublication() == year){
                newBooks.addElement(book);
            }
        }
        return newBooks;
    }
}

The code you wrote works great and the new system is delivered with your filter. A couple of months later, you receive the requirements for the next release of the system. Now, your filter needs to be enhanced to allow for filtering on a books language in addition to the publication date. So, you go into your filter class and make some changes:

import java.util.Enumeration;
import java.util.Vector;

public class Filter
{
    public Vector filterOnYearOfPublication(Vector books, int year)
    {
        Vector newBooks = new Vector();
        Enumeration enum = books.elements();
        while(enum.hasMoreElements()){
            Book book = (Book)enum.nextObject();
            if(book.getYearOfPublication() == year){
                newBooks.addElement(book);
            }
        }
        return newBooks;
    }

    public Vector filterOnLanguage(Vector books, String language)
    {
        Vector newBooks = new Vector();
        Enumeration enum = books.elements();
        while(enum.hasMoreElements()){
            Book book = (Book)enum.nextObject();
            if(book.getLanguage().equals(language)){
                newBooks.addElement(book);
            }
        }
        return newBooks;
    }
}

Alarms and sirens go off in your head because you have just violated the open-closed principle. Your filter was not closed for modification even though it needs to be open for extension. You contemplate whether there is a way to refactor your filter so that it will never need changing despite the possibility you will need to provide more filtering options in future releases. Looking at the two methods, you notice they only differ by one line of code, the if clause. You decide to make the if clause open for extension by creating a Selector abstraction.

public interface Selector
{
    boolean shouldSelect(Book book);
}

public class YearOfPublicationSelector implements Selector
{
    private int year;

    public YearOfPublicationSelector(int year)
    {
        super();
        this.year = year;
    }

    public boolean shouldSelect(Book book)
    {
        return book.getYearOfPublication() == year;
    }
}
 
public class LanguageSelector implements Selector
{
    private String language;

    public LanguageSelector(String lang)
    {
        super();
        language = lang;
    }

    public boolean shouldSelect(Book book)
    {
        return book.getLanguage().equals(language);
    }
}
 
public class Filter
{
    /**
     * @deprecated use filter instead
     */
    public Vector filterOnYearOfPublication(Vector books, int year)
    {
        Selector selector = new YearOfPublicationSelector(year);
        return filter(books, selector);
    }

    public Vector filter(Vector books, Selector selector)
    {
        Vector newBooks = new Vector();
        Enumeration enum = books.elements();
        while(enum.hasMoreElements()){
            Book book = (Book)enum.nextObject();
            if(selector.shouldSelect(book)){
                newBooks.addElement(book);
            }
        }
        return newBooks;
    }
}

Your new and improved filter code now satisfies the open-closed principle. The filter class will never need to change for different filtering option needs. Yet it can perform any filtering imaginable simply by deriving a class from the Selector interface.

Restating the Benefits

The major benefits received from the open-closed principle are modules that are open for extension, yet closed for modification. Why are these benefits so big? They are big because they seek to achieve two goals of object-oriented programming. With the module being open for extension, it is highly reusable and can be used to satisfy a ever changing list of requirements. Also, the module is extremely maintainable, as it never needs to change.

Next Time

As stated, the open-closed principle is the cornerstone of object-oriented design and probably the most important convention in the practice of object-oriented design. Another very important principle that governs the creation of inheritance hierarchies is the Liskov Substitution Principle, which will be explained next time.