4.1OBJECT-ORIENTED RELATIONSHIPS
A famous quote from Aristotle, “the whole is greater than the sum of its parts”, emphasizes the power of combination. By reusing and combining types, class designers may expediently construct new types and new interfaces. A central design question is how to do so. An implied design responsibility is to manage the dependency of the whole on its parts.
OOD defines different relationships (composition, containment, and inheritance) that determine the form and flexibility of reuse. How types are connected – association, cardinality, and ownership – differentiate design options. An association between two objects may be temporary, stable, or for the lifetime of the primary object. Cardinality may reflect a one-to-one or a one-to-many relationship, may be defined at the class or object level, and may vary or be stable. Ownership implies that the primary object is responsible for a secondary object, requiring explicit decisions for allocation, release, replacement, or transfer of ownership.
Basic structural relationships are has-a (composition), holds-a (containment), and is-a (inheritance). Historic OOD discussion defined aggregation as a structure where the aggregate object contains many subobjects of the same type. Aggregation addresses only form and not intent or effect. For example, both a container and a building toy (such as a Lego set) may be described as aggregates. However, a container retains no dependency on the subobject type while a building toy is strongly dependent on its components. A container illuminates a holds-a relationship where there is little restriction on the type of subobjects held while a composite illustrates a has-a relationship with significant dependency on the subobject type. While structurally similar, holds-a and has-a may be distinguished via design details such as association, ownership, lifetime, and reuse of functionality.
The simplest relationship is none: two types do not interact. Next in simplicity is the uses-a relationship where one type uses another in a transient fashion such as call by value. Other relationships represent associations that are more enduring and suggest some type dependency.
4.2CONTAINMENT (HOLDS-A)
Standard containers model the holds-a relationship well because there is no type dependency on the subobjects. A stack provides the same utility no matter what type of data held. A stack is well-defined when empty, full, or in-between. The operations of push()
, pop()
, clear()
, etc. function in the same manner regardless of the type of data processed. The type of data stored provides no functionality and has little or no effect on containers. The holds-a relationship reflects little or no type dependency.
Example 4.1 portrays weak type dependency: a customer holds-a gift card. The no-argument constructor zeroes out the pointer defined to hold the address of a (heap-allocated) gift card, suggesting that a customer may operate without a gift card and that not all customers have a gift card. If a customer is well-defined without a gift card, then a customer may have zero gift cards and still function as a customer. A customer is not dependent on a gift card if gift cards do not drive core functionality, or if another item, such as a free shipping certificate, may replace a gift card.
Holds-a does not require ownership. The customer may not be responsible for the destruction of a gift card, especially if ownership is temporary. Disposal of a gift card may differ by design. If the customer is the sole owner of a dynamically allocated gift card, then the gift card should be ‘destroyed’ (reference zeroed out or destructor invoked) unless ownership is transferred out. Since the presence of a gift card is optional in the customer class of Example 4.1, any method that accesses the gift card must first test for existence, as is done in replace()
.
Example 4.1C++ Customer Holds-A Gift Card
// transient ownership of subobject(s)
// implies memory management
// => must provide destructor
// => support or suppress copying
class Customer // replaceable gift card
// handle only, no object yet
{ GiftCard* c = 0;
public:
// assumption constructor: ownership
// of transfer
assumed
Customer(GiftCard*& transfer)
{ c = transfer;
transfer = 0;
}
// no argument constructor:
// no gift card allocated
Customer() { c = 0; }
// again ownership transferred in
void replace(GiftCard*& backup)
// dispose existing card
{ if (c) delete c;
c = backup;
backup = 0;
}
˜ Customer() { if (c) delete c; }
};
A container may hold objects, copies of objects or references to objects. The objects contained may be passed in and out, transferred, or destroyed (redeemed), yielding a fluctuating cardinality across the lifetime of the container. Logically, a customer may hold a positive number of gift cards, or none. If different gift card types are available (bonus, restricted by item or calendar date, etc.), the mix of gift card types held may vary over the lifetime of a customer. Only a temporary association exists between the customer object and the gift card object.
Independent of implementation language, a containment relationship is flexible because cardinality, ownership, and association may vary. Designs differ though because of implementation language. In C++, memory management must be addressed for any object with internally allocated heap memory. The class must track ownership so that all heap-allocated memory is deallocated before objects owning the heap-allocated memory go out of scope. In all languages, aliases should be tracked so that dead objects may be reclaimed and data is not corrupted.
Copying is an essential design decision. Often, it is undesirable to copy large collections either for data integrity or performance concerns. What are the effects of supporting or suppressing copying? What does a container hold: original data, duplicates, references? Copying may be more complex when data is referenced indirectly, that is, via a reference or a pointer. What is copied? – the address holder (reference or pointer), or the actual data values?
Copy semantics should be an explicit design decision. If a C++ class neither defines nor suppresses copying, the compiler generates a default copy constructor and overloaded assignment operator that yield shallow copies, and, thus aliasing and possibly data corruption. If no decision is made in C#, copying is also shallow. Recall the difference between shallow and deep copying as examined in Chapter 3.