The Open-Closed Principle for Languages with Open Classes 14

Posted by Dean Wampler Fri, 05 Sep 2008 02:42:00 GMT

We’ve been having a discussion inside Object Mentor World Design Headquarters about the meaning of the OCP for dynamic languages, like Ruby, with open classes.

For example, in Ruby it’s normal to define a class or module, e.g.,

    
# foo.rb
class Foo
    def method1 *args
        ...
    end
end
    

and later re-open the class and add (or redefine) methods,

    
# foo2.rb
class Foo
    def method2 *args
        ...
    end
end
    

Users of Foo see all the methods, as if Foo had one definition.

    
foo = Foo.new
foo.method1 :arg1, :arg2
foo.method2 :arg1, :arg2
    

Do open classes violate the Open-Closed Principle? Bertrand Meyer articulated OCP. Here is his definition1.

Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.

He elaborated on it here.

... This is the open-closed principle, which in my opinion is one of the central innovations of object technology: the ability to use a software component as it is, while retaining the possibility of adding to it later through inheritance. Unlike the records or structures of other approaches, a class of object technology is both closed and open: closed because we can start using it for other components (its clients); open because we can at any time add new properties without invalidating its existing clients.

Tell Less, Say More: The Power of Implicitness

So, if one client require’s only foo.rb and only uses method1, that client doesn’t care what foo2.rb does. However, if the client also require’s foo2.rb, perhaps indirectly through another require, problems will ensue unless the client is unaffected by what foo2.rb does. This looks a lot like the way “good” inheritance should behave.

So, the answer is no, we aren’t violating OCP, as long as we extend a re-opened class following the same rules we would use when inheriting from it.

If we use inheritance instead:

    
# foo.rb
class Foo
    def method1 *args
        ...
    end
end
...
class DerivedFoo < Foo
    def method2 *args
        ...
    end
end
...
foo = SubFoo.new    # Instantiate different class...
foo.method1 :arg1, :arg2
foo.method2 :arg1, :arg2
    

One notable difference is that we have to instantiate a different class. This is an important difference. While you can often just use inheritance, and maybe you should prefer it, inheritance only works if you have full control over what types get instantiated and it’s easy to change which types you use. Of course, inheritance is also the best approach when you need all behavioral variants simulateneously, i.e., each variant in one or more objects.

Sometimes you want to affect the behavior of all instances transparently, without changing the types that are instantiated. A slightly better example, logging method calls, illustrates the point. Here we use the “famous” alias_method in Ruby.

    
# foo.rb
class Foo
    def method1 *args
        ...
    end
end
# logging_foo.rb
class Foo
    alias_method :old_method1, :method1
    def method1 *args
        p "Inside method1(#{args.inspect})" 
        old_method1 *args
    end
end
...
foo = Foo.new
foo.method1 :arg1, :arg2
    

Foo.method1 behaves like a subclass override, with extended behavior that still obeys the Liskov-Substitution Principle (LSP).

So, I think the OCP can be reworded slightly.

Software entities (classes, modules, functions, etc.) should be open for extension, but closed for source modification.

We should not re-open the original source, but adding functionality through a separate source file is okay.

Actually, I prefer a slightly different wording.

Software entities (classes, modules, functions, etc.) should be open for extension, but closed for source and contract modification.

The extra and contract is redundant with LSP. I don’t think this kind of redundancy is necessarily bad. ;) The contract is the set of behavioral expectations between the “entity” and its client(s). Just as it is bad to break the contract with inheritance, it is also bad to break it through open classes.

OCP and LSP together are our most important design principles for effective organization of similar vs. variant behaviors. Inheritance is one way we do this. Open classes provide another way. Aspects provide a third way and are subject to the same design issues.

1 Meyer, Bertrand (1988). Object-Oriented Software Construction. Prentice Hall. ISBN 0136290493.

Comments

Leave a response

  1. Avatar
    Dave Hoover about 1 hour later:

    I wouldn’t say it’s normal in Ruby to define a class in foo.rb and then re-open it in foo2.rb later. It’s certainly possible, but it’s more common to re-open someone else’s class, such as String. If you own the class, then the normal thing to do is just edit the source, or if you want to adhere to OCP, then sub-class or decorate.

  2. Avatar
    Micah Martin about 5 hours later:

    I’m still intrigued by the design implication of open classes. I agree that opening classes does not necessarily violate OCP. But contrary to Dave Hoover’s comment, I frequently want to reopen my own classes to avoid violating the ISP, Interface Segregation Principle.

    For example, I have class Dog. One client of Dog uses it to bark(). Another client uses Dog to bite(). The barking client doesn’t care about biting and the biting client doesn’t care about barking. So in each client I’ll open the Dog class and add the desired behavior.

    But I wonder…. is this bad design? Reading the code becomes more challenging since the behavior of Dog is in many locations.

  3. Avatar
    An impressed ... about 12 hours later:

    “Object Mentor World Design Headquarters.”

    WOOOAAAAHHH!!!

  4. Avatar
    Brian Chiasson about 14 hours later:

    @Micah,

    That’s an interesting spin on class definitions and would indeed be confusing. If we weren’t talking Ruby, I would suggest defining two different interfaces for the Dog class that your clients can use and leave the two methods in the same class definition – ILoudDog and IAggressiveDog.

  5. Avatar
    Ravi Venkataraman about 15 hours later:

    I am impressed, too, with “World Design Headquarters”.

    This creates the impression that there are several design locations around the world. It also suggests that there are other non-design locations, maybe a training headquarters, a development headquarters, a QA headquarters, etc.

    Would not such divisions of software construction go against the Agile philosophy?

  6. Avatar
    Hugo Baraúna about 15 hours later:

    Good post! =) I would like to see more posts about Object Oriented Design inside the dynamic languages world, mainly the Ruby world.

  7. Avatar
    Michael Feathers about 16 hours later:

    I have a couple of problems with seeing OCP as a source level thing. The first is that if we do, OCP doesn’t really offer much guidance. In a language as dynamic as Ruby, you can replace anything you like without touching the original source. If the source of a class can always remain closed (i.e., it doesn’t have to change) we could legitimately say that dynamic languages give you OCP trivially so it’s probably not worth talking about OCP at all.

    The second problem I have with seeing OCP as a source level thing is that open classes present problems of their own that, ideally, OCP should say something about. OCP adherence often triggers the creation of orthogonal abstractions: you split the class and then you know that mucking with one responsibility isn’t going to muck with the other. With open classes, you could easily make a change to a class in one file which accidentally clobbers functionality in another file. In a behavioral sense, the original file was never really closed then, was it? Other clients could be affected.

    Languages which allow self-modifying code are a bit of a different animal than what Meyer was considering.

  8. Avatar
    Dean Wampler about 16 hours later:

    Concerning World Design Headquarters, I was of course inspired by the Daily Show ;)

    @Hugo, thanks. I’m sure we can add more blogs on “dynamic design”!

    Here’s another perspective; what we really care about is feature modularity. I’d like to be able to reason about how a feature behaves (for any appropriate definition of “feature”). Think about OOP for a second; we allow the definition of a class to be spread over several “physical artifacts”, i.e., a base class + mixins/interfaces + subclasses, sometimes in separate files. If I want to reason about the behavior of a FileInputStream, I have to look in several places, for example.

    I remember when OOP went viral. Lots of C programmers complained that they couldn’t figure out what their code was doing anymore, because the flow of control hoped around. With better tools (and drugs?) we got used to it.

    The challenge of open classes is similar; we need modular reasoning over a set of artifacts. Tooling will help, so will programming conventions, practice, etc.

    I agree to a point with Michael that I don’t think Meyer intended OCP to be just a source thing. It really is a statement of the power of extension through inheritance. He probably didn’t have open types in mind. (IIRC, Eiffel doesn’t support them.) That’s partly why I added the “contract” bit, so that our “non-local” extensions are done in a principled way.

  9. Avatar
    Glenn Vanderburg about 17 hours later:

    One of the guiding principles of dynamic languages like Ruby is that there should be rules, but nearly always with escape hatches.

    I think open classes (or at least some uses of them) are definitely violations of OCP, in the same way that #instance_variable_get violates encapsulation. That doesn’t mean it’s bad; it just means it’s worse than other alternatives.

    Face it … it’s pretty hard to design a class that gets it exactly right with respect to OCP, and is extensible in all the right ways. And we have to work with classes that aren’t ideally designed. In the face of that, those escape hatches are invaluable.

    The danger, of course, is that when designing classes we will just rely on the escape hatches and not try to design our classes for extension.

  10. Avatar
    Michael Feathers about 17 hours later:

    @Glenn I agree. I keep thinking that there’s a principle below all of this which is something like: Never write code that nullifies the behavior of existing code.

    We should be able to understand the system from the sources. If we see a method, we should know that its behavior is there in the system, and although people may add to that behavior in various ways, they won’t nullify it through other code.

  11. Avatar
    Pat Maddox about 18 hours later:

    Ruby’s dynamic nature allows you to make the kind of changes you proposed (adding a method) without violating OCP. You can even modify the original source without violating OCP. Consider the basic Ruby class:

    class Dog def bark; “woof!” end end

    and its equivalent-ish Java class:

    public class Dog { public String bark() { return “woof!”; } }

    Now we add another method to Ruby.Dog:

    class Dog def bark; “woof!” end def snarl; “gnashing of teeth” end end

    and we go on with business as usual.

    Now we add another method to Java.Dog:

    public class Dog { public String bark() { return “woof!”; } public String snarl() { return “gnashing of teeth”; } }

    and now we have to recompile every client of Dog.

    To me, OCP is all about avoiding changes that screw up clients of a given class. You can really screw them up by changing the implementation in such a way that violates the original contract, or you can simply cause a nuisance by forcing them to be recompiled. In Ruby and other dynamic languages, you still have to think about the first part, but at least the second class of problems has gone away.

  12. Avatar
    murphee about 19 hours later:

    @PatMaddox: “and now we have to recompile every client of Dog.”

    No we don’t. Try it.

  13. Avatar
    Dean Wampler about 21 hours later:

    @Glenn and @MichaelFeathers, agreed that “nullification” or “throw-a-curve-ball-ification” are no no’s.

    One of the problems I’ve thought a lot about lately is how an “optimal” domain model is only optimal in a narrow context. In the large enterprises I work with, different subsystems and parts of the business need the same type to have different attributes and behaviors. It’s something of a lose-lose for the designer. If the type has the superset of stuff, that’s bad. If we have lots of variants of the type (with boilerplate copying back and forth), that’s bad for different reasons.

    I want a way to keep a consistent, universal domain model, yet provide context-dependent extensions, even to shared instances. That and world peace….

    By the way, the aspect-oriented programming community has wrestled a lot with ways to specify allowed extension points to a type without assuming anything (or much) about the actual extensions.

  14. Avatar
    Brett L. Schuchert 1 day later:

    What happens if the class itself performs the extensions? For example, using some kind of rule (e.g. naming convention), it finds all of its extensions and “loads” them once and for all?

    The class is closed – source not changing.

    The opening up of the class is defined in one place (the class itself) but the extension of the class can be determined by the source code (to Feathers point).

    I’ve got such an example I’m working with (a simple one sure).

    About the only thing I’d want to add is that the extension point verifies that the other “openings” don’t step on each other.

Comments