Do Ruby's "Open Classes" break encapsulation?
In Ruby, programmers are allowed to change predefined classes. So a really bad programmer could do something like:
class String
def ==(other)
return true
end
end
Obviously, almost no one would be quite this dumb, but the idea that more subtle changes to a predefined class could cause problems in already-working code seems to me to violate the principle of encapsulat开发者_Go百科ion.
Four questions:
- First, does this, in fact, violate the OO principle of encapsulation?
- Second, is there a way, as a programmer, that I can guarantee in my code that I am working with an unmodified version of a class?
- Third, should I ever be "opening" classes in my code, for any reason?
- Finally, how is this sort of thing handled in a large-scale, production coding environment? In other words, do people in the programming industry actually do this in code that others will use? Or even if they don't, how do you ensure that some plugin author somewhere isn't doing something like this that will ruin an essential part of your program?
I know this is a somewhat subjective question, but I'd really like to know how the wider programming community feels about this so called "monkey patching."
First, does this, in fact, violate the OO principle of encapsulation?
Yes.
Second, is there a way, as a programmer, that I can guarantee in my code that I am working with an unmodified version of a class?
Not yet. Classboxes in Ruby 2.0 are (hopefully) going to be the solution.
Third, should I ever be "opening" classes in my code, for any reason?
Only as a last resort.
You should never monkey-patch your own classes. There's simply no point. You control them, you can make them do what you want in the first place.
You should never monkey-patch classes in a library. (The exception to this rule are libraries whose sole purpose it is to monkey-patch something, e.g. Marc-André Lafortune's backports
library, which monkey-patches Ruby 1.8.6, 1.8.7, 1.9.0 and 1.9.1 with as much as possible of the functionality from Ruby 1.9.2.) You may provide an add-on library which provides monkey-patches that make it easier to use your library (e.g. you have an encryption library which provides a Kryptonite.encrypt(str)
method and you provide an add-on String#encrypt
method), but that add-on should be in a separate library, which the user needs to explicitly require
. It should be fully optional.
You should not monkey-patch core classes. This refers to classes like Array
or Symbol
in Ruby, but for a Rails library, I would also include classes like ActiveRecord::Base
under the "core" label. (Same caveat as above. E.g. in versions of Rails before 3.0, there was no well-defined plugin API, monkey-patching was the only way to extend Rails. Without people who broke this rule, there would never haven been any plugins, and Rails would never be where it is now.)
Try inheritance first. Try composition (wrappers, proxies, facades, adapters, …) first. Try refactoring first. Try helper objects first. Only if that doesn't work, turn to monkey-patching.
Be respectful when you monkey-patch: if you are adding a new method, make sure it doesn't already exist, and deal with it if it does (e.g. call it from your method). If you are wrapping an existing method, make sure that if somebody else has already wrapped it, their wrapper gets called and that when somebody wants to wrap it afterwards, your wrapper allows that. (In particular, this means you must preserve the method's existing contract.)
Put your monkey-patches in a mixin, if at all possible. That way, they show up in the inheritance chain, which will give anybody who tries to debug the code a fighting chance to figure out what is going on. Put your monkey-patches in separate, obviously named files.
Finally, how is this sort of thing handled in a large-scale, production coding environment? In other words, do people in the programming industry actually do this in code that others will use? Or even if they don't, how do you ensure that some plugin author somewhere isn't doing something like this that will ruin an essential part of your program?
Don't work with "really bad programmers", as you call them.
As simplistic as this sounds, that's basically what it boils down to. Yes, of course, you can write tests, do code reviews, practice pair programming, use static analysis tools, run your code with warnings enabled (e.g. the code you posted in your question will generate a warning: method redefined; discarding old ==
). But for me, that's all something that a not-really-bad-programmer would do anyway.
- In some cases yes. If you follow the paradigm of one class is responsible for one job and one job only then uses of reopening classes will often (though not necessarily) break encapsulation. It seems that this however not the tradition in ruby. For example the Array class acts as a list, array and a stack so the stdlib doesn't seem to adhere to strict encapsulation either. Matter of taste I guess.
- I don't know of any way. Maybe someone else will come up with something.
- My opinion is that I'd avoid doing it if you're writing a library that others will use. If you're writing an application and the need comes (trivial example: you need to have a mean method for arrays of numbers - it's a choice between added readability and not monkeypatching) I'd go for it.
- The most (in)famous real world monkeypatcher are rails. So often it's good to document especially well changes to core classes. And yes testing helps.
First, does this, in fact, violate the OO principle of encapsulation?
Encapsulation is there to hide implementation details away, not dictate how a class should be used. In ruby, you typically respect private variables, and when you want to get around that you know what you are doing is a hack that may break when you upgrade the library. I would say ~90% of the time that I will break encapsulation is in testing situations, and I find it very irritating when I can't do that in other languages
Second, is there a way, as a programmer, that I can guarantee in my code that I am working with an unmodified version of a class?
That would kind of violate the whole "open class" thing, wouldn't it ;-)
Third, should I ever be "opening" classes in my code, for any reason?
Think of it as a "last resort" type of thing. Usually the answer would be "no", since you control the class definition there shouldn't be a need to. Adding stuff to the singleton class of specific instances is a totally other story though ;-)
Finally, how is this sort of thing handled in a large-scale, production coding environment? In other words, do people in the programming industry actually do this in code that others will use? Or even if they don't, how do you ensure that some plugin author somewhere isn't doing something like this that will ruin an essential part of your program?
As a rule, if a library is opening up another library it should be done as a last resort (i.e. you can't accomplish the same thing through normal OO features), and when they do it they should make damn sure it hasn't already been opened by someone else. There are tricks you can do to make the process safer, like the old alias_method_chain, or the new stuff around using mixins and calling super.
That being said, in ruby that happens all the time, in rails it is how you get plugins.
I work on a product with a 250k loc codebase, and we have monkey patched loads of things. We also practice TDD (and have a 1:1.5 ratio in loc to test loc), and run all tests before committing to the mainline repository. All monkey patches are in files with their purpose clearly labelled in "config/initializers", and all of them are fully tested. Been working there for a year now, and at least in that time we have never run across a monkey-patch related issue.
That being said, it is the best team I have ever been on, and we are all extremely committed to extreme programming. If either of those were not the case, I don't think rails would be a good idea. You need to trust your team to do the right thing in a language with as much power as ruby, and have as many checks and balances as you can.
Short answer: Yes. Longer answer: kind of. The point of encapsulation is indeed to prevent this sort of thing from happening, however encapsulation can be violated in other languages, albeit via more difficult means.
Test cases, perhaps, but again, Ruby is notorious for having quirks when writing applications, especially when using heavy frameworks like Rails, which was guilty for polluting the global namespace and causing strange results on unexpected occasions until version 3 came out.
I'm not sure what you mean by this question.
In the real world, developers decide which packages to use, preferably heavily-tested ones.
As an extra note, other developers can and frequently do break programs they use. Encapsulation isn't a software feature that locks out access to parts of your application, it's a language feature that helps prevent coders from directly messing up your code.
My current experience of Ruby tells me that:
- Yes, since you can add a method to return private attributes of external classes: programs can break encapsulation at will.
- No, there is nothing you can do to prevent that, it's a language feature.
- Yes, sometimes it can seem useful or at least produce good looking code to add methods to existing classes: for example adding applicative filtering methods to String or Array. In any case, create these methods in modules and include those. I particularly like the way it's done in ActiveRecord, read their sources it's all nice and clean.
- In large scale code, unless you have good unit tests and disciplined developers, consider switching to a less brittle language (yes I know some of you will disagree).
For part 4., there's the principle of "Select isn't broken". If lots of people are using the plugin you're using, odds are if a plugin does something bad, then someone would have spotted it.
Then again, you could be using a combination of plugins no-one else does.
精彩评论