Horizontal reusability with traits

The ability for a class to inherit from multiple parents is maligned by many, but can be a good thing in some situations. For those working in PHP, multiple inheritance has never been an option; classes are limited to one parent, though they can implement many other datatypes through the use of interfaces. Interfaces can lead to code duplication in improperly-factored inheritance hierarchies. Even in well-architected hierarchies, multiple classes that implement similar methods can contain a lot of overlap.

To address these concerns, PHP 5.4 includes a new feature called traits which allow us to do something that seems an awful lot like multiple inheritance at first glance. Traits allow PHP classes to be “extended” horizontally, rather than vertically. In other words, we can use traits to write methods once and use them in multiple classes, without the multiple inheritance consequence of adding more explicit type information to the classes we modify.

What traits are

A trait is very similar to a class. It is a collection of methods along with a specific notion of state. Traits use the state of the instance of a class into which they’re inserted; they have access to $this. Their use allows methods to be injected into a class at run time. Traits allow you to define a method and then use it in multiple classes.

There are four main differences between a class and a trait:

  1. A trait cannot be instantiated
  2. A trait should avoid the creation of additional state
  3. A trait cannot implement an interface but it can extend a class or extend a class
  4. A trait has no specific type even if it has a parent class

(A note on the above: the first alpha release of PHP 5.4 allowed traits to extend classes. Since there are no useful semantics associated with this feature, it’s been written up as a defect and will be corrected in the next release.)

Number 2 is important, because traits are not intended to be a replacement for classes. As soon as traits contain methods along with properties, they begin to resemble non-instantiable classes. In the current release, properties are allowed within traits, but there is no formal provision for handling such state in the RFC “specification.” It’s probably a good thing to avoid trait properties.

Traits can’t do things that classes do, so why even bother? The good part isn’t their limitation relative to classes, but how they can augment classes’ capabilities. Here are some reasons why traits are even worth bothering with:

  1. traits allow methods to be written once and injected into multiple, derived classes
  2. traits allow class functionality to be extended
  3. traits can be composed—in other words, a trait may consist of other traits
  4. classes can use multiple traits
  5. classes can modify trait method visibility but not vice versa
  6. classes can refer to trait method names using aliases

In a lot of ways, using a trait is like telling the interpreter to copy and paste source code from that trait into any number of classes. Some even refer to it as “compiler-assisted copy and paste.” If that’s how you want to think of it, then go for it. Just keep in mind that it’s not exactly a copy/paste procedure, as I explain below.

Here follow two situations involving code duplication that can be remedied by traits. The first gives a good motivation on traits in general for those who are unfamiliar.

A divergent hierarchy

Say you’re in a situation where you must implement two nephew classes to extend a class hierarchy. For the sake of clarity, imagine that you’re working with a simple hierarchy like the one in the image below. At the head, you’ve got Super, from which all other classes will inherit. Below that, Sub1and Sub2 contain some functionality useful to you, but both need to be extended. So you create your own subclasses, MySub1 and MySub2.

Code duplication

Somewhere along the way, you realize how great it would be if both classes performed the same action—call it someFunction. Your hierarchy now looks something like the following:

At first, you write someFunction once and stick it in MySub1. After you’re satisfied that it behaves how you expect, you copy and paste its source into MySub2. Your tests pass; then, the full weight of what you’ve just done hits you like a train.