There are several types of interaction of objects, united under the general concept of "Has-A Relationship" or "Part Of Relationship". This relationship means that one object is part of another object.
There are two subspecies of this relationship: if one object creates another object and the lifetime of the "part" depends on the lifetime of the whole, then this is called "composition", if one object receives a link (pointer) to another object in the design process, then this aggregation.
Let's look at an example from the .NET Framework to see what restrictions / consequences this relationship StringWriter : StringWriter + StringBuilder .
The StringWriter class is a specialized version of the TextWriter class that is used extensively in serialization and to get a textual representation of objects.
Specifically, a StringWriter creates a string representation of an object or object graph and relies on an instance of StringBuilder in its work. Those. we can say that 'StringWriter HAS is a StringBuilder' or 'StringBuilder is part of StringWriter'. Now let's see how to decide whether a StringWriter receive an instance of a StringBuilder externally or create it?
On the one hand, for us, as a client of the StringWriter class, StringWriter often doesn’t matter what exactly is used inside this class to get a string representation. This means that in terms of ease of use, it is better that StringWriter create an instance of StringBuilder itself.
But, on the other hand, a specific StringWriter object can only be responsible for obtaining parts of the string representation, and another part of the string can be calculated in another way. From this point of view, it is better that StringWriter take an instance of StringBuilder in the constructor. The same is true for highly loaded systems in which the use of a pool of objects is reasonable.
Since StringWriter is a library class that should support both scripts, it has overloaded constructor versions: one of them creates a StringBuilder inside, and the other accepts it outside.
In other words, the choice between composition and aggregation is based on respect for the balance between the various design requirements:
Composition: object A controls the lifetime of object B
Pros:
Composition allows you to hide the use of objects from the eyes of the client.
It makes the class usage API simpler and allows you to switch from using one class to another (for example, a StringWriter could change the implementation and start using another type, for example, CustomStringBuilder ).
Minuses:
- The relationship is quite tough, since one object must be able to create another: it must know the specific type and have access to the creation function. The composition does not allow the use of interfaces (without the involvement of factories) and requires that the class have access to the constructor of another class: imagine that the
StringBuilder constructor is internal or is it the IStringBuilder interface and only the client code knows which instance should be used here and now.
Aggregation: object A receives a reference to object B
Pros:
Minuses:
Exposure of implementation details. Since the class client must provide a dependency at the time of creating the object (pass an instance of StringBuilder at the time of creating the StringWriter , the fact of this relationship becomes known to the client.
The first point implies an increase in the complexity in the work of clients, as well as a large “rigidity” of the solution in the long term. Now the author of the TextWriter class TextWriter no longer decide on its own and move from StringBuilder to something else. Yes, you can "add another level of abstraction" and highlight the IStringBuilder interface, but it will be impossible to break this relationship completely without breaking all existing customers.
In conclusion: design is a search for a compromise between various factors. The composition is simpler from the point of view of the clients of the class, but it imposes certain restrictions: the "whole" must be able to create the "integral part". Aggregation is more flexible, but imposes other restrictions: now the "whole" does not hide the existence of a "component", and therefore cannot replace it with another "component" in the future.
PS If you really want to use an example from the real world, then a screwdriver may be appropriate to explain the composition and aggregation. If the screwdriver is solid, i.e. the handle and the nozzle are tightly connected to each other, then we have the composition relation. If the nozzle is removable and can exist without a handle or used with another handle, then we have an aggregation relationship.