Passing…Traits

Let’s start with a definition:

“a trait is a concept used in object-oriented programming, which represents a set of methods that can be used to extend the functionality of a class” (Wikipedia)

So, a set of methods. No properties. No extra state representation. After all, as you may know, this is the fundamental difference between a trait and a mixin. Traits contain no state. The purpose of a trait: to extend the functionality of a class. So, this set of methods is going to be attached to a class (or, most probably, to several classes).

In a way, traits is an effort to bring some of the flexibility provided by multiple inheritance to single-inheritance languages while at the same time avoiding all the drawbacks of the former. More specifically, it is an attempt to provide a new way of building reusable pieces of functionality. What is wrong with the “old” way, the “traditional” class inheritance ? Well, a subclass is supposed to work as a subtype. And it is a template for object instantiation. These are not desirable properties in our case. Do you remember the Liskov Substitution principle ? Class inheritance is supposed to be used for conceptual modeling. More specifically, the specialization/generalization concept. A subclass is supposed to express a subtype. It carries semantics. Not only mechanics! A class hierarchy is supposed to maintain semantic interoperability. At least, this is the recommended way to go in order to stay away from troubles.

So, traits is our attempt to focus on the mechanics (reduce code duplication) leaving semantics aside. They are meant to provide units of reuse that should be applicable at arbitrary places. This freedom from semantics must be treated carefully. It is not coincidence that, although the idea of traits has been around for decades (since 1982 or even earlier), many modern languages adopted this approach only within the last decade. PHP, a language appeared in 1995, has added support for traits in 2012. Java, coming from 1995, added traits (default methods) support in 2014. C#, born in 2000, added traits (Default Interface Methods) in 2019.

Though I am not that familiar with C#, I can’t help noting that Default Interface Methods in C# feel a bit odd to me since you cannot call these methods unless you typecast the object to the the interface type. In the following example, the second call to WriteWarning() will raise an error.

Since traits are deliberated from semantic interoperability, we could argue that they are more related to the concept of aggregation than specialization. This new mentality should be treated carefully. It is easy to mess your architecture and seriously hurt the readability of your code. I don’t say not to use them. Just, be careful.

Some recommendations

1. Decouple traits from classes

Traits are supposed to be units of re-use. In that sense, we should take any effort to make them as reusable as possible. And that means keeping them as decoupled as possible from the classes they are going to be used by. Though when a class C uses a trait T, trait T is part of C, C is not part of T. So, as a rule of thumb, a trait should not directly access the state or any non-public methods of class C. And that’s because we don’t want to brake encapsulation of C. Otherwise, C and T will be so tightly depended on each other that we should start wondering why these two are not part of the same class (remember the single responsibility principle ? the concept of cohesion ?). So, ideally, we should be able to look into a trait and understand what it offers and how it works without knowing which classes are going to use it.

What is more, do not forget that a trait is meant to be re-usable! If something doesn’t need to be reusable, why make it a trait ? Making a Quack trait makes no sense since this trait can only be used by a Duck class. It should be part of the Duck class. Not a separate module. Except there are some kinds of ducks that don’t quack… 🙂

The following is a very nice example of a trait that is totally decoupled from any class that is using it:

trait ArrayOrJson
{
    public function asArray() : array
    {
        return get_object_vars($this);
    }

    public function asJson() : string
    {
        return json_encode($this->asArray());
    }
}

2. Point out any requirements

If you need to build a trait that will require the existence of specific methods from any class that will be using it, then try to make these requirements as clear as possible in the trait definition. If this is not possible, then make crystal clear what is the set of classes that can be used by.

3. Make traits detectable

One “pain” point relevant to how PHP implements traits is that we cannot use type-hints to detect whether or not an object comes from a class that uses a specific trait or not. Probably, that is why other languages, like Java and C# have used interfaces to implement traits. The use of interfaces is handy. However, on the other hand, it twists a bit the concept of interface. Anyway, because of that, when working in PHP, I would consider it a good practice that all traits are accompanied by relevant interfaces. That way we easily tell whether an object has a trait or not. Here is an example:

interface ISingleton {
    public static function getInstance();
}

trait Singleton
{
    private static $instance;

    public static function getInstance() {
        if (!(self::$instance instanceof self)) {
            self::$instance = new self;
        }
        return self::$instance;
    }
}

class SqlProfiler implements ISingleton
{
    use Singleton;
}

$profiler = new SqlProfiler();
if ($profiler instanceof ISingleton) {
    
}

4. Do not use them as services

I have included in this post an example that I have seen in several other blogs and articles. An example of making a logger widely available to classes as a trait. Though it serves its purpose, I can’t help noticing how wrong it feels. In a real application, this would probably be a cruel violation of single responsibility principle. After all, we have already a better way to make services available to classes. Dependency injection. And if we are talking about a lot of services, a dependency injection container, that most modern frameworks provide. But, again, as I always say, the last word belongs to the developer. Every case is special one and you should judge what is the best way to go.

5. Pick trait method names carefully.

Not much to say here. Since you cannot know in advance the full set of classes that will be using the trait, try to avoid naming conflicts by carefully picking trait method names that closely match their semantics.

Multiple ways to deal with code duplication

At this point, I would like to put down some thoughts on deciding the way to reduce code duplication. I am talking about the case where multiple classes use the same code block (a method, or a bunch of them).

(a) When the work done by this block is not tied to a specific class domain and there is no need to access class context (class properties) and the work requires no state between subsequent invocations, then we can extract the code block to a global function or a static method put in a helper class.

(b) When the work to be done has no need to access class context and is not tied to a specific class domain but requires state, then the code can be moved to a service class that will be passed through dependency injection.

(c) When the work is tied to a specific class domain and is exactly the same in all class versions, then the code block can be moved to a parent class (abstract or not).

(d) When the work is tied to a specific class domain and code block implementation has small differences between class versions, then we can implement each code block version as a separate trait and add each implementation/trait to the relevant class.

(e) When the code block is not tied to class domain but needs to access class context, then we can use a trait if the use of this code block is limited. Or, we can use a global object super class if the code block needs to be applied to all classes (e.g default implementation of magic methods).