Conceptually speaking, composition models "consists of" relationships, whereas inheritance models "is a".
Using the car analogy, "a car has wheels" is a textbook example for composition, and it makes sense to have a class Wheel
, and a class Car
with a property wheels
of type Wheel[]
.
In theory, an example of inheritance would be "a truck is a vehicle": properties common to all vehicles can be implemented in class Vehicle
, while those specific to trucks can be implemented in class Truck
.
The truck example, however, also illustrates the problem of the inheritance approach: what if you have to make your vehicle class polymorphic not only for the vehicles purpose (passengers vs. freight), but also for fuel type? You'd have to create four classes to cover passenger cars and freight vehicles, as well as diesel vs. gasoline powered. Add another orthogonal property, and the number of classes doubles again. Worse yet, you have to decide which of these orthogonal properties comes first in the class hierarchy: is it Vehicle
-> DieselVehicle
-> DieselFreightVehicle
-> Truck
, or is it Vehicle
-> FreightVehicle
-> DieselFreightVehicle
-> Truck
? Either way, you have to duplicate some functionality, either the freight-specific things, or the diesel-specific things. The solution is to use composition anyway: A vehicle has an engine (which can be diesel- or gasonline-powered), and a cargo type (which can be passengers or freight); you don't need a truck class anymore, because your vehicle class can already model all sorts of vehicles by combining suitable engines and cargo types. Note that the components are still polymorphic, but the container object is not. The trick is to keep polymorphism one-dimensional, that is, each polymorphic hierarchy models one of many orthogonal properties, such as engine type, freight type, etc.
This is why people say "favor composition over inheritance"; of course you are still inheriting, but you have separated your inheritance into independent strains.
If i understand your question correctly (and i am not sure that i do), you are having a similiar issue than i had a few weeks ago.
Ask yourself whether Sprite has
a baseImage [instance] or rather is
a baseImage [subclass]
Further reading:
my own question
Is-a
has-a
If i got something wrong, just ignore this answer :)
Best Answer
Traits are another way to do composition. Think of them as a way to compose all the parts of a class at compile time (or JIT compile time), assembling the concrete implementations of the parts you will need.
Basically, you want to use traits when you find yourself making classes with different combinations of features. This situation comes up most often for people writing flexible libraries for others to consume. For example, here's the declaration of a unit test class I wrote recently using ScalaTest:
Unit test frameworks have a ton of different configuration options, and every team has different preferences about how they want to do things. By putting the options into traits (which are mixed in using
with
in Scala), ScalaTest can offer all those options without having to create class names likeWordSpecLikeWithMatchersAndFutures
, or a ton of runtime boolean flags likeWordSpecLike(enableFutures, enableMatchers, ...)
. This makes it easy to follow the Open/closed principle. You can add new features, and new combinations of features, simply by adding a new trait. It also makes it easier to follow the Interface Segregation Principle, because you can easily put functions not universally needed into a trait.Traits are also a good way to put common code into several classes that do not make sense to share an inheritance hierarchy. Inheritance is a very tightly-coupled relationship, and you shouldn't pay that cost if you can help it. Traits are a much more loosely-coupled relationship. In my example above, I used
MyCustomTrait
to easily share a mock database implementation between several otherwise unrelated test classes.Dependency injection achieves many of the same goals, but at runtime based on user input instead of at compile time based on programmer input. Traits are also intended more for dependencies that are semantically part of the same class. You're sort of assembling the parts of one class rather than making calls out to other classes with other responsibilities.
Dependency injection frameworks achieve many of the same goals at compile time based on programmer input, but are largely a workaround for programming languages without proper trait support. Traits bring these dependencies into the realm of the compiler's type checker, with cleaner syntax, with a simpler build process, that makes a more clear distinction between compile-time and runtime dependencies.