Object composition is a concept older than object oriented programming itself. Let us start off with a well understood example of how to represent a rectangle on a Cartesian plane:

struct point {
   int x;
   int y;
} ;
struct rectangle {
   point tl; /* top left corner */
   point br; /* bottom right corner */
}

Here the rectangle object is a composition of two point objects and tl & br are sub-objects (not to be confused with sub-classing) of rectangle.

Revealing the sub-structure

Most objects end up revealing the state captured by their sub-objects in some shape or form. Writing a “java bean” that marks the sub-objects as private only to reveal them via setters & getters is effectively the same as having no visibility controls in place. The interesting question to ask is if the composing object allows programmers to interact with a snapshot of the sub-object or the actual sub-object itself. Let us once again go back to some code written in two different languages and see if you can spot the difference.

struct point { int x; int y; };
class rectangle {
   point tl; point br;
public:
   void set_top_left(point a) {tl = a; };
   point get_top_left() {return tl;};
};
class point {
  public int x; public int y;
}
class rectangle {
  private point tl; private point br;
  public void set_top_left(point a) {tl = a;}
  public point get_top_left() {return tl;}
}

The first snippet is written in C++ and the second one is in Java.  While the code looks identical, the behaviour of the two programs is completely different.

In the C++ version, the setter accepts a point and copies over its state onto the sub-object that represents the top-left point. Any manipulation of the argument by the caller after set_top_left has been invoked does not affect the state of the rectangle. Likewise, the getter returns a copy of state of the top-left point. Any changes done to the top-left point in the rectangle after the getter has returned is not visible to the caller. The reverse insulation also exists in both cases i.e. any changes done by the rectangle after the setter has been invoked is not reflected in the variable that was passed to the setter and any changes done to the variable holding the return value of the getter does not affect the state of the rectangle.

In the Java version, no snapshotting occurs. For example: if the caller chooses to update the abscissa of value that it passed to the setter, then the rectangle would also see the update. The assignment is not a transfer of state. Instead, it is merely transfer of responsibility to some other object.

It is perfectly possible to implement either kinds of behaviour in both languages, it just so happens that the most simplistic (and natural) style of coding produces different results. The exact means to mimic the “other behaviour” is left as an exercise to the reader.

Are sub-objects the same as free standing objects?

Often times, programmers tend to forget the difference between a class and an object and also implementation details from core concepts. Try and answer the following question to see if you understand the difference: “Does the top-left corner of a rectangle have an identity and existence outside of the rectangle?”

For starters, the notion of a top-left corner itself is very questionable. It comes into play only if we are to assume that the edges of rectangle are parallel either the x or y axis. In effect, any point that needs to be treated as a top-left corner of the rectangle is only valid as far as a specific implementation of the rectangle is concerned. Unfortunately, this is not the main topic of the post. We shall instead spend more time on the existence part of things. Point, as a concept, can clearly exist without rectangles as a concept. A specific instance of point, say (3,5 ) can also exist without a rectangle. The same co-ordinates (3, 5) can also be a vertex of a rectangle. However, it does not mean that a free standing point (3,5) has the same utility as a vertex (3,5) that is the top-left corner of a rectangle has.

A piece of code that is logically expecting a top-left corner of a rectangle can potentially fail if it is given an arbitrary point. Protection against such failures is usually managed by carefully structuring code paths. One must realize that the semantic type of these entities are different enough though the user-defined data type happens to be the same. This causes a lot more problems than one would anticipate. The most perplexing question is can a vertex of a rectangle outlive the rectangle itself?

The answer is not unless it is semantically casted to something else. As mentioned earlier, this is usually not possible to express this, at least conveniently. This causes weird situations if the designer of the rectangle class has chosen either to use an otherwise general purpose point to represent its vertex or has directly exposed the underlying vertex to the outside world. The former case is not so troubling as the latter one and here is why. Try answering the question “What happens to the leaked/revealed vertex after the rectangle dies?”. If we are still able to meaningfully use that object, then it is because we have done a semantic cast that is enabling us to use it in a different context. If not, subsequent operations that we would perform is gibberish. Having a memory managed runtime usually makes it very hard to appreciate this aspect since the object as understood by the language continues to remain valid and hence one finds it that much more harder to spot the issue.

What does it really mean?

In short, if you choose to directly reveal a live sub-object as opposed to a snapshot, care must be taken to ensure that code that is operating on the sub-object understands the lifecycle of the containing/composing object. Just because your runtime guarantees that there will be no memory corruption/leaks occur does not mean that you can be sloppy. If you are still a non-believer see the code below and spot the trouble for yourself.

class TopMostPointSeeker {
   private List pts = new ArrayList();
 
   public TopMostPointSeeker(List rs) {
      if(r == null || r.size() == 0) {
         throw new RuntimeException("Need non-empty collection")
      }
      for(Rectangle r :rs) {
         pts.add(r.get_top_left());
      }
   }
 
   public getTopMost() {
      /* Implementation delinked from list of active rectangles
       * May not return the topmost point at a given instance.
       *
       * Nothing in the externally visible interface reveals this feature/bug.
       */
      Point retval = pts[0];
      for(Point p: pts) {
         if(p.y > retval.y) {
            retval = p;
         }
      }
      return retval;
   }
}

Wait for part 2 of this story where we look at the most common implementations and its related implications.

h3