omahony@swift.cs.tcd.ie (07/31/90)
> There will be a recipe in the library for a plain cake. If you want to write a > recipe for a fruit cake, you just write "Proceed as for a plain cake, but add a > handful of fruit in the final mixing" (this is not a recommendation for my > cakes!). You "inherit" the plain cake recipe from the library, and produce a > descendant recipe. This will create a different object, and it is the > _objects_ which are the "components"; they are created at run-time and behave > according to their class descriptions (i.e. the programs). You don't make a > good fruit cake by buying a plain cake from the shop and then trying to stuff > fruit into it when you get home. I like this analagy - now suppose you want to make a loaf of bread, or an omlette. Even though both of these involve gathering materials together in a bowl, agitating them and then applying heat (am I making you hungry!) - they are not instances of the class 'cake'. My basic point is that the use of classes/objects as reusable components suffers from similar (though not as severe) problems to procedure libraries. The author of the component divides the functionality into a fixed part (the class decription/ procedure body) and a variable part (the parameter list/ the inheritance mechanism). The problem occurs when a potential (re-) user of the component has an application for about 90% of the fixed part. Comments?
ogden@seal.cis.ohio-state.edu (William F Ogden) (08/01/90)
In article <7057.26b57e9c@swift.cs.tcd.ie> omahony@swift.cs.tcd.ie writes: >>There will be a recipe in the library for a plain cake.If you want to write a >>recipe for a fruit cake, you just write "Proceed as for a plain cake, but add >>a handful of fruit in the final mixing" (this is not a recommendation for my ... >I like this analagy - now suppose you want to make a loaf of bread, or an >omlette. Even though both of these involve gathering materials together >in a bowl, agitating them and then applying heat (am I making you hungry!) > - they are not instances of the class 'cake'. > >My basic point is that the use of classes/objects as reusable components >suffers from similar (though not as severe) problems to procedure libraries. ... >Comments? Actually there is a good example of reusability lurking here, but not in the recipes. The designer of kitchen equipment looks at the omelet, bread, cake, etc. problem and notices that they all involve agitating ingredients in a bowl. The potential for reusability is clear (and vital to marketing, since cooks don't really want a million gadgets in their kitchens) and thus the mixer is born. Similarly, the `applying heat' problem is solved by the highly reusable stove. The containment of ingredients while heating problem has special features that lead to the less reusable bread pan, cake pan, frying pan, etc. An important point to note here is the independence of the reusable components. You don't inherit a stove with a built-in bread pan, notice that it's 90% appropriate for the cake heating problem, and modify it to create a stove with a built-in cake pan. Moreover, you rarely find a stove with a built-in mixer. Another point to note is that recipes respect the integrity of reusable components. They don't ask the cook to reshape the blades in the mixer or cut a small hole in the oven door. /Bill
rick@tetrauk.UUCP (Rick Jones) (08/03/90)
In article <7057.26b57e9c@swift.cs.tcd.ie> omahony@swift.cs.tcd.ie writes: > [I wrote]: >> There will be a recipe in the library for a plain cake. If you want to write a >> recipe for a fruit cake, you just write "Proceed as for a plain cake, but add a >> handful of fruit in the final mixing" (this is not a recommendation for my >> cakes!). You "inherit" the plain cake recipe from the library, and produce a >> descendant recipe. This will create a different object, and it is the >> _objects_ which are the "components"; they are created at run-time and behave >> according to their class descriptions (i.e. the programs). You don't make a >> good fruit cake by buying a plain cake from the shop and then trying to stuff >> fruit into it when you get home. > >I like this analagy - now suppose you want to make a loaf of bread, or an >omlette. Even though both of these involve gathering materials together >in a bowl, agitating them and then applying heat (am I making you hungry!) > - they are not instances of the class 'cake'. > >My basic point is that the use of classes/objects as reusable components >suffers from similar (though not as severe) problems to procedure libraries. >The author of the component divides the functionality into a fixed part >(the class decription/ procedure body) and a variable part (the >parameter list/ the inheritance mechanism). The problem occurs when a >potential (re-) user of the component has an application for about 90% >of the fixed part. There are two problems here. The last paragraph suggests that the author of a component decides what may inherit from it, and how. In other words the scope of re-use is being fixed in the design of the component. Although some object-oriented languages encourage this viewpoint (C++ being one of the worst offenders, IMHO), for re-use to be valuable in the long-term classes need to be re-usable in ways which the original author never envisaged. The second problem gets back to an issue I mentioned earlier in my previous article, i.e. what is inheritance? The nice theoretical, text-book concept says that the sub-class has all its parent's properties, plus some more. Thus an object of the sub-class must be prepared to do everything its parent can do - it can't refuse to inherit some of its parent's behaviour. This means the object exhibits total conformance to its parent, and allows a compiler to treat classes and types as synonymous making type conformance checking nice and easy. This approach doesn't always reflect reality, though. You often do want an inheritance which says "A is like B except ...", meaning the sub-class is NOT going to conform to some aspects of its parent. An example I was quoted recently was the case of a square inheriting from a rectangle; not many people would argue with that concept. But suppose the rectangle supports an "elongate" operation which changes its aspect ratio - you can't do that to a square without changing it back into a rectangle. The implication is partial inheritance. The result is that the class inheritance tree and the type conformance tree are not the same, and static type checking in a compiler becomes a whole new ball game. The only convincing statement I've seen which seriously addresses this issue is a recent re-posting by Bertrand Meyer in comp.lang.eiffel of a paper originally written about a year ago. The article was posted in two parts because of size, and the references are <385@eiffel.uucp> & <386@eiffel.uucp>. There is also an article which covers the Eiffel type system in general, ref. <387@eiffel.uucp>. I won't waste time & bandwidth quoting from these articles here, If you're interested in this problem, I seriously suggest you read them. -- Rick Jones You gotta stand for something Tetra Ltd. Maidenhead, Berks Or you'll fall for anything rick@tetrauk.uucp (...!ukc!tetrauk.uucp!rick) - John Cougar Mellencamp
rick@tetrauk.UUCP (Rick Jones) (08/03/90)
In article <82593@tut.cis.ohio-state.edu> William F Ogden <ogden@cis.ohio-state.edu> writes: > [the OOP/recipe analogy, etc ... ] > >Actually there is a good example of reusability lurking here, but not in >the recipes. The designer of kitchen equipment looks at the omelet, bread, >cake, etc. problem and notices that they all involve agitating ingredients >in a bowl. The potential for reusability is clear and thus the mixer is born. >Similarly, the `applying heat' problem is solved by the highly reusable >stove. etc. Perhaps the re-usable kitchen equipment which turns the recipes into food is more analagous to the compiler which mixes up the program (I chose the words carefully!) and the computer which cooks (executes) it for the user to eat (use). After all, compilers and computers are exceedingly re-usable, to mis-quote Mr. Kipling. -- Rick Jones You gotta stand for something Tetra Ltd. Maidenhead, Berks Or you'll fall for anything rick@tetrauk.uucp (...!ukc!tetrauk.uucp!rick) - John Cougar Mellencamp
steve@taumet.com (Stephen Clamage) (08/05/90)
rick@tetrauk.UUCP (Rick Jones) writes: >There are two problems here. The last paragraph suggests that the author of a >component decides what may inherit from it, and how. In other words the scope >of re-use is being fixed in the design of the component. Although some >object-oriented languages encourage this viewpoint (C++ being one of the worst >offenders, IMHO), for re-use to be valuable in the long-term classes need to be >re-usable in ways which the original author never envisaged. I'm not sure of what you are accusing C++ here. It is possible to define a C++ class in such a way that it is hard to re-use, but it is not necessary to do so. Using C++ terminology: Some languages, such as Eiffel, make all member functions virtual, and so can be adjusted for any derived class, whatever the original desinger envisioned. In C++ you may make all functions virtual if you wish. If run-time efficiency is more important, you can eliminate the overhead of virtual function calls. A later user may acquire the source code, change declarations in just one place per function to add the keyword "virtual", and recompile. >an object of the sub-class must be prepared to do everything its parent can do >- it can't refuse to inherit some of its parent's behaviour. This means the >object exhibits total conformance to its parent... >classes and types as synonymous making type conformance checking nice and easy. >This approach doesn't always reflect reality, though. You often do want an >inheritance which says "A is like B except ...", meaning the sub-class is NOT >going to conform to some aspects of its parent.... Again, you can do this in C++. It is possible to eliminate some of a base (parent) class's functionality within a derived (sub) class. You simply override the base class function in the derived class with an empty function which is private. While I understand your example, I don't agree it is good design. You have an "extend" operation for a rectangle which changes its aspect ratio, something not possible for a square. I suggest instead something like this inheritance graph: quadrilateral ______|______ | | rectangle trapezoid ____|____ | | extensible_rectangle square Now there is no need to suppress a parent's functionality. -- Steve Clamage, TauMetric Corp, steve@taumet.com
dl@g.g.oswego.edu (Doug Lea) (08/05/90)
> From: steve@taumet.com (Stephen Clamage) > >rick@tetrauk.UUCP (Rick Jones) writes: > >This approach doesn't always reflect reality, though. You often do want an > >inheritance which says "A is like B except ...", meaning the sub-class is NOT > >going to conform to some aspects of its parent.... > > Again, you can do this in C++. It is possible to eliminate some of a > base (parent) class's functionality within a derived (sub) class. You > simply override the base class function in the derived class with an > empty function which is private. This is a TERRIBLE thing to do in any OOPL (like C++ and Eiffel) that attempts to maintain some relation between subclassing and subtyping. As Rick Jones says, it breaks conformance: If some base class Base contains some method m that returns an int, perhaps with some promises (postconditions) about the nature of the return value and/or the resulting state of the object, and a derived class Derived redefines m to simply abort execution or somesuch, then any polymorphic function accepting argument b of type Base or subclass thereof, and calls b.m may no longer be able to meet its own postconditions, since its implicit preconditions (i.e., that b is an object containing method m with specified behavior) are broken. Rick is right that programmers do often wish to create a new class B that is like some existing class A, except that it does not contain one or more methods/features. Probably the most common case of this is during the honorable activity of `retrospective abstraction', where you find that a class was overdesigned and you need something with fewer features; or that two previously unrelated classes could benefit by being linked via a new common ancestor. For among the simplest (perhaps least realistic) examples, suppose you HAVE a Deque class lying around, but you NEED a simple Stack class. The Deque contains methods push, pop, empty, rearpush, and rearpop. For both safety and coherence reasons, you don't want to just use the Deque as a Stack (since you want to guarantee that pushrear is NEVER called). But if you create class Stack as a subclass of Deque by nulling out rearpush and rearpop, you open up the Stack component to misuse: Sending a Stack to any function expecting a Deque might result in unexpected behavior like a core dump. In fact, it is easy to see that Stack is a SUPERclass of Deque. A Deque may be safely used wherever a Stack is expected. No `common' OOPLs allow you to RETROSPECTIVELY state this in any way short of redesigning the heirarchy. However, support for such constructs is not a completely novel idea: Retrospective superclassing is nearly the same notion as `views' in OBJ (see any of Goguen's recent papers) and LILEANNA (an Ada tool/preprocessor described by W. Tracz in his in-progress thesis) and `reductions' in Alberich (see July '90 SIGPLAN notices article by Paaki et al). It is also possible to get this effect in languages based on implicit conformance-checking rather than explicit subtyping/subclassing, e.g., Quest (see Cardelli's DEC-SRC TRs), and to some extent, Emerald/Jade (see Raj & Levy's ECOOP '89 paper). What might an OOPL supporting `views' (I'll stick to that term) look like? First, and most importantly, it MUST allow (even nicer: mandate) the separation of type specification (i.e., declaration of method argument and return types, perhaps with pre- and post-conditions) from class implementation (i.e., member data and code needed to carry out these specs). It is possible to maintain this separation via the convention of using `Abstract' or `Deferred' classes in C++, Eiffel, and other languages, or via more direct support found in Johnson's Typed Smalltalk (TS) and Emerald. This separation is necessary in order to ensure that a new view is properly self-contained. For example, if the Deque class, above, mixed specification and implementation, it might be the case that Deque::push was implemented by calling Deque::rearpush when the Deque is empty. A Stack view of the Deque that omits rearpush would then fail to even make sense. The consensus these days seems to be that such separation is a Good Thing anyway: it allows specifications to be realized via multiple implementations with different performance characteristics; it allows independent reuse of either specifications or implementations; and allows independent extensions of both types and implementations. However, separation of specification and implementation can make it hard to reuse implementations without further aids. A composition mechanism similar to that of Emerald/Jade that is equally useful for both composing and splitting apart implementations would be a valuable adjunct. Alternatively, the implementation side might even employ a form of prototyping/delegation (as in SELF) instead of subclassing or composition: Separating specifications from implementations allows the design and coding tools/constructs to vary independently. There are a number of other interesting issues as well. Among them: *) Renaming: Suppose the Deque method to place things on the front was called `frontpush', but you'd like it to be called just `push' in the Stack view. Is this OK? *) Multiple views: A Deque can be view'd as a Stack in either of two ways (using the Deque push/pop pair OR the rearpush/rearpop pair to perform Stack push/pop). Can more than one view be supported? *) Structure: Programs written with views might be hard to read since new superclasses can be declared on the fly. Tools are needed to reconstruct/fake the underlying hierarchical design. *) And a host of implementation matters ... Are views important enough constructs to warrant creation of new languages or tools? (All of the above COULD be incorporated into a front-end design tool/preprocessor for C++, Smalltalk, SELF or nearly any other OOPL.) -Doug -- Doug Lea, Computer Science Dept., SUNY Oswego, Oswego, NY, 13126 (315)341-2688 email: dl@g.oswego.edu or dl@cat.syr.edu UUCP :...cornell!devvax!oswego!dl or ...rutgers!sunybcs!oswego!dl
rick@tetrauk.UUCP (Rick Jones) (08/06/90)
In article <382@taumet.com> steve@taumet.com (Stephen Clamage) writes: >rick@tetrauk.UUCP (Rick Jones) writes: > >>The scope of re-use is being fixed in the design of the component. Although some >>object-oriented languages encourage this viewpoint (C++ being one of the worst >>offenders, IMHO), for re-use to be valuable in the long-term classes need to be >>re-usable in ways which the original author never envisaged. > >I'm not sure of what you are accusing C++ here. Sorry! I'm not trying to start an object (class?) war. Ultimately, anything is possible in any language, but I do believe that the characteristics of a language influence the design of the software; in fact, I take the view that coding is just the final step in the design process. Yes, you _can_ make everything virtual, but how many C++ programmers actually _do_, in anticipation of something they haven't yet thought of? This isn't to say I'm accusing C++ programmers of being wrong, per se, either. Perhaps it's just human nature to use a tool in the most obvious way which the tool itself suggests; if you make a corkscrew in the shape of a hammer, someone is bound to open a bottle by smashing the top off! (this isn't a good analogy, but it was off the top of my head). >>This approach doesn't always reflect reality, though. You often do want an >>inheritance which says "A is like B except ...", meaning the sub-class is NOT >>going to conform to some aspects of its parent.... > >Again, you can do this in C++. It is possible to eliminate some of a >base (parent) class's functionality within a derived (sub) class. You >simply override the base class function in the derived class with an >empty function which is private. But it's not that simple if you are going to use polymorphic references, which for me is what an effective class-based design is all about. If you assign an object of the sub-type to a pointer variable of the parent type, the compiler will assume that all the parent's functionality is available on all objects which may be attached to it. >While I understand your example, I don't agree it is good design. You >have an "extend" operation for a rectangle which changes its aspect >ratio, something not possible for a square. I suggest instead something >like this inheritance graph: > > quadrilateral > ______|______ > | | > rectangle trapezoid > ____|____ > | | >extensible_rectangle square The example is certainly contrived, but your solution illustrates my point. Suppose the inheritance tree was already in place, without square and with a single (extensible) rectangle, and your task was to implement square. Your solution requires a re-arrangement of the existing hierarchy. Although ultimately desirable, it is not helpful if it has to be done in order to solve the immediate problem - it's making re-use more difficult. Good re-use is achieved by finding and implementing the "nearest fit" to what is already there, even if not perfect. The ability subsequently to re-arrange parts of the hierarchy without having to change things which derive from them is equally important. There will be many real problems of this type which are far less trivial. (Ducking flames: holiday period 3 weeks, expiry period 2 weeks :-) -- Rick Jones You gotta stand for something Tetra Ltd. Maidenhead, Berks Or you'll fall for anything rick@tetrauk.uucp (...!ukc!tetrauk.uucp!rick) - John Cougar Mellencamp