Skip to main content

Section 17.12 More Complex Composition

So far, we have considered just a simple case - a class that uses a single instance of another class. That model is sufficient to understand all of the concepts related to composition, but it is important to realize that designs can get much more complex.
One possible complexity is a class that makes use of multiple instances of another class. For example, a mathematical segment is defined by two endpoints. So a Segment class could be defined using two Point members:
      classDiagram
          class Segment {
            -m_start : Point
            -m_end : Point
            ...functions()
          }
          class Point{
            ...details_omitted...
          }
          Segment *-- Point
      
Figure 17.12.1. A Segment class that stores two Points
This definition of Segment β€œowns” two Point objects, but does not control any other data directly. Every function in the class would need to rely on m_start, m_end, or both to do its work. Here are what some functions might look like:
Listing 17.12.2.
double Segment::getStartX() const {
    return m_start.getX();
}

double Segment::length() const {
    return m_start.distanceTo(m_end);
}
Of course we could add more data to it. Perhaps we want a Segment to have a label associated with it:
      classDiagram
          class Segment {
            -m_start : Point
            -m_end : Point
            -m_label : string
            ...functions()
          }
          class Point{
            ...details_omitted...
          }
          Segment *-- Point
          Segment *-- string
      
Figure 17.12.3. A Segment class that stores two Points and a string
Now a Segment is composed from three other objects - the two points and a string (don’t forget that strings are objects!). Note in the UML diagram, each class is only represented one time. So even though a Segment uses two instances of the Point class, there is just one Point box. However, in a memory diagram of a Segment s1 at run time, there would be two contained points:
Segment s1 contains two Point objects m_start and m_end that each have m_x and m_y
Figure 17.12.4. A memory diagram of Segment s1.
There is no reason we have to stop with two Points. We could have a Path that represents a series of Points. It might be represented as a vector of Points:
      classDiagram
          class Path {
            -m_points : vector<Point>
            ...functions()
          }
      
Figure 17.12.5. A Segment class that stores two Points
We can also use composition to construct objects that are composed of other objects. This would be a nice way to build a Cylinder object. A Cylinder is defined by a Circle that is its base and a height (as well as an angle if we want to represent oblique or β€œleaning” cylinders). So we could define a Cylinder class that has a Circle and a double:
      classDiagram
          class Cylinder {
            -m_base : Circle
            -m_height : double
            ...functions()
          }
          class Circle{
            ...details_omitted...
          }
          class Point{
            ...details_omitted...
          }
          Cylinder *-- Circle
          Circle *-- Point
      
Figure 17.12.6. A Segment class that stores two Points
This Cylinder could do some work directly. Other work it would have to hand off to the Circle. Which might in turn hand off the job to the Point object it contains:
Listing 17.12.7.
double Cylinder::getHeight() const {
    // height is the Cylinder's direct responsibility
    return m_height;
}

double Cylinder::getCircumference() const {
    // The base is in charge of the radius and data
    // calculated from it. So ask the base for the answer
    return m_base.getCircumference();
}

double Cylinder::getX() const {
    // Cylinder does not have an x, or even a direct link to the center point
    // Ask the base to get the x value for us. It will in turn ask the Point
    // it contains.
    return m_base.getX();

    // Or, ask the base for its center and then ask that Point for its x
    //Point center = m_base.getCenter();
    //return center.getX();
}

Circle Cylinder::getBase() const {
    return m_base;
}
Given this Cylinder class, we can even compose function calls. A common looking construct in object-oriented code is something like: cylinder1.getBase().getCenter().getY(). We first get the base of the Cylinder, which is a Circle. Then we ask that Circle to do getY. This is a place where returning a const reference from Cylinder::getBase() and Circle::getRadius could avoid needless work. The versions we have return copies of the objects. So even though we do not store those copies into variables, it still involves making some new, nameless objects that then get discarded.

Checkpoint 17.12.1.

Form a statement to get the area of cylinder1’s base. You will not need all the blocks.

Checkpoint 17.12.2.

Form a statement to check if the x location (as defined by the center of its base) of cylinder1 is 5.

Checkpoint 17.12.3.

Form a statement to get the distance between the centers of cylinder1 and cylinder2. You will have to use some blocks more than once.
You have attempted of activities on this page.