16.1 SOME EXAMPLES FOR MI


16.1 SOME EXAMPLES FOR MI

We will now illustrate some example problem domains in which multiple inheritance seems natural. However, beware that there are ways of "re-engineering" these problems so that they can be solved without the complications that could arise from a straightforward implementation of multiple inheritance. For example, one can use role-playing classes, as we will explain later in this chapter; one can limit the inheritance of implementation code to a single superclass while allowing behaviors to be inherited from multiple interfaces (as done in Java); and so on.

For our first example, let's say we wish to use classes to represent the following roles in an educational system:

      Student      Teacher      TeachingAssistant 

While the roles Student and Teacher are obviously distinct, the role TeachingAssistant combines the two. So instead of duplicating the data members and the functions of the Student and the Teacher classes in TeachingAssistant class, it would make more sense if we had the class hierarchy shown in Figure 16.1.

click to expand
Figure 16.1

For our next example, consider the class hierarchy of Figure 16.2 in which we mix a class GenericVehicle with specialized "trait" classes to create different kinds of vehicles. In a hierarchy such as this, the basic passenger safety norms and regulations, presumably different from those for freight carriers, would be stored in the class PeopleHaulerTraits, while those for carrying freight will be stored in FreightHaulerTraits. Regulations specific to vehicles meant for personal and light commercial use could be kept in the class PersonalTransporterTraits, while those intended for heavy-duty commercial use in CommercialTransporterTraits.

click to expand
Figure 16.2

While the above two examples lend themselves straightforwardly to multiple inheritance, let's now consider an example where the decision to use multiple inheritance may be dictated by some basic tenet of object-oriented programming, such as keeping different data abstractions as loosely coupled as possible.

Let's say we want to model the various ways in which a set of mechanical widgets can be assembled in a factory. (We may want to do so to carry out a cost-benefit analysis of the different methods for assembly.) The assembly operations may be carried out robotically, manually, or semiautomatically using different systems. If parts are assembled from random initial positions in a work area, automatic and semiautomatic assembly would need some sort of a computer vision module for localizing the parts before they can be assembled. After the parts are localized, the computer would also have to calculate the motion trajectories to use for mating one part with another part. For that, it would need to know the initial and the final pose of each part. We will assume that the computer has available to it full 3D geometric models of the parts for such path planning calculations.

We obviously have the following three issues to deal with here and, for program organization, it is best to think of them separately:

  1. Specifying the assembly operations at a purely abstract level.

  2. The choice of the agent that would actually carry out the assembly—again at an abstract level. The agent could be a robot, a human, or some semiautomatic system.

  3. A geometry engine for computing the motion trajectories to be used for mating one part with another part when assembly is carried out robotically. Such motion trajectories may also be needed for some types of semiautomatic assembly. For manual assembly, the calculated motion trajectories may help us determine the level of dexterity expected of a human worker.

The first issue—capturing at an abstract level the assembly operations needed—could be addressed by defining an Assemble class as we do below. This class uses two ancillary classes: Part for representing the parts to be assembled, and Pose to represent the location and the orientation of each part in space. The class Part presumably has at least a data member that points to a geometric model of the part. Such models would be needed by a geometry engine to figure out the collision-free trajectories for assembling one part with another. Here is what Assemble could look like:

      class Assemble {      protected:          Part* part1;       // part1 to be assembled with part2          Part* part2;       // part2 assumed fixtured          Pose* part1_initial_pose;          Pose* part1_final_pose;          Pose* pose_part2;          bool done;      public:          Assemble( Part1* p1, Part p2,                    Pose* s1_init, Pose* s1_final,                    Pose* s2, done = false );           virtual void graspPart() {}           virtual void orientPart() {}           virtual void pickupPart() {}           virtual void insert() {}           virtual bool isAssemblyDone() { return done; }           // the rest of the class           virtual ~Assemble();      }; 

The names of the member functions speak for themselves. As a data abstraction, the class Assemble stands on its own, independent of the physical mechanism used for assembly. The simplistic implementations provided for the functions are supposed to take care of the requirement that when a function is declared to be virtual, it must be defined at the same time (see Chapter 15). Obviously, their override definitions in the subclasses of Assemble would be more useful.

For addressing the second of the three issues outlined above, we can now extend the Assemble class and provide more meaningful implementations for its various member functions depending on the specific assembly agent used:

      class AssembleWithRobot : public Assemble {           // stuff related to robot calibration           // and the coordinate transformation from           // the world frame into a robot end-effector           // based coordinate frame      public:           AssembleWithRobot( Part1* part1, Part* part2,                              Pose* part1_init_pose,                              Pose* part1_final_pose,                              Pose* s2, done = false );           void graspPart();           void orientPart();           void pickupPart();           voidinsert();           // the rest of the class     };      class AssembleSemiAutomatically                : public Assemble { /* ........ */ };      class AssembleManually                : public Assemble { /* ........ */ };      .... 

The functions such as graspPart (), orientPart (), and so on, for the robotic and semiautomatic assembly would take into account the kinematic and dynamic constraints of the machines involved, but again at a purely abstract level.

Now we can write a function that, through polymorphism, could be used to perform assemblies:

      void assemble( Assemble* agent ) {           agent->graspPart ();           // grasp part1           agent->insert();               // insert part1 into part2           //...           if ( agent->done() ) {                // assembly finished, start next step           }                else {               // ...           }           // ...      } 

The important thing to note here is that the assemble() function is independent of the kind of Assemble object that we may actually be using. Polymorphism would guarantee us that, inside assemble(), the correct function is invoked for each Assemble.

This brings us to the issue of how to actually do geometry calculations for figuring out the motion trajectories needed for taking part1 from its initial pose, as given by the value of the data member part1_initial_pose of the class Assemble, to its final pose, as given by the data member part1_final_pose. The function insert() defined for Assemble would simply not work unless it has access to some kind of a geometry engine for path planning. The question now is: "How do we incorporate the path planning facilities offered by a vendor-supplied geometry engine in the Assemble class hierarchy?"

One option is to declare theGeometryEngine,class as a base for the Assemble class:

      class Assemble : public GeometryEngine {      protected:           Part* part1;           Part* part2;           Pose* part1_initial_pose;           Pose* part1_final_pose;           Pose* pose_part2;           bool done;      public:           Assemble( Part1* p1, Part* p2,                     Pose* s1_init, Pose* s1_final,                     Pose* s2, done = false );           virtual void graspPart();           virtual void orientPart();           virtual void pickupPart();           virtual void insert();           virtual bool isAssemblyDone() { return done; }           // the rest of the class           virtual ~Assemble();      }; 

The path planning functions inherited from theGeometryEngine,class would be overridden in each subclass of Assemble class to take into account the special constraints of the assembly agent corresponding to that class. Graphically, our class hierarchy for Assemble and its extensions would look like what is shown in Figure 16.3. While this design could be made to serve its intended function, it violates a basic tenet of good OO programming: Data abstractions that are conceptually separate and distinct should be kept as uncoupled as possible. As originally conceived, the data abstraction represented by the class Assemble was complete unto itself and distinct from the path planning implementation code packaged in theGeometryEngine,class. But, by making Assemble a subclass of GeometryEngine, we have destroyed the separate identity of Assemble.

click to expand
Figure 16.3

Now we will show a different design in which we do not violate the separateness of the abstractions. In this new design, we will specify Assemble as a pure interface, meaning an abstract class with no implementation code:

      class Assemble {      public:           virtual void graspPart() = 0;           virtual void orientPart() = 0;           virtual void pickupPart() = 0;           virtual void insert() = 0;           virtual bool isAssemblyDone() = 0;           // the rest of the class           virtual ~Assemble() {}      }; 

Now that all the functions of Assemble are pure virtual, we do not have to provide them with the simplistic implementations that we had to in our previous design. Being an abstract class, our new Assemble class does not need a constructor. We have also included a virtual destructor that can be used for cleaning up the data to be defined in the derived classes.

Now the definition of AssembleWithRobot might look like:

      class AssembleWithRobot           : public Assemble, protected GeometryEngine {           Part* part1;           Part* part2;           Pose* part1_initial_pose;           Pose* part1_final_pose;           Pose* pose_part2;           bool done;      protected:           // code for overriding any virtual functions of           // GeometryEngine class      public:           AssembleWithRobot( Part1* p1, Part* p2,                     Pose* s1_init, Pose* s1_final,                     Pose* s2, done = false );           virtual void graspPart();           virtual void orientPart();           virtual void pickupPart();           virtual void insert();           virtual bool isAssemblyDone();           ~ AssembleWithRobot(); }; 

Here we have multiple inheritance. In this particular implementation of MI, the nature of inheritance from the two bases of AssembleWithRobot is different. The public derivation from the base class Assemble will allow us to use polymorphism with respect to the virtual functions declared in that base class. On the other hand, the protected derivation fromGeometryEngine,will allow AssembleWithRobot and its subclasses to inherit the path planning implementation code in that base. With this construction, we are evidently making a design decision that we do not need polymorphism with respect to the path planning functions in GeometryEngine. It goes without saying that AssembleWithRobot class is required to provide implementations for all the abstract functions declared in the base Assemble.

The other derived classes in the Assemble hierarchy can now be defined as follows:

      class AssembleSemiAutomatically           : public Assemble, protected GeometryEngine      { /*....... */ };      class AssembleManually           : public Assemble, protected GeometryEngine      { /*........*/ };       .... 

Graphically, the entire hierarchy can be shown as in Figure 16.4.

click to expand
Figure 16.4

The two approaches to the design we have presented are not the only ones available. Another possibility would be to use GeometryEngine* as a data member inside Assemble. That could be made to work, provided thatGeometryEngine,has no virtual member functions that would need to be overridden in Assemble and its subclasses. In any case, the Mi-based design appears more natural and more logical to the situation at hand, and it meets a design criterion that it is best to keep distinct abstractions separate. Nonetheless, it is worthwhile to point out that the implementation code at the level of concrete classes such as AssembleWithRobot will remain substantially the same no matter which approach is used.




Programming With Objects[c] A Comparative Presentation of Object-Oriented Programming With C++ and Java
Programming with Objects: A Comparative Presentation of Object Oriented Programming with C++ and Java
ISBN: 0471268526
EAN: 2147483647
Year: 2005
Pages: 273
Authors: Avinash Kak

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net