August 28, 2007
The Open Closed Principle and Ruby
A few weeks ago I wrote about Dependency Injection and how it doesn't really matter in Ruby. On one of the comments for this post Yoni said (in regard to extending classed, both in .Net 3.5 and Ruby):
Regarding the issue of extending classes (and partial classes) I am very sorry it was ever invented. Developers should be encouraged to divide large classes by decomposing them into decoupled sub-components using design patterns and not by simply splitting them between files. Even though a feature is called "extending" a class it is in violation of the Open Closed Principle...
that sounds like a serious charge :) So here's another round of "Ruby does it better" the time featuring Bertrand Meyer's "Open Closed Principle" or OCP for short.
OCP was defined in 1988 in Bertrand's book "Object Oriented Software Construction as follows:
Modules should be both open (for extension and adaptation) and closed (to avoid modification that affect clients)
When we work with a language like C# or Java we have several ways to do that -- here's what I had to say about it :
It is easy to see the benefit of having a class that answers this principle: When you need to add a requirement, instead of breaking dependent code (and tests) you just extend it somehow and everything is nice and dandy. Furthermore, violating OCP can result in Rigidity,Fragility, and Immobility.
But how do you do that? The obvious (and naïve) answer is inheritance. Every time something needs to change just add a sub-class. The parent class is not changes and voilà. However, if you add sub-classes all the time you'd get "lazy classes" or freeloaders -- sub-classes without a real reason for existence not to mention a maintenance nightmare.
Thus, sub-classing is an option but we need to consider carefully where to apply it. Other (more practical ) OCP preserving steps include:
The way I see it Ruby (and other dynamic languages for that matter) just allows us to extend this repertoire by adding a few other options such as: Singleton methods (that's not a very good name, but the more appropriate name "instance methods" was already taken by another poor naming choice...) which are methods added to a specific instance of a class:
class Foo
def bar
puts "foo.bar"
end
end
obj = Foo.new
obj2 = Foo.new
def obj.bar # redefining bar just for obj
puts "foo.newbar"
end
obj.bar # prints foo.newbar
obj2.bar # prints foo.bar
Closures. I already mentioned closures for C# but it existed in dynamic lanaguages for a long time now. Closures are sort of like injecting a procedure. Here's a quick sample:
class Foo
def bar(myProc) # accepts a callable object
foobar = "foo.bar"
myProc.call(foobar) # call the object with a parameter
end
end
obj=Foo.new
printer=Proc.new {|msg| puts msg}
obj.bar(printer)
Meta programming. Ruby has a lot of ways to add and change classes, methods, and whatnot. Again. here is a simple example:
class Foo
end
obj=Foo.new
# obj.bar - error
Foo.class_eval do # this simplistic example can also use regular def
define_method :bar do # but class_eval can also evaluate strings to create
puts "foo.bar" # dynamic methods like setters etc.
end
end
obj.bar
And there are a few other similar ways. All of them are not violating OCP! OCP is kept since we do not alter the original class. Anyone using the original class not in our context (i.e., not in the modified context) will not be affected by the change. The interface of the class, in the sense that was originally defined is not changed either, and remains stable. Any client that uses the modified or the original class will not have to change it syntax because of the changes we've maid. This is also in-line with the protected variation way of looking at OCP :
Identify points of predicted variation and create a stable interface around them.
While in C# your would maybe want to add a template method or a specialized interface in Ruby you can apply YAGNI and only change the behavior if the need arise.
Lastly, we can also extend classes -- but OCP lets us do that so again, we are okay.
Nevertheless, we should be careful not to violate Liskov Substitution Principle when we do all that, which I guess is relatively tempting to do if you using Ruby -- but that's for another post ....
Posted by Arnon Rotem-Gal-Oz at 07:42 AM Permalink
|