Unit 9: Composition
Learning Objectives
Students should
- 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 coordinate \((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. In this example, what happens if the application 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 coordinate, we will have to change them as well. It is easy for bugs to creep in. For instance, we might pass in the polar coordinate \((r, \theta)\) to a method, but the method treats the two parameters as the Cartesian \((x,y)\). After all, both \((r, \theta)\) and \((x, y)\) can be abstracted as a pair of double
(i.e., (double
, double
)).
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. Instead, try to practice converting class diagram into a code.
With the Point
class, our Circle
class looks like the following:
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 33 |
|
This example also illustrates the concept of composition. Our class Circle
has been upgraded from being a bundle of primitive types and its 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 that HAS-A relationship between two entities. For instance, a circle has a point as the center.
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 |
|
This process of composition can be extended further. For instance, if we think of a tire as a cylinder, then we can construct a class called Car
as consisting of 4 cylinders corresponding to each tire.
1 2 3 4 5 6 7 |
|
This way, we can create more and more complicated models!
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 assume that the method moveTo
is a mutator on the class Point
that mutates both field x
and y
. Suppose we want to move c1
and only c1
to be centered at (1,1). In particular, we do not want to move c2
at all.
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. In other words, assume that there is no moveTo
method in Point
but there is a moveTo
method in Circle
.
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? There is no way we can mutate p
because invoking p.moveTo(1, 1)
will no longer work. We have removed the method moveTo
on class Circle
. Unfortunately, recall that we can further composed 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.