From: Reimer Behrends Date: 2002-09-06T20:50:36+09:00 Subject: Re: Ruby aesthetics Christian Szegedy (szegedy@t-online.de) wrote: [...] > In C++, you don't have to know the implementation of the base class, > i.e., you only need to know the interface. If you declare a new > data member, then it will hide the original one, so you don't have to > worry about the private part of all ancestor classes, if you define > a new data member. I am well aware of that. However, as I said, it's a religious issue that has been debated to death over the years (see the comp.object or comp.lang.eiffel archives, for instance). It is essentially a question of methodology. Let me present you with the other side of the coin. When you use private methods to encapsulate part of a class against access by its descendants, you have failed to identify a substructure of that class. Given that substructure, it could be implemented in a class of its own, and accessed through its public methods, achieving just the same purpose. For instance, we may discover that we have an instance variable that is an array. But the only operations that we are using on it are push, shift, and whether the array is not empty. Essentially, we have implemented an ad-hoc queue and now proceed to make the array private, accessible only through the three operations (protected or public) 'get', 'put', and 'has_data?'. But we do not really need to make anything private here: we could instead replace that array with a queue object implementing just the three operations 'get', 'put', and 'has_data?' In essence, we have identified a structural abstraction and factored it out. Moreover, private methods can actually be harmful. To begin with, they encourage the above phenomenon: pieces of loosely related functionality within a class. In software engineering, this is called "low cohesion", and generally considered undesirable, because it creates unnecessary interdependencies and replicated functionality. The other harmful aspect is that they may prevent legitimate uses of inheritance. It is often not possible to redefine or add functionality without having access to the data structure. Consider that we find out in our queue example that we want to implement operations 'get_pair' and 'has_pair?', where 'get_pair' retrieves a pair of values and 'has_pair?' tests if there are at least two more elements available in the queue. We cannot implement 'has_pair?' without first counting every element in the queue by retrieving them and then putting them back in, or hacking in some additional code to transparently pull out the first element. However, if we first factored out the functionality of the queue, we can now subclass the implementation of the queue separately (the queue having no private members), and substitute the new implementation for the old one. (See http://citeseer.nj.nec.com/257843.html for a more elaborate discussion.) > In Ruby, the situation is even worse, as all methods may introduce new > data memebers, so you have to check the body of all methods, if you > want to avoid name collisions for sure. Why would somebody create such a mess in the first place? Information hiding does not exempt the implementation side from documentation and clean design. > It may be OK for small projects, but if you have to work in a large > class hierarchy written by someone else, it can become hopeless, > and you can only hope that nothing goes wrong. If the parent class you're inheriting from is too convoluted to be understood, then you have a far bigger problem on your hand. I'd suggest that it isn't safe to inherit from the class at all, and that you should use aggregation instead. The reason is that by inheriting you make the promise that you implement the same contract as the parent class, and that you do typically by reusing part of the implementation. This is far stronger than the client-supplier relationship. If you don't know what you are doing there, don't do it at all. For what it's worth, if you introduce private variables, the problem is only transferred to the functions accessing it. Redefine 'put' in the queue example above by accident, and you have the same problem; except that it's more subtle, since two pieces of code accessing the same variable are going to show up on your unit tests much quicker than one errant function. > Besides, I think it is a clear violation of the separation of > mplementation and interface. The relationship between subclass and parent is a different one from that between client and supplier. Nothing can be assumed between client and supplier, but the subclass is-a parent, and needs to know a whole lot more about it. The justification for information hiding in the client-supplier relationship -- to hide design decisions from the client so as not to create inadvertent dependencies -- does not hold for the parent-subclass relationship, because there already is an interdependency: the subclass needs to know about those design decisions, as opposed to the client. Conversely, the impact is much smaller: the parent has much fewer subclasses than potential clients. This leads us to the real underlying problem: We have a trade-off between two alternatives: Making classes fully extensible by inheritance or limiting that extensibility. In fact, if all variables were private, then we could essentially do no more with inheritance than composing the functions of the parents, but not add any real new and non-trivial functionality. And we don't really need inheritance for that -- it would become more or less superfluous. The openness, the extensibility of classes through inheritance plays a critical part in object-oriented programming. It allows us to keep the interfaces of base classes fixed and unchanging. If we need additional functionality, we derive a subclass and add the functionality there. If we can't do that, then we have to go back to the base class and augment that, leading to the fragile base class problem (even adding a single method to access data that subclasses aren't aware of can break entire caching schemes, for instance) and possibly even changing public interfaces. Inheritance is essentially our safety-valve so that base classes remain stable and that changes are localized. Conversely, if we shut down that safety valve, we pay our price in terms of more frequent changes to the base classes, and the resultant instability of the entire system. Yet that doesn't mean that we want our subclasses to mess around with instance variables introduced by their parents at will. But rather than throwing out the baby with the bathwater -- disallowing any and all changes -- it is much more sensible to just disallow those changes that are actually illegitimate. For that, we distinguish the abstract behavior -- that which is visible to external clients -- from the concrete behavior: that which is internal to the class. Note that writing unit tests for the concrete behavior of a class is not hard, since instance_eval allows us to peek inside a class with ease; but also that private variables can make writing unit tests for the concrete behavior of a subclass a real pain. Anyway, once we've got unit tests for the concrete behavior for the parent class, we can apply the same tests to the subclass, to see if the behavior has been preserved. As I said, it is largely a religious issue, and this has become much longer than I wanted it to be. But there is more to the problem than you think. And some programming languages have actively chosen _not_ to have an information hiding barrier between subclass and parent for reasons such as the ones outlined above. Reimer Behrends