davidm@cimshop.UUCP (David Masterson) (07/27/89)
I'm working in an area doing C++ development that has several processes communicating with one another. The messages shipped back and forth tend to be asynchronous, so a receiving process cannot know what the message is or who it came from until it looks at the message data. My question is this: What is the appropriate method of taking a generic object (Message) and turning it into an object of the type that is embedded in the message (say, Employee)? Should this be done through virtual functions and subclassing of the Message object? Or is it more appropriate to have the Message object look at itself and construct an object of the appropriate type (perhaps some sort of array of pointers to object constructors)? The fundamental question hinges around the lack of context of the incoming message. Therefore, the message user must look at the data in order to determine what to coerce (definition?) the message into. Is this appropriate or is there a better way? aTdHvAaNnKcSe David Masterson uunet!cimshop!davidm or DMasterson@cup.portal.com
dog@cbnewsl.ATT.COM (edward.n.schiebel) (07/27/89)
From article <444@cimshop.UUCP>, by davidm@cimshop.UUCP (David Masterson): > The fundamental question hinges around the lack of context of the incoming > message. Therefore, the message user must look at the data in order to > determine what to coerce (definition?) the message into. Is this appropriate > or is there a better way? > The same problem arises reading objects from disk. Until you look at the data, you don't know what class of object to instance so it can read itself. It seems to me, this is one place you still need a header/type field and a switch statement. Ed Schiebel AT&T Bell Labs att!vilya!dog
jaa@hutcs.hut.fi (Jari Alasuvanto) (07/28/89)
In article <1273@cbnewsl.ATT.COM> dog@cbnewsl.ATT.COM (edward.n.schiebel) writes: >The same problem arises reading objects from disk. Until you look at >the data, you don't know what class of object to instance so it >can read itself. It seems to me, this is one place you still >need a header/type field and a switch statement. > Or you can hide peeking at your data inside a library code as done in OOPS (NIH library). There you can read objects from disk and either give an expected class as an argument (the library code verifies it) or not give it. There are some problems in that, however: 1) Classes are not objects and there is no common base class in standard C++. A work-around is needed (as done in OOPS) 1) It may be hard to know later on what actually came - and you may have to do a case later on In our work we use the OOPS methods for writing the C++ objects into files to implement a simple persistent workspace as well as to exchange them via email - works fine. We need to tag the mail messages containing C++/NIH objects in order to be able to read also plain email. Jari Alasuvanto Lab. of Information Proc. Science, Helsinki Univ. of Techology, Finland Internet: jaa@hutcs.hut.fi Bitnet: jaa%finhutcs.bitnet tel: +358-0-451 3236 fax: +358-0-465 077
hardin@hpindda.HP.COM (John Hardin) (07/28/89)
dog@cbnewsl.ATT.COM (edward.n.schiebel) writes: > From article <444@cimshop.UUCP>, by davidm@cimshop.UUCP (David Masterson): > > The fundamental question hinges around the lack of context of the incoming > > message. Therefore, the message user must look at the data in order to > > determine what to coerce (definition?) the message into. Is this appropriate > > or is there a better way? > > > The same problem arises reading objects from disk. Until you look at > the data, you don't know what class of object to instance so it > can read itself. It seems to me, this is one place you still > need a header/type field and a switch statement. >---------- Reading objects from disk is exactly the subject of one of the demo programs that comes with Borland's Turbo Pascal 5.5 (with object oriented extentions). I haven't looked at it in detail, but I believe they derive classes from their stream class and then the derived classes are somehow "registered" with the stream class. A get from the stream then first reads the next object type and calls the "get" method of that class. This embeds the switch within the steam's "get" method. BTW, I believe the demo is called CARDFILE. Has anyone else looked at this? If you have, what do you think of it? John Hardin hardin%hpindgh@hplabs.hp.com --------
davidm@cimshop.UUCP (David Masterson) (07/29/89)
>The same problem arises reading objects from disk. Until you look at >the data, you don't know what class of object to instance so it >can read itself. It seems to me, this is one place you still >need a header/type field and a switch statement. > Agreed. In fact, my messages deal with a database server to get information from the database. I think a function pointer table is a cleaner way of implementing this in C, but can you implement an object construction pointer table in C++? By the way, would the object/function that reads from the external entity be a separate thing or should it be a base class that looks at itself and casts to the appropriate derived class (can that even be done?!?)? Consider: 1. All external objects have a common header in order to determine what type of object they are. 2. A generic object would get the external object and decide what to do with it. 3. Once it decides, it makes (casts) itself a specific object of the appropriate type. What problems are there in this method? I think I see a few, but I'm curious about what others think? (I'm looking for ammunition ;-) David Masterson uunet!cimshop!davidm
dog@cbnewsl.ATT.COM (edward.n.schiebel) (07/29/89)
> ... By the way, would the object/function that reads from the > external entity be a separate thing or should it be a base class that looks at > itself and casts to the appropriate derived class (can that even be done?!?)? As a base class, I don't think so This base class doesn't have enough memory allocated to hold derived classes. You need an object that knows about all class types it may see, read the header, then create one on the free store so it may read itself. Ed Schiebel AT&T Bell Labs att!vilya!dog
raph@tigger.planet.bt.co.uk (Raphael Mankin) (08/01/89)
davidm@cimshop.UUCP (David Masterson) writes: >I'm working in an area doing C++ development that has several processes >communicating with one another. The messages shipped back and forth tend to >be asynchronous, so a receiving process cannot know what the message is or who >it came from until it looks at the message data. My question is this: >What is the appropriate method of taking a generic object (Message) and >turning it into an object of the type that is embedded in the message (say, >Employee)? Should this be done through virtual functions and subclassing of >the Message object? Or is it more appropriate to have the Message object look >at itself and construct an object of the appropriate type (perhaps some sort >of array of pointers to object constructors)? >The fundamental question hinges around the lack of context of the incoming >message. Therefore, the message user must look at the data in order to >determine what to coerce (definition?) the message into. Is this appropriate >or is there a better way? This is a fundamental problem of strongly typed languages. Ada pretends that it is not there but really has a worse problem than C++. Suppose you have a file with many variant records (unions) in it. You cannot allocate buffer space for the record until you know what type it is, and you cannot know what type it is until you have allocated space and read the record in. Ada does not permit you to read into a char* or (void*) and then cast the data when you have examined the non-variant header. The only way I have found of doing this in C++ is to go back to the old fashioned way of using a switch with a different cast in each branch. This is messy verbose and generally nasty. The only satisfactory solution is to allow types to be first-class objects so that we could have 'variable' casts. e.g. Type t = typelist[discriminant]; return (t)record_area; The run-time costs of this are horrible and I am not really suggesting it as a solution in C++. Does anyone have a sensible solution? Raphael Mankin raph@planet.bt.co.uk
davidm@cimshop.UUCP (David Masterson) (08/03/89)
In a previous message, I talked about the mechanics of changing general messages into specific objects in order to turn a bit-stream (say, from a network) into a known object. Most seem to feel that it is the responsibility of an object to write itself out and read itself back in (construct itself). Obviously, the message processor (disk reader) has to get clues from the bit-stream as to what type of object is coming in and, therefore, switch to the appropriate object construction mechanism. The problem is that this leads to inefficiencies because of the need to move bits around more than once in order to get it into a usable form (move bits to a buffer, determine object type, construct object type, and move bits into constructed area). An implication has been that there are well-known ways of dealing with such things in C that C++ does not handle as well. Perhaps, there is a need to define what types of data are capable of being written out of a program and (by implication) what types of data can be read into a program. Since an object is represented by its data and methods and methods are of no use outside a process (cannot be written out), maybe it is better to not define external data as an object (at least in C++). An object can be constructed to contain this external data, but the data itself is not an object (therefore, no virtual function pointers, esoteric object types, etc.). Therefore, anything passing between C++ programs (via shared memory, messages, disk files, databases, etc.) can only be data structures. Many people would say that this is obvious, but others might not. What I'm looking for is to see if people agree with this formalization of definition and its implications for understanding the problem. Now that I've made that point, let me jump back into message passing in a multitasking system and my main question. Let's assume a situation in order to clarify the problem. In a system where a number of processes may be communicating with each other via interprocess messages, a single process can not make an assumption as to what message is coming in. Therefore, the message processor will have to read the message, look for clues as to what type of message it is, and have it processed in the appropriate manner. In a C++ scenario, an object might be working on constructing itself by sending a message to a data server and awaiting a reply. Due to the nature of the process, several things may be going on at the same time (like window management), so its not appropriate for the process to hang waiting for the reply to the data request (it might never arrive anyway). In C, this would be handled by having the request procedure issue the request and return to the message processing procedure. The reply would eventually come back and the message procedure would give it to the reply procedure. However, in C++, its not a good idea for a constructor function to return before the reply (and, therefore, the data) has been received. Thus the C++ program is hung. So, my question is what is the appropriate C++ fashion for constructing an object when the object construction may involve sending a message and getting a reply that you don't want to wait for? Some might suggest treating the message processor as an object and having the constructor function call this processor when it needs a reply message in order to complete its work. I think this leads to a recursive problem in that potentially many messages may have come in that led to many objects being constructed that led to many outstanding requests. This sounds like a potential stack overflow problem. If your trying to create a server of some sort, this is not a stateless way of doing things. There must be a better way! I see I'm beginning to ramble, so I'll hold it here and see what sort of "brilliant" ideas come in (I hope, I hope ;-). Have a good one! David Masterson uunet!cimshop!davidm or DMasterson@cup.portal.com
eberard@ajpo.sei.cmu.edu (Edward Berard) (08/03/89)
In article <452@cimshop.UUCP>, davidm@cimshop.UUCP (David Masterson) writes: > In a previous message, > > I talked about the mechanics of changing general messages into specific > objects in order to turn a bit-stream (say, from a network) into a known > object. Most seem to feel that it is the responsibility of an object to > write itself out and read itself back in (construct itself). Obviously, the > message processor (disk reader) has to get clues from the bit-stream as to > what type of object is coming in and, therefore, switch to the appropriate > object construction mechanism. The problem is that this leads to > inefficiencies... [stuff deleted] > There are other potential problems as well, in particular the probability of undesirable object coupling. Before I define "object coupling," I must first define the concept of "primitive" objects and classes. A primitive object or class is one which is furnished by the environment, e.g., an object or class which is defined in the definition of the programming language. Primitive objects and classes have two main uses: the construction of non-primitive (i.e., user-defined) objects and classes, and a means of low-level communication among objects, and with the outside world. Object coupling occurs when a class refers to some _specific_ non-primitive object which is an instance of a class other than itself, or to some _specific_, _different_ non-primitive class. There are two types of object coupling: white-box object coupling and black-box object coupling. Black-box object coupling occurs when the class treats the specific, different class as a black box, i.e., it makes no direct references to the state of the class, or to the states of objects which are instances of the class. For example, consider a "list of names" class. Assume that this class is treated like any other list, e.g., one can add names to the list, delete names from the list, and ask how many names are contained in the list. However, there are no specific references to the state of an item contained in the list (i.e., a name). Assuming that this class refers to a specific "name" class, and that the "list of names" class was derived from a more generic "list class," then we say that the "list of names" class black-box couples the list class with the name class. White-box coupling occurs when a class makes specific references to the state of an object which is an instance of a specific different class, or takes explicit advantage of the knowledge of the underlying implementation of a different class. For example, using the "list of names" class from the above example, suppose that we required that the list be an ordered list. If we accomplished this by, for example, letting the list class know that the underlying implementation of a name object was an ASCII string, then the original list class is now white-box coupled with the name class. The most common examples of white-box coupling are when people place input and output operations in the interface to a class. For example, consider a counter class, i.e., instances of this class are used to retain counts of some phenomenon. Suppose the operations in the interface for the counter class are: zero (the counter), increment (the value of the counter by one), decrement (the value of the counter by one), and display (the value of the counter). Concerning the "dislay" operation: the question is "display the value of the counter _where_?" Because the "display" operation is required to change the state of an object which is not an instance of the counter class (i.e., it must change the state of some "output object") we say that the counter object is white-box coupled with the "output object." Object coupling has two undesirable side effects: - It reduces the reusability of objects and classes. For example, suppose another application required a counter class, but did not require the specific output object to which the above counter class was coupled. A generic list class, for example, can be combined with many other classes. - It reduces the reliability of systems which contain coupled objects by providing a mechanism for "ripple effects," i.e., it allows for a change in one part of a system to potentially result in unwanted erroneous changes in other parts of the same system. For example, suppose the operations in the interface to the output object change. Then all the methods in other classes which make specific reference to these operations may potentially have to be changed. Please note that object coupling is necessary for object-oriented applications to accomplish work. What we want to avoid is unnecessary object coupling. This brings up a number of systems questions, e.g., when should objects be coupled, and what collections of coupled objects (i.e., systems of objects) are both highly reusable, and reasonably reliable? [By the way, there are specific techniques for removing undesirable object coupling. There are also specific guidelines for both avoiding unnecessary object coupling, and in the construction of reusable systems of objects.] > Perhaps, there is a need to define what types of data are capable of > being written out of a program and (by implication) what types of data can be > read into a program. Since an object is represented by its data and methods ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ > and methods are of no use outside a process (cannot be written out), maybe it > is better to not define external data as an object (at least in C++). Objects encapsulate: - knowledge of state information - operations (and their corresponding methods) - other objects, i.e., there can be: - homogeneous composite objects - heterogeneous composite objects - exceptions - constants - concepts If an object is a composite object, then you have your original problem defined recursively. Specifically, if you have large objects which are built on smaller objects, which are themselves built on still smaller objects, you have a very interesting "objectification" problem. On another matter: suppose a distributed application is, for example, constructed using a different object-oriented programming language at each node. The language at node X supports exceptions as part of a class definition. The language at node Y supports constants as part of a class definition. The language at node Z supports assertions as part of method definition. Suppose further that the language used at node W does not directly support some, or all, of these items. This obviously will have some impact on "objectification" strategies. [Before you say, "no one should ever attempt such a thing," consider this. Even if a single object-oriented programming language is used in a distributed application, there are still the issues of: - interfacing this system to other similar systems - enhancements to the original object-oriented language which may require a phased introduction of new software on the network - a decision to change the implementation language (or a desire to have the flexibility to make that decision) before all nodes are brought on line. This will probably only be an issue in systems which take years to develop and deploy.] There are quite a number of other issues and solutions I would like to discuss, but I have already taken enough space. -- Ed Berard (301) 353-9652
davidm@cimshop.UUCP (David Masterson) (08/05/89)
In message <533@ajpo.sei.cmu.edu>, eberard@ajpo.sei.cmu.edu writes: >There are quite a number of other issues and solutions I would like to >discuss, but I have already taken enough space. > Discuss more... Discuss more... I agree with you that there are quite a number of interesting issues here that need to be discussed in order for the object-oriented paradigm that C++ uses to move out of the single process type of development into a more multi-tasking environment (yes, you can write multiple cooperating tasks in C++, but does C++ effectively define how they will interface?). >[stuff deleted] There >are two types of object coupling: white-box object coupling and >black-box object coupling. > Your definitions and examples here threw me some. I think I understand what you're talking about, but never before put names to it, so I'm having trouble matching up what you're saying with what I think I know. I'm interested in how coupling might go too far and where the cross-over point from black to white is (is it really that black and white?). > Because the "display" operation is required to change the >state of an object which is not an instance of the counter class >(i.e., it must change the state of some "output object") we say that >the counter object is white-box coupled with the "output object." > I don't understand "output object" or "display". Isn't it more appropriately called asking the "counter" object for its current value? Is that black or white coupling (one object requests information from another)? >[By the way, there are specific techniques for removing undesirable >object coupling. There are also specific guidelines for both avoiding >unnecessary object coupling, and in the construction of reusable >systems of objects.] > Where? I think I want to look this up. >Objects encapsulate: > > - knowledge of state information > - operations (and their corresponding methods) > - other objects, i.e., there can be: > - homogeneous composite objects > - heterogeneous composite objects > - exceptions > - constants > - concepts > Agreed about composite objects (a point I've been trying to make around here). Could you define "exceptions, constants, and concepts" more? Particularly as to transferring this information from process to process? A similar question: should processes be thought of as objects and what implications does that have? David Masterson (415) 691-6311 uunet!cimshop!davidm or DMasterson@cup.portal.com
eberard@ajpo.sei.cmu.edu (Edward Berard) (08/05/89)
In article <455@cimshop.UUCP>, davidm@cimshop.UUCP (David Masterson) writes: > In message <533@ajpo.sei.cmu.edu>, eberard@ajpo.sei.cmu.edu writes: > >There are quite a number of other issues and solutions I would like to > >discuss, but I have already taken enough space. > > > Discuss more... Discuss more... I agree with you that there are quite a > number of interesting issues here that need to be discussed in order for the > object-oriented paradigm that C++ uses to move out of the single process type > of development into a more multi-tasking environment (yes, you can write > multiple cooperating tasks in C++, but does C++ effectively define how they > will interface?). First, I would like to give you some background. Since 1981, I have been researching, applying, and writing about the object-oriented paradigm. In addition to applying object-oriented technology on approximately one million lines worth of source code in house, I have consulted on object-oriented techniques with a substantial number of clients. I also developed (and still give) a number of courses on object-oriented technology. My clients and students use many different programming languages, and develop a wide range of applications (everything from embedded, real-time to classic business information systems). Since I never had the "luxury" of a single programming language or application domain, I had to extract "the essence" of object-oriented technology, i.e., the _concepts_ as opposed to the _implementations_. My clients have always wanted me to take ideas and words and make them real. My job is to make the technology systematic, to clearly define terms and processes, and to do this on many different levels of abstraction. What you see discussed here is the result of a great deal of research and "hard knocks." I'm still learning. > >[stuff deleted] There > >are two types of object coupling: white-box object coupling and > >black-box object coupling. > > > Your definitions and examples here threw me some. I think I understand what > you're talking about, but never before put names to it, so I'm having trouble > matching up what you're saying with what I think I know. I'm interested in > how coupling might go too far and where the cross-over point from black to > white is (is it really that black and white?). > One way to decrease both the resuability and reliability of an object or class is through "object coupling." When Larry Constantine was researching structured design he noticed that (functional) modules could be "coupled," but that all coupling was not the same. I (and others) noticed the same thing about objects. In attempting to quantify object coupling (really "class coupling") I observed: - There was a type of object coupling which had a strongly negative impact on software reusability, but a fairly small impact on software reliability. This was because one class might know about another specific class by name, but would know little else about the other class, i.e., it treated this other specific class as a "black-box." - There was another type of object coupling which not only had a strongly negative impact on software reusability, but also had a very detrimental effect on software reliability. This other type of object coupling made maintenance and modification of object-oriented systems difficult and error prone. The main reason for this was because one class made use of some very specific characteristics of one, or more, different classes. Unlike classes exhibiting the first type of object coupling, classes exhibiting this type of object coupling "knew" something about another class, and took explicit advantage of what it "knew." In effect, the other class was a "white-box." [System Axiom: The more any part of a system "knows" about any other part of the same system, the more tightly coupled those two parts of the system become. Further, tight coupling both reduces the reusability of at least one of the components, and makes the overall system both harder to modify, and more vulnerable to negative side effects when the system is changed in any way. (See Herbert Simon's discussion of "nearly decomposable systems" in his book: "Sciences of the Artificial.")] The first type of coupling (what I now call "black box object coupling") was less serious than the second type of coupling (what I now call "white box object coupling"). I also noticed: - That black box object coupling was relatively easy to remove through a technique I refer to as "data abstraction decoupling" (although it is probably more accurately described as "object abstraction decoupling"). - That white box object coupling takes more work, and more skill to remove. The three techniques for removing black box object coupling are: "selector decoupling," "constructor decoupling," and "iterator decoupling." - That while there are varying degrees of both white box and black box object coupling there does not appear to be, as yet, any reason to identify any further refinement of either form of object coupling. > > Because the "display" operation is required to change the > >state of an object which is not an instance of the counter class > >(i.e., it must change the state of some "output object") we say that > >the counter object is white-box coupled with the "output object." > > > I don't understand "output object" or "display". Isn't it more appropriately > called asking the "counter" object for its current value? Is that black or > white coupling (one object requests information from another)? Two key points to remember when describing any object or class are: describe the object or class from an _external_ viewpoint only, and describe the characteristics of the object or class _in_ _isolation_, i.e., independent of any other object or class, and definitely independent of any specific application. This favorably increases the probability that the object or class will be constructed in such a way as to make it more reusable. Assume that "output object" is a simple object with one operation in its interface, i.e., "put." "Put" accepts strings of characters as its input, and its method directs these strings of characters to the "outside world." (In Smalltalk parlance: messages sent to the output object consist of a selector (i.e., "put") and a "string object.") If you like concrete examples, imagine that "output object" is a one-line LCD display, or a speech synthesizer, or even a very dumb ASCII terminal screen. "Display" is a composite operation whose function is to take the present value of the counter object, and to give it to "output object" for "'display' to the outside world." A "composite operation" is an operation which is composed of two, or more, primitive operations. A "primitive operation" is an operation which cannot be accomplished simply, reliably, and efficiently without knowledge of the underlying implementation of the object or class in which it is encapsulated. *** In the next three paragraphs, I know I will be defining some terms *** *** in a manner which is inconsistent with their usage in some *** *** programming languages. I am specifically thinking about Smalltalk *** *** and C++. Please be kind. If my definitions offend you, focus on *** *** the concepts, not the terminology. Please :-) *** A "selector" operation is an operation which returns state information about the object in which it is encapsulated, but cannot, by definition, change the state of that object. Examples of selector operations include: sine functions, end-of-file functions, and an operation which tells you how may items are contained in a list. A "constructor" operation is an operation which can (and often does) change the state of the object in which it is encapsulated. Examples of constructor operations include: an operation which adds an item to a list, an operation which increments a counter, and an operation which causes characters to appear on an output device. An "iterator" operation is only applicable to homogeneous composite objects. (A homogeneous composite object is a composite objects whose component objects are all (conceptually) instances of the same class.) An iterator capability is a simple concept, it provides the user with the capability to "iterate over" ("loop through") the composite object. Although a user of a homogeneous composite object may know conceptually that the object is a composite object, he or she is unaware of the underlying implementation of the object, e.g., it could be a linked list, an array, a record structure, or something entirely different. Further, the designer of the object or class cannot possibly imagine all the possible things users might want to do as they iterate over the object. Therefore, iterators are actually two-part operations. The designer of the object or class obviously knows about the underling implementation of the object or class, and thus supplies the capability to visit one, or more, nodes of the object in succession. The user of the iterator capability obviously knows what he or she wants done at each node, and hence they supply the appropriate operation(s). (There are several types of iterators, e.g., selective, constructive, active, and passive.) *** And now, back to our story ... *** A rule of class design is that classes contain only primitive operations in their interfaces. (Don't worry. There are well-defined ways for dealing with highly-reusable composite operations.) Another rule is that each operation in the interface of a class must be definable as a selector, constructor, or iterator _solely_ with respect to that class. Let us now re-examine the "counter class." We said that the counter class had several operations in its interface: - Zero (the value of the counter), which is a constructor operation with respect to the counter, and is also a primitive operation for the counter class. - Increment (the value of the counter by one), which is also a constructor operation with respect to the counter, and is also a primitive operation for the counter class. - Decrement (the value of the counter by one), which is yet another primitive constructor operation for the counter class. However, when we come to the display (the current value of the counter on an output device) operation, we run into trouble. We might be tempted to describe this operation as a selector with respect to the counter class since it does not change the state of a counter object, but that is not the whole story. Further examination reveals that the state of the output object (device) is being changed, e.g., characters appear on the device. We now realize that we have a composite operation on our hands, i.e., "Display." There are at least two primitive operations, each for a different class, i.e,: - a selector operation which returns the current value of a counter object, and - a constructor operation which alters the state of the output object (e.g., the aforementioned "put" operation) There may also be other classes and operations involved. Our course of action is simple. We first remove the composite operation (i.e., "Display") from the interface of the counter class and replace it with a selector operation which returns the current value of a counter object. We have now decoupled the counter class from the output object class using selector decoupling. At this point, we have quite a number of options available to us, but I choose not to discuss them, or the tradeoffs among them, here. However, I will say that a good course of action is to decompose the composite operation ("Display"), uncovering primitive operations as we go, and decoupling objects as you go as well. > >[By the way, there are specific techniques for removing undesirable > >object coupling. There are also specific guidelines for both avoiding > >unnecessary object coupling, and in the construction of reusable > >systems of objects.] > > > Where? I think I want to look this up. See the above discussion. More details are available now in the my course notes, and, later, in my book (if I ever get it finished). > >Objects encapsulate: > > > > - knowledge of state information > > - operations (and their corresponding methods) > > - other objects, i.e., there can be: > > - homogeneous composite objects > > - heterogeneous composite objects > > - exceptions > > - constants > > - concepts > > > Could you define "exceptions, constants, and concepts" more? Particularly as > to transferring this information from process to process? A similar question: > should processes be thought of as objects and what implications does that > have? Please allow me to provide you with some partial answers to these questions. (This message has got to be quite long already, and I'm bushed.) Exceptions are a mechanism which allows an object to alert the system of objects in which it is encapsulated that an exceptional condition has occurred. Unlike an "error flag," or the return value for a function, which the system can forget to check for, or ignore, exceptions demand that the system take some action -- even if that action is only to "turn off" (or "lower") the exception. Many programming languages provide exceptions (e.g., PL/1, Mesa, Ada), and the semantics of exceptions, and exception handling is slightly different in each one. (Exception semantics and exception handling in the presence of multiple threads of control is another fairly interesting topic area.) However, the designer of a class must determine what exceptional conditions might arise through use (or abuse) of the class, and provide well-named exceptions for these conditions. Examples of exceptional conditions for which one might want to provide exceptions are: attempting to add another item to a list which is already full, attempting to address a memory location in an EEPROM which is out of range, and a temperature sensor which detects a value outside of the limits set by a user. Notice that exceptions are one mechanism which allows an object to notify other objects of specific situation. Notice further that since exceptions do not require mention of other specific classes or objects, they should not contribute to object coupling. Well-designed objects and classes provide well-named exceptions in their interfaces. By "constants" I mean objects of constant state. The designer of an object or class may occasionally find it useful, or even necessary, to provide a constant in the interface to the object or class. These constants are usually used by one, or more, of the operations in the interface for the class, but may have other uses. Note that the constants I am referring to are not part of the state of an object. By "concepts" I mean the idea behind the abstraction, including such things as the intended, and proper, use of the object or class. For example, given a savings account object, one must put money in before one can expect to take money out. Objects and classes which lack coherent, and well-thought-out concepts are useless, dangerous, or both. There is so much more to tell, but Uncle Ed is really beat. (Its after 1:00 a.m.) See you next time. ;-) ;-) ;-) -- Ed Berard Berard Software Engineering, Inc. 18620 Mateney Road Germantown, Maryland 20874 Phone: (301) 353-9652 E-Mail: eberard@ajpo.sei.cmu.edu
jacob@gore.com (Jacob Gore) (08/05/89)
/ comp.lang.c++ / eberard@ajpo.sei.cmu.edu (Edward Berard) / Aug 4, 1989 / > Our course of action is simple. We first remove the composite > operation (i.e., "Display") from the interface of the counter class > and replace it with a selector operation which returns the current > value of a counter object. We have now decoupled the counter class > from the output object class using selector decoupling. This appears to imply that there is an intermediate representation for the object's value that can be returned by the source object's selector and the destination object's constructor. In this case, it's easy: a number object can represent the counter's value. What do you do if the value is quite complex? Encode it in some agreed-upon representation, such as in a structure or a character string? Jacob -- Jacob Gore Jacob@Gore.Com {nucsrl,boulder}!gore!jacob
eberard@ajpo.sei.cmu.edu (Edward Berard) (08/06/89)
In article <110003@gore.com>, jacob@gore.com (Jacob Gore) writes: > / comp.lang.c++ / eberard@ajpo.sei.cmu.edu (Edward Berard) / Aug 4, 1989 / > > Our course of action is simple. We first remove the composite > > operation (i.e., "Display") from the interface of the counter class > > and replace it with a selector operation which returns the current > > value of a counter object. We have now decoupled the counter class > > from the output object class using selector decoupling. > > This appears to imply that there is an intermediate representation for the > object's value that can be returned by the source object's selector and the > destination object's constructor. In this case, it's easy: a number object > can represent the counter's value. > > What do you do if the value is quite complex? Encode it in some > agreed-upon representation, such as in a structure or a character string? You have just touched on quite a number of interesting issues, e.g.: - The difference between primitive, simple, and composite objects and classes - The concept of parameterizing classes (thus creating metaclasses) - Designing object-oriented systems such that you minimize coupling and maximize reusability Lets start with the differences between primitive, simple, and composite classes: - primitive objects and classes are those supplied by the environment, e.g., typically by the programming language. They have two main uses: creation of user-defined objects and classes and low-level communication among user defined objects (i.e., they are the "lingua franca," the "common language understood by all entities in the system"). Although available primitive objects and classes vary from environment to environment (e.g., from programming language to programming language), common examples of primitive objects and classes are: integer, character, string, and real numbers. - simple objects and classes are user-defined objects and classes which, from an _external_ viewpoint, _conceptually_ have no discernible structure, i.e., they are monolithic entities. Although we might suspect that the _internal_ structure of a simple object or class might be quite complex, if we cannot unambiguously describe what that structure is, given only an outside view, then the object or class is a simple object or class. Examples of simple objects and classes include: a temperature sensor, a month, and a counter. - composite objects and classes are (for the most part) user-defined objects and classes which, from an _external_ viewpoint, _conceptually_ have a discernible structure. Those outside the object or class, may not know the precise underlying implementation of the composite object or class, but the _conceptual_ structure is known. There are two types of composite object and classes: heterogeneous composite object and classes and homogeneous composite objects and classes. Homogeneous composite objects and classes are _conceptually_ composed of other objects and classes which are _conceptually_ all of the same type. Examples of homogeneous composite objects and classes include: a list of names, a mailbox, and a priority queue. Heterogeneous composite objects and classes are _conceptually_ composed of other objects and classes which are _conceptually_ of potentially different types. Examples of heterogeneous composite objects and classes include: a date (composed of a month, a day, and a year), a purchase order, and (in a communications system) a message. Sometimes the primitive objects and classes provided to us include composite objects and classes. The class "string" is an example of a primitive homogeneous composite class. The above distinctions are quite useful, for example, in systematically designing resuable objects and classes, avoiding object coupling, and in overall object-oriented development. (Yes, there are guidelines, rules, techniques, and methods which involve the above.) Next, we move to the discussion of parameterizing classes. Suppose I wish to create a "date class." I know that I want this class to conceptually involve a month, a day, and a year. I have a number of options available to me: - "Hardcode" some primitive class, or primitive classes in the underlying implementation. This is not flexible. Suppose, for example, I wish to change how months are represented, or I wish to extend the range for the years. - I could "hardwire" a specific "day class," a specific "month class," and a specific "year class." This again doesn't sound terribly flexible. Suppose, however, I decided to parameterize the date class. Specifically, I designed the date class in a "generic" manner, i.e., I treated the day class, month class, and year class as abstractions. I stipulated that users of the "generic date class" would have to supply their own month class, day class, year class, and any other necessary associated operations and other appropriate items. As you might have guessed, this "generic date class" is no longer a conventional class, i.e., it is a metaclass. A metaclass is a class whose instances are themselves classes. To use the generic date class, I supply the necessary parameters, and create an instance, i.e., a specific date class. This "specific date class" can then be used to create objects, i.e., date objects. Now, let's answer Jacob's specific question with an example. Suppose I want to create a simple application which will read in, and print out, dates. I decide that I will need the following classes: - the aforementioned "generic date class" - a month class - a day class - a year class - an input object class - an output object class - for purposes of this example, a primitive class, i.e., string In the process of creating this application, I realize that I will need something to coordinate all of these classes, and their associated objects. We will call this intelligent ether "the glue" The glue is itself an object (actually, a "system of objects" -- see, e.g., my recent posting on comp.lang.smalltalk). It is the glue which knows about all of the objects and classes in the application. Now let us look at the generic date class. We find that there are six operations in its interface: - given a specific date object, and a specific month object, set the value of the month in the date object to the value of the month object - given a specific date object, return a month object containing the value of the month for the specific date - a pair of similar operations for the day - a pair of similar operations for the year Notice that the date object has no conception of what a month, a day, or a year look like. It is as if each date object was a black box with three bins, i.e., one each fro a month object, a day object, and a year object. (Yes, there are ways to create an "intelligent date class," but we don't have the space to discuss it here.) We do know that if we wish to set the value of, say, the year for a given date object, we had better show up with a black box called a year object. Further, if we ask for the value of a day for a given date, we will be handed a black box called a day object. On the other side, we have our input object and our output object, which are instances of the input object class, and the output object class respectively. We notice that each has only one operation in its interface (please, this is a simple example): - The input object has an operation called "get" in its interface which returns instances of the primitive homogeneous composite class "string." It knows nothing about dates, months, days, years, output objects, and glue. It just spews out strings. - The output object has an operation called "put" in its interface which requires input in the form of instances of the primitive class string. Like the input object, it knows about no other objects or classes, except string. So here we sit. The input and output objects only know about strings, and the date objects only know about month, day and year objects (from a strictly external viewpoint at that). We must now examine the month class, day class, and year class. Since we will most likely do the same thing to all three classes, let's pick the year class as an example: In the interface to the year class, we will find two operations: - a constructor operation we will call "from_string," whose purpose is to take a string object, and return an instance of the year class, i.e., a year object. - a selector operation we will call "to_string," whose purpose is to take a year object, and return its value as an instance of the string class. Note that the underlying implementation of a year could be anything, e.g., a string, an integer, an enumerated type, or an encrypted value. In the interest of polymorphism, we will have operations with identical names and purposes in the month class and the day class. Since I am trying to make this discussion programming-language-independent, we will now discuss how the overall application will work in general terms. Remember that it is the "glue" which contains all the application-specific information. We will start from the point where all objects have been declared.: 1. The "glue" gives a prompt, in the form of a string object, to the output object. 2. The glue waits for a string object from the input object. 3. The glue sends the sting object to the appropriated class (i.e., month, day, or year) requesting the from_string operation. Assuming all goes well, an object of the appropriate class is returned. 4. The glue sends the month, day, or year object to the date object requesting that the appropriate operation (set_month, set_day, or set_year) be accomplished. 5. Steps 1-4 are repeated until we have one, or more, date objects with values for each of its component objects. 6. The glue then requests a month, day, or year object from a given date object. 7. The glue then sends the month, day, or year object requesting the to_string operation. Assuming all goes well, an object of the class string is returned. 8. The glue then sends a prompt in the form of an instance of class string to the output object. Then, the glue sends the string object representation for the month, day, or year object to the output object. 9. Steps 6-8 are repeated until all the desired dates are printed (displayed, or whatever). Notice that knowledge of the primitive class string was only required by the input object class, the output object class, and the month, day, and year classes. In true polymorphic form, the date class dealt with other objects only as black boxes, and in a uniform manner. If we had had a "list metaclass," we could have constructed a list of dates. I have said way too much already. I have to go. Thanks for listening. -- Ed Berard (301) 353-9652
gregw@otc.otca.oz.au (Greg Wilkins) (08/22/89)
davidm@cimshop.UUCP (David Masterson) writes: > However, in C++, its not a good idea for a constructor function to >return before the reply (and, therefore, the data) has been received. Thus >the C++ program is hung. So, my question is what is the appropriate C++ >fashion for constructing an object when the object construction may involve >sending a message and getting a reply that you don't want to wait for? The problem of Objectifying incoming messages is a very large complex problem. However I think you have invented an extra problem for yourself. If you cannot completely construct an object until a message is sent and a reply recieved, then I see the the solution is to construct a base class, this bass class then sends the message to find out which particular specialization it is and coerces itself to a derived class when it recieves the reply. As I said before, I dont think this is the main issue with Objectifying incoming messages. The main problem is coming up with a general method for inserting/extracting type information without big case statements. The central type server (to which you send a querry and recieve your type in reply) may be one solution, but it appears that all it is achieving is moving the case statement somewhere else and ignoring the problem of placement of explicit type information. gregw@otc.oz
dog@cbnewsl.ATT.COM (edward.n.schiebel) (08/22/89)
From article <828@otc.otca.oz>, by gregw@otc.otca.oz.au (Greg Wilkins): [stuff deleted] > However I think you have invented an extra problem for yourself. > If you cannot completely construct an object until a message is sent and > a reply recieved, then > I see the the solution is to construct a base class, this bass class > then sends the message to find out > which particular specialization it is and coerces itself to a derived > class when it recieves the reply. > You cannot do this. The memory allocated at the time of the call to the constructor is for a base class. By coercing it to a derived class you 1. are probable going to walk of the end of your memory 2. are requiring a base class to know of its derived classes, something which you may want to avoid. One possible way around this is to define a base class which defines the interface to your hierarchy of objects which manages only a pointer to another base class, the real base class of your hierarchy. The interface-base's constructor can figure out what type of object is needed, allocate/construct it, and set its member pointer-to-base-class to the new object. It costs you a level of indirection, but it works. Another win with this scheme, is you can change the apparent type of the object at run-type by deleting the current poniter and re-creating a new different one from the hierrchy. i.e.: class interface_base { public: // interface functions void interface_function1() {ptr->interface_function1();} private: abstract_base* ptr; // points to one of many possibilities // in the hierarchy }; Ed Schiebel AT&T Bell Laboratories ...att!vilya!dog ps. thanks to jonathan shopiro for this idea a couple years ago!
davidm@uunet.UU.NET (David S. Masterson) (08/22/89)
>As I said before, I dont think this is the main issue with Objectifying >incoming messages. > Agreed, to a point. However, the need to objectify an incoming message is often the result of a outgoing request of some sort. So, the question of exitting a constructors and reentering constructors was more of "is there a proper arrangement for making requests that will facilitate processing replies without switch statements in C++?" >The main problem is coming up with a general method for inserting/extracting >type information without big case statements. The central type server (to >which you send a querry and recieve your type in reply) may be one solution, >but it appears that all it is achieving is moving the case statement somewhere >else and ignoring the problem of placement of explicit type information. > Agreed. In C, there is the method of function call jump tables to get around massive switch statements. Is there such a thing as an object call jump table? (The return types would probably be different.) Could you define coercion of a base class into a derived class (the method still throws me)? David Masterson uunet!cimshop!davidm