Xref: utzoo comp.lang.eiffel:1336 comp.object:2438 Path: utzoo!utgpu!news-server.csri.toronto.edu!rutgers!att!pacbell.com!ucsd!hub.ucsb.edu!eiffel!bertrand From: bertrand@eiffel.UUCP (Bertrand Meyer) Newsgroups: comp.lang.eiffel,comp.object Subject: Re: Inheritance and Information Hiding Keywords: oop, ood, Eiffel, inheritance, information hiding Message-ID: <485@eiffel.UUCP> Date: 25 Jan 91 18:28:56 GMT References: <1991Jan23.224203.3206@runx.oz.au> Organization: Interactive Software Engineering, Santa Barbara CA Lines: 298 From <1991Jan23.224203.3206@runx.oz.au> by chrisv@runx.oz.au (Chris Velevitch): > I disagree with allowing a class to access inherited features that are > not exported. It does not make sense that secret features are known to > heir of a class. If a feature is not publicly known, then how can you > know about the feature to use in the descendent class. > > Eiffel allows a class to be designed so it has no interface, which can > then be used in descendent classes. What use is a black box in which you > cannot put anything in or take anything out. This has come up before, of course. There is little to add to Steve Tynor's response. Let me, however, reproduce an extract from a paper called ``Static Typing in Eiffel'', of which a preliminary version was posted about two years ago. The current version is part of a book called ``An Eiffel Collection'', which is a collection of Eiffel-related articles, published by Interactive. The text is an ASCII-equivalent of something meant for typesetting, with heavy use of italics, boldface and some graphics. The general ideas, however, should survive e-mail translation. - -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- Extracted from ``Static Typing for Eiffel'' In: An Eiffel Collection Published by Interactive Software Engineering, 1991. Reference TR-EI-20/EC. Copyright B. Meyer, 1991 - -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 4.1 Rationale for the export rule Consider first the export-inheritance rule, which often surprises newcomers as an apparent violation of information hiding principles. How can a descendant override export restrictions of an ancestor, or hide what the ancestor exported? To obtain a better perspective on this rule it is useful to examine its application to what may be the archetypal example of inheritance. Assume PARENT is a class POLYGON and HEIR is a class RECTANGLE. The inheritance relation in this case is clear and natural. It is not absurd, however, to assume that class POLYGON has a procedure add_vertex (new: POINT) is -- Insert new vertex new at current cursor position do ... ensure nb_vertices = old nb_vertices + 1 end -- add_vertex For general polygons, this procedure may make sense. It is not, however, applicable to rectangles, whose class invariant should contain the clause nb_vertices = 4 The Eiffel solution is to ensure that class RECTANGLE does not export procedure add_vertex. This is the kind of case that led to situations such as [REFERENCE TO EARLIER EXAMPLE OF ERRONEOUS FEATURE CALL OF THE FORM x.f], with f being add_vertex. |- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- ---| | | | | | (Sorry, no graphics available for e-mail) | | | |(Please draw a figure with a bubble labeled POLYGON at the center | |top; below it, bubbles labeled FIXED_POLYGON and VARIABLE_POLYGON, | |with a single arrow from each of these bubbles to the top one; | |below FIXED_POLYGON, a bubble labeled RECTANGLE, with a bubble | |to FIXED_RECTANGLE.) | | | | | | Figure 1: Alternative inheritance structures | | | - -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -| Three policies are possible in the face of such cases. + Policy 1 considers that this use of inheritance is inadequate: RECTANGLE is not a subtype of POLYGON because not all operations applicable to the latter are applicable to the former. According to this policy, the proper solution (see figure 1) is to consider two heirs to POLYGON: FIXED_POLYGON and VARIABLE_POLYGON. Procedure add_vertex is a feature of VARIABLE_POLYGON only, whereas RECTANGLE inherits from FIXED_POLYGON. This way the Eiffel rule can be changed to require that every class should export all features exported by its ancestors. (Even with this policy, however, there is no reason to prohibit a class from exporting a feature hidden by a proper ancestor. This technique is commonly used in practical Eiffel programming and, as discussed above, entails no danger of type failure.) + Policy 2 considers that inheritance is used properly in this case, but only for half of its capacity: as a module enrichment mechanism, not as a subtyping facility. The definition of class RECTANGLE should be permitted, but not polymorphic assignments of the form p := r (with p of type POLYGON and r of type RECTANGLE). With such a policy the conformance rules governing such assignments, as outlined in section 11.3.1 of ``Object-Oriented Software Construction'' (Prentice- Hall, 1988) and given fully in ``Eiffel: The Language'' (Interactive Software Engineering, 1989, and Prentice-Hall, 1991), would have to be updated to exclude such cases. If polymorphic assignments are needed, then the inheritance structure should be redesigned as shown in figure 1. + Policy 3 is more liberal: it permits the above polymorphic assignments as long as it can be ascertained statically at reasonable cost, for any Eiffel system involving these classes, that no type failure may result; in other words, that no add_vertex operation may ever be applied to p or an entity that may become associated with p. This is the policy applied in Eiffel, for which the exact rules will be given below. What speaks in favor of policies 1 and 2 is that they are extremely easy to implement for the compiler writer. Restricting exports in descendant classes (policy 1) is trivial; restricting polymorphic aliasing (policy 2) is almost as immediate. These solutions would also have the advantage of quieting the theoreticians and removing the fears of prospective users. In short, all obvious arguments seemed to push the designers of Eiffel towards policy 1 or 2: convenience (since the same group is also in charge of an implementation) and ease of convincing future users. So it takes some dose of fortitude to choose policy 3 and stick to it. Why should this be the Eiffel policy? The reasons can only be understood by going beyond the purely theoretical discussions and considering the practice of object-oriented software construction. Policies 1 and 2 assume that programming is for gods. Gods never make any mistake. Those among them who aside from their other business indulge in the heavenly pleasures of object- oriented programming always get their inheritance structures right the first time around. In today's job market, however, most employers must resort to hiring programmers who are only demigods, or even in some cases (although they will deny it) mere mortals. Then we have to accept the need to work with inheritance structures that may already in be place and have been designed with less-than-perfect foresight. Assume for example that POLYGON has already been designed and has descendants such as CONVEX_POLYGON and CONCAVE_POLYGON. In other words, the existing inheritance structure does not take into account the difference between ``variable'' and ``fixed'' polygons. Then a new programmer comes in and has a need for RECTANGLE; writing it as an heir to POLYGON seems the obvious solution. Policy 1 precludes doing this without first reorganizing the entire inheritance structure. This is unrealistic in a practical development environment. True, we should accept the need for regular improvements of inheritance structures, reflecting improved understanding of software artefacts. But as anyone who has managed a class library in an industrial environment will appreciate, such evolution should be carefully planned and properly controlled. One cannot accept a situation in which new but legitimate uses of a class require constant changes in the design of existing libraries. Policy 2 is only marginally better. It does allow the programmer to define RECTANGLE as an heir to POLYGON but prevents him from using polymorphism in this case; it is not possible, for example, to define a data structure such as polygon_stack: STACK [POLYGON] and push a RECTANGLE object onto polygon_stack. This is particularly frustrating in an application that never even uses add_vertex. Again, the only solution is to redesign the inheritance structure. Policies 1 and 2 are overly inflexible. In contrast, the idea of policy 3 is to avoid bothering programmers with unnecessary restrictions when their use of inheritance cannot possibly lead to any type failure. The criterion to apply is obvious in the example given: the checker should reject any software system that contains both of the operations + p := r + p.add_vertex (...) Because checkers cannot realistically carry out flow analysis, it is not necessary to check whether or not these two instructions can be executed on the same control flow path; the mere presence of both in the same system is reason enough for the checker to reject that system. In other words, we accept without regret that the following sequence (with n an integer entity) will be rejected even though it cannot possibly lead to a type failure: if n >= 0 then p := r end if n < 0 then p.add_vertex (...) end The reason for this was discussed in [AN EARLIER] section [OF THE PAPER]: checkers should only be bound to perform ``reasonable'' controls. For a checker to recognize that the above is safe requires control flow analysis, whose cost, if it is at all possible, is not justified by the benefit. Using the terminology of [AN EARLIER] section, we are past the point of diminishing returns. As noted [EARLIER], type checking is always a pessimistic strategy; the only question is what degree of pessimism is acceptable. Rejecting extracts of the above form appears acceptable because it is hard to conceive of them being used in useful programs. This is not true, however, of the much stronger pessimism implied by policies 1 and 2, which leads to rejection of programs that, for the reasons discussed above, are useful and even needed. How policy 3 can be implemented at reasonable cost will be discussed [IN LATER SECTIONS]. 4.2 Further reflections on inheritance The argument made above for orthogonal export and inheritance mechanisms was that inheritance structures may be temporarily imperfect. The implied consequence is that eventually these structures will be perfected; then policies 1 and 2 could be implemented if we were willing to implement tougher library management procedures (forcing restructuring when appropriate, and requiring new heir designs to wait in the meantime). Unfortunately, even such an approach may not be viable. In practice we must probably accept that some inheritance structures will always be imperfect. Inheritance is the application to software engineering of the most fundamental of all scientific techniques: classification. The modern forms of the natural sciences, since Linnaeus, and of mathematics, at least since Bourbaki, were borne out of systematic efforts to classify natural and artificial objects; object-oriented programming attempts the same for software objects. Classification is humanity's attempt to bring a semblance of order to the description of an otherwise rather chaotic world. As anyone who has ever tried to use a botany book to recognize actual flowers knows, classifications never quite achieve perfection. Close as one can get to a fully satisfactory system, a few exceptions and special examples will remain. Moving from botany to zoology, the cliche' example of ostriches and flying illustrates these problems well. In class BIRD, it seems appropriate to include and export a feature ``fly''. But a descendant class such as STRUTHIO (the name of the genus that includes ostriches) should not export ``fly''. This is really the Eiffel form of selective inheritance, or rejecting part of your heritage: the rejected feature is still there internally, but not visible by your clients. (The discussion of selective inheritance in section 10.5.3 of ``Object-Oriented Software Construction'' is too restrictive in this respect: it seems to reject any form of selective inheritance, even though Eiffel has always supported the form discussed here.) In a case such as this one there does not seem to be a much better solution. Any other classification of birds, for example into flying and non-flying ones, would miss key criteria for distinguishing between various classes of birds, and would undoubtedly cause bigger problems than the original solution. Multiple inheritance from a class FLYING_THING would not help much. So far, only a minority of Eiffel users have admittedly reported ostrich-oriented programming as their major area of technical interest. But the need to deal with imperfect inheritance structures seems universal. Of course, we should strive to make these structures as complete and regular as possible. But we must also accept that unexpected special cases and exceptions may always occur. When they do and the programmer is only performing safe manipulations, the programming environment should help him, not put undue restrictions in his way. - -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- End of extract - -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- Bertrand Meyer bertrand@eiffel.com