This is a companion discussion topic for the original entry at https://thoughtbot.com/upcase/videos/open-closed-principle
One thought I had on this episode is that the decorator mentioned in the Printer
example is particularly useful for changing the interface of an object from a library that we don’t control:
class Printer
def initialize(item)
@item = item
end
def print
puts @item
end
end
Ruby’s puts
method implicitly prints the return value of the #to_s
method on the object.
We previously needed to print these types:
Text.new.to_s
Image.new.filename
Document.new.formatted
One option would be to not care about modifying those classes. I would first grep
the project for instances of Image
and Document
and examine how they are being used. Assuming that search makes me feel like this change is possible, I would probably add an Image#to_s
and change Document#formatted
to Document#to_s
, then run all the specs and see if there are any failures.
What if Document
came from a library like Prawn or DocRaptor, though? I may not have control over modifying the class (beyond monkey patching, which I try to avoid). This is the case where a decorator would be great:
class PrintableDocument < SimpleDelegator
def initialize(*args)
super(Document.new(args))
end
def class
__getobj__.class
end
def to_s
__getobj__.formatted
end
end
There’s more detail on decorators in Ruby in Evaluating Alternative Decorator Implementations in Ruby, Tidy Views and Beyond with Decorators, and Decorators Compared to Strategies, Composites, and Presenters.
Hello.
Open/Closed isn’t about compiling, it is about change. If you can make a class, module, or method extendable without the need to edit the file itself, you have a win, even without the compilation part.
Let me give you an example i recently wrote:
module Spider
extend self
def crawl(identity)
crawl_method = "crawl_#{identity.provider.to_s}"
protect_against_unsupported_messages(crawl_method)
send("crawl_#{identity.provider.to_s}", identity)
end
protected
def protect_against_unsupported_messages(method)
..
end
def crawl_facebook(identity)
...
end
end
This is an example of OCP on the method. We can extend the method all day long without modifying it. Instead, we just add a new private method to handle new providers, like so:
...
def crawl_facebook(identity)
...
end
def crawl_twitter(identity)
..
end
end
Now we have added new functionality without changing the public method!
However we still edited the module to change it, so let’s move onto OCP on a whole file/class.
class Animal; end
So if I wanted to add new methods onto the current animal definition:
Animal.exend(Tiger)
If I wanted to add new methods to this instance of animal:
Animal.new.include(Tiger)
And all of this is done in a new file, not animal.rb, thus extending functionality without modifying the existing code. Or Open/Closed Principle.
Hope that helps!