Unit 9: Composition
Learning Objectives
After learning this unit, students should understand:
- how to compose a new class from existing classes using composition
- how composition models the HAS-A relationship
- how sharing reference values in composed objects could lead to surprising results
Adding more Abstractions
Our previous implementation of Circle
stores the center using its Cartesian coordinates \((x,y)\). We have a method contains
that takes in the Cartesian coordinate of a point. As such, our implementation of Circle
assumes that a 2D point is best represented using its Cartesian coordinate.
Recall that we wish to hide the implementation details as much as possible, protecting them with an abstraction barrier, so that the client does not have to bother about the details and it is easy for the implementer to change the details. Let's consider an example: what if the programmer finds that it is more convenient to use polar coordinates to represent a 2D point? We will have to change the code of the constructor to Circle
and the method contains
. If our code contains other shapes or other methods in Circle
that similarly assume a point is represented with its Cartesian coordinates, we will have to change them as well. It is easy for bugs to creep in. For instance, we might pass in the polar coordinates \((r, \theta)\) to a method, but the method treats the two parameters as the Cartesian \((x,y)\).
We can apply the principle of abstraction and encapsulation here, and create a new class Point
. The details of which are omitted and left as an exercise.
With the Point
class, our Circle
class looks like the following:
Circle v0.5 | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
|
This example also illustrates the concept of composition. Our class Circle
has been upgraded from being a bundle of primitive types and methods to a bundle that includes a reference type Point
as well. In OOP, composition is a basic technique to build up layers of abstractions and construct sophisticated classes.
We have mentioned that classes model real-world entities in OOP. The composition models the HAS-A relationship between two entities. For instance, a circle has a point as the center.
Example: Cylinder
Now let's build up another layer of abstraction and construct a 3D object — a cylinder. A cylinder has a circle as its base and has a height value. Using composition, we can construct a Cylinder
class:
1 2 3 4 5 6 7 8 9 10 |
|
Sharing References, aka Aliasing
Recall that, unlike primitive types, reference types may share the same reference values. This is called aliasing. Let's look at the subtleties of how this could affect our code and catch us by surprise.
Consider the following, where we create two circles c1
and c2
centered at the origin (0, 0).
1 2 3 |
|
Let's say that we want to allow a Circle to move its center. For the sake of this example, let's allow mutators on the class Point
. Suppose we want to move c1
and only c1
to be centered at (1,1).
1 |
|
You will find that by moving p
, we are actually moving the center of both c1
and c2
! This result is due to both circles c1
and c2
sharing the same point. When we pass the center into the constructor, we are passing the reference instead of passing a cloned copy of the center.
This is a common source of bugs and we will see how we can reduce the possibilities of such bugs later in this module, but let's first consider the following "fix" (that is still not ideal).
Let's suppose that instead of moving p
, we add a moveTo
method to the Circle
instead:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Now, to move c1
,
1 2 3 4 |
|
You will find that c1
will now have a new center, but c2
's center remains at (0,0). Why doesn't this solve our problem then? Recall that we can further compose circles into other objects. Let's say that we have two cylinders:
1 2 |
|
that share the same base, then the same problem repeats itself! One solution is to avoid sharing references as much as possible. For instance,
1 2 3 4 5 6 7 |
|
Without sharing references, moving p1
only affects c1
, so we are safe.
The drawback of not sharing objects with the same content is that we will have a proliferation of objects and the computational resource usage is not optimized. This is an example of the trade-offs we mentioned in the introduction to this module: we are sacrificing the computational cost to save programmers from potential suffering.
Another approach to address this issue is immutability. We will cover this later in the module.