Here's an overview of when to use composition during the object modeling process:
When to Use Composition:
Part-Whole Relationships:
Use composition when one class is logically a part of another. For example, a Car might contain an Engine, Wheels, and Seats. Composition models this part-whole relationship where the existence of parts depends on the whole.
Behavioral Delegation:
When a class needs to exhibit certain behaviors but doesn't want to implement them itself, it can compose objects that already provide those behaviors. This allows for delegation of responsibilities, like a Robot using a MovementController for navigation.
Code Reusability:
If you want to reuse functionality across different classes without resorting to multiple inheritance (which might not be available or desirable in some languages), composition lets you include the same objects in different classes, thereby promoting code reuse.
Flexibility and Maintainability:
When anticipating changes or extensions to the class's behavior or structure, composition allows for easy modifications. You can change or extend functionality by altering or adding composed objects without changing the class's interface.
Encapsulation:
To maintain encapsulation while providing complex functionality, use composition to hide internal complexities behind a simpler interface. This is particularly useful when you want to control how data or methods are accessed.
Avoiding Deep Inheritance:
If your design leads to deep inheritance hierarchies, which can make maintenance difficult, composition can help flatten this structure. It allows for combining behaviors from different classes without creating a deep tree of subclasses.
Dynamic Behavior:
When behavior needs to be changed at runtime, composition is advantageous. You can swap out composed objects or add new ones dynamically, which is harder to achieve with inheritance alone.
Testing:
Composition can make unit testing easier since you can mock or replace composed objects to test different scenarios without changing the class under test.
Design Patterns:
Certain design patterns like Strategy, Decorator, or Composite are inherently based on composition, advocating for its use in specific scenarios to achieve particular design goals.
Practical Examples:
Game Design: A Character might compose a Weapon, Armor, and Inventory to manage different aspects of gameplay without the Character class directly handling all these details.
Software Systems: In a system for managing documents, a Document class might compose Text, Image, and Table classes, each handling their specific rendering and manipulation logic.
Considerations:
Granularity: How detailed should your composition be? Fine-grained composition can lead to more modular code but might increase complexity.
Ownership and Lifespan: Decide whether the composed object should be owned by the container class or if it should exist independently. This affects memory management and object lifecycle.
Performance: While composition provides many benefits, excessive use can lead to performance overhead due to indirection or increased memory usage.
By applying composition judiciously, you can create designs that are more modular, easier to maintain, and more adaptable to changes, thereby enhancing the overall quality of your software architecture.
More on Composition
As you progress in an object-oriented design, you will likely encounter objects in the problem domain that contain other objects.
In this situation you will be drawn to modeling a similar arrangement in the design of your solution. In an object-oriented design of a Java program, the way in which you model objects that contain other objects is with composition, the act of composing a class out of references to other objects. With composition, references to the constituent objects become fields of the containing object.
For example, it might be useful if the coffee cup object of your program could contain coffee. Coffee itself could be a distinct class, which your program could instantiate. You would award coffee with a type if it exhibits behavior that is important to your solution.
Perhaps it will swirl one way or another when stirred, keep track of a temperature that changes over time, or keep track of the proportions of coffee and any additives such as cream and sugar. To use composition in Java, you use instance variables of one object to hold references to other objects. For the CoffeeCup example, you could create a field for coffee within the definition of class CoffeeCup.
Class Relationship Implementation
In the video store example, the Customer class has attributes that are just strings and numbers. But consider the Rental class:
It should know what video has been rented and by whom. This means that each Rental object is connected to a Video object and a Customer object, like this:
Exactly how you implement this relationship will be different depending on the programming language that you use.
C++ Composition:
In C++, you have a choice of three types for your attributes: as pointers, as references, or as solid objects. Pointers: Example 1
If you use pointers in C++, the class definition will look like the code below.
class Rental{
private:
Date startDate, returnDate, actualreturnDate;
Customer* rentedby;
Video* itemrented;
// remainder of class omitted
};
When you use these attributes, you will have to dereference the pointers.
References: Example 2
View the code below to see how references are used in C++.
class Rental{
private:
Date startDate, returnDate, actualreturnDate;
Customer& rentedby;
Video& itemrented;
// remainder of class omitted
};
If you use references for the attributes, the class definition will look like the example shown above. You do not need to dereference references; more important, you cannot change them to refer to a different object. Pointers can be changed to point to something else. Because it makes little sense for the Rental object to change the customer who is renting or the item that is rented, references are probably a better choice than pointers for this class.
Solid Objects in Java: Example Code
Both pointers and references enable the class to reach an object that has been created elsewhere. If the Customer object changes during the life of this Rental object, the new values will be available to the Rental object. In contrast, a solid object is fully contained within the class, and is a private copy of the original information.
View the code below to see an example of the class definition. If the customer returns the video late, your code will add late charges to the customer's balance. You want that change to affect the real customer, somewhere outside this Rental object, not just some local copy. Using solid member variables is not the right decision for the Rental class.
class Rental{
private:
Date startDate, returnDate, actualreturnDate;
Customer rentedby;
Video itemrented;
// remainder of class omitted
};
Java Composition and Creating User-defined Data Types
In Java, your Rental class will have attributes that are references to the other object:
View the code below to see an example
class Rental{
private Date startDate, returnDate, actualreturnDate;
private Customer rentedby;
private Video itemrented;
// remainder of class omitted
}
Because this is the design stage, you do not yet need to consider those details. It is important to record and understand the relationships among your classes. Think back to the Check class you designed earlier. You had several choices for how to represent the amount of the check: You could use:
A floating-point number, like 57.35, but then there would be round-off issues to consider
An integer, like 5735, representing the number of pennies the check was for, but then you'd have to divide by 100 whenever you had calculations to do
Two integers, like 57 and 35, for the dollars and the cents, but then you'd have to be careful they didn't get "out of sync."
What would it mean if the dollars were 57 and the cents were -35?