DEV Community

Md Al Mustanjid
Md Al Mustanjid

Posted on

The Struggle of Code Reusability in Laravel: Why Traits Win the Race?

The title has already conveyed so much. In web development with Laravel or a PHP environment, we often need to use the same method across multiple classes. Suppose we are creating a vehicle rental system that includes various types of vehicles. We can consider a base class named Vehicle, which can be extended to encompass different kinds of vehicles as child classes. So, here, inheritance is the solution? Let's see.

The Vehicle class serves as our parent class. It includes shared properties that all vehicles have, with the primary function being movement. In this parent class, we'll define these properties and the function as a method, creating a solid foundation for all vehicle types. We will rent cars, boats, and amphibious vehicles. Vehicles can have different capabilities:

  • Cars can only drive.
  • Boats can only sail.
  • Amphibious Vehicles can both drive and sail.
//Vehicle Class
namespace App\Models;

use Illuminate\Database\Eloquent\Model;


class Vehicle extends Model{
    protected $fillable = ['model', 'speed'];

    public function move(){
        return "{$this->model} is moving at {$this->speed} km/h.";
    }
}
Enter fullscreen mode Exit fullscreen mode

Attempt: Using inheritance?

Now, all vehicles inherit this base class.

Car Class

namespace App\Models;

class Car extends Vehicle {
    public function drive() {
        return "{$this->model} is driving on land.";
    }
}
Enter fullscreen mode Exit fullscreen mode

Boat Class

namespace App\Models;

class Boat extends Vehicle {
    public function sail() {
        return "{$this->model} is sailing on water.";
    }
}
Enter fullscreen mode Exit fullscreen mode

But what are we gonna do for AmphibiousVehicle?

First, it can't inherit both Car and Boat classes together to get both functions, php doesn't support it. But, it can inherit from the vehicle class and implement both the drive and sail functions. We currently have duplicate drive( ) and sail( ) logic in two different classes. What's the issue? One thing! Have you noticed the repeated code? Here, we are creating DRY code. If we need modifications or changes, we will have to write the code twice!

Attempt: Using Interface?

Since multiple inheritance isn't possible, another option we can try is interface.

//Drive Interface

namespace App\Contracts;

interface Drivable {
    public function drive();
}

//Sail Interface

namespace App\Contracts;

interface Sailable {
    public function sail();
}
Enter fullscreen mode Exit fullscreen mode

Implementing Interfaces in Car & Boat

//Car
namespace App\Models;

use App\Contracts\Drivable;

class Car extends Vehicle implements Drivable {
    public function drive() {
        return "{$this->model} is driving on land.";
    }
}

//Boat
namespace App\Models;

use App\Contracts\Sailable;

class Boat extends Vehicle implements Sailable {
    public function sail() {
        return "{$this->model} is sailing on water.";
    }
}
Enter fullscreen mode Exit fullscreen mode

Now, let's implement AmphibiousVehicle using both interfaces.

namespace App\Models;

use App\Contracts\Drivable;
use App\Contracts\Sailable;

class AmphibiousVehicle extends Vehicle implements Drivable, Sailable {
    public function drive() {
        return "{$this->model} is driving on land.";
    }

    public function sail() {
        return "{$this->model} is sailing on water.";
    }
}
Enter fullscreen mode Exit fullscreen mode

Wait, isn't this duplicate code? Interfaces don't solve code duplication! We still have to write the same logic in multiple places.

Ultimate Solution: Traits!

Okay, now we will explore the most important feature of the PHP environment, which is traits. A trait is a mechanism for reusing code across multiple classes without relying on traditional inheritance. It simplifies code reusability and facilitates easier sharing of behaviour. We will examine the usage of traits in depth later on.

Create Traits for Drive & Sail

//Drive Trait
namespace App\Traits;

//any vehicle drive on land will use this trait
trait DriveTrait{
    public function drive(){
        return "{$this->model} is driving on the land";
    }
}

//Sail Trait
namespace App\Traits;

//any vehicle sail on water will use this trait
trait SailTrait{
    public function sail(){
        return "{$this->model} is sailing on the water";
    }
}
Enter fullscreen mode Exit fullscreen mode

Set Traits in Models

//Car
namespace App\Models;

use App\Traits\DriveTrait;
use App\Models\Vehicle;

class Car extends Vehicle{
    use DriveTrait;
}

//Boat
namespace App\Models;

use App\Traits\SailTrait;
use App\Models\Vehicle;

class Boat extends Vehicle{
    use SailTrait;
}

//AmphibiousVehicle
namespace App\Models;

use App\Traits\DriveTrait;
use App\Traits\SailTrait;
use App\Models\Vehicle;

class AmphibiousVehicle extends Vehicle{
    //Trait helps to share multiple behaviours at the same time easily
    use DriveTrait, SailTrait;
}
Enter fullscreen mode Exit fullscreen mode

No more duplicate code! AmphibiousVehicle now inherits both!

Set a route and test our classes and traits.

use Illuminate\Support\Facades\Route;
use App\Models\Boat;
use App\Models\Car;
use App\Models\AmphibiousVehicle;

Route::get('/vehicles', function(){
    $car = new Car(['model' => 'Tesla', 'speed' => 150]);
    $boat = new Boat(['model' => 'Yamaha', 'speed' => 80]);
    $amphibious  = new AmphibiousVehicle(['model' => 'Hydra Spyder', 'speed' => 100]);

    return [
        'Car Drive' => $car->move().", ".$car->drive(),
        'Boat Sail' => $boat->move().", ".$boat->Sail(),
        'Amphibious Drive' => $amphibious->move().", ".$amphibious->drive(),
        'Amphibious Sail' => $amphibious->move().", ".$amphibious->sail(),
    ];
});
Enter fullscreen mode Exit fullscreen mode

Get the source code here

Discover even more to uncover the essential traits that can make a difference.

Can a Trait have properties?

Yes, it should be in a function of a trait, not directly within a trait.

namespace App\Traits;

trait Logger {
    //$logPrefix = "[LOG]:"; // Error: props can't declare within trait

    public function logMessage($message){
        $logPrefix = "[LOG]:"; // set props inside a function
        return $logPrefix. $message;
    }
}
Enter fullscreen mode Exit fullscreen mode

What happens if two Traits have methods with the same name in a class? How do you resolve conflicts?

To address conflicts in the same method between two traits, we can utilize either 'insteadof' or 'as'.

//Debugger Trait
namespace App\Traits;

trait Debugger{
    public function logMessage($message){
        return "Logging from Debugger:" . $message;
    }
}

////Logger Trait
namespace App\Traits;

trait Logger {
    public function logMessage($message){
        $logPrefix = "[LOG]:";
        return $logPrefix. $message;
    }
}

//LogService
namespace App\Services;
use App\Traits\Logger;
use App\Traits\Debugger;

class LogService{
    use Logger, Debugger
    { Debugger::logMessage insteadof Logger;
        Logger::logMessage as logFromLogger;
    }
}
Enter fullscreen mode Exit fullscreen mode

Can a Trait use another Trait?

Yes.

Can Traits have constructors?

No. But, we can call a method from a trait in the constructor of the class.

trait LoggerTrait {
    public function setup() {
        echo "Logger is ready!\n";
    }
}

class Car {
    use LoggerTrait;

    public function __construct() {
        $this->setup(); // Calling the trait method
    }
}
Enter fullscreen mode Exit fullscreen mode

Can Traits have abstract methods?

Yes, A trait can define abstract methods that must be implemented by the class using the trait.

Can Traits implement interfaces?

No, a trait cannot directly implement an interface. However, a class that incorporates a trait can implement an interface, allowing the methods from the trait to fulfill the contract of the interface.

interface Loggable {
    public function logMessage($message);
}

trait Logger {
    public function logMessage($message) {
        echo "Logging: $message";
    }
}

class AppService implements Loggable {
    use Logger; // This satisfies the Loggable interface
}
Enter fullscreen mode Exit fullscreen mode

Can we restrict access modifiers in a Trait?

Yes, we can change the visibility of a method from a trait inside a class.

trait Logger {
    public function logMessage() {
        echo "Logging message";
    }
}

class App {
    use Logger {
        logMessage as private; // Now it's private in this class
    }
}

$app = new App();
$app->logMessage(); //Error: logMessage() is private

Enter fullscreen mode Exit fullscreen mode

Happy coding!!!

Top comments (9)

Collapse
 
xwero profile image
david duymelinck • Edited

Traits are a last option feature, not an goto solution.

The better solution for the car, boat, amphibious car example is to have a usedOn method. And that can return the surface and the verb.

class Car extends Vehicle {
   public function usedOn() : array
   {
        return ['road' => 'drive'];
   }
}

class Boat  extends Vehicle {
   public function usedOn() : array
   {
        return ['water' => 'sail'];
   }
}

class Amphibious  extends Vehicle {
   public function usedOn() : array
   {
        return [
            'water' => 'hover',
           'land' => 'hover'
       ];
      // or
     return [
          'water' => 'sail',
          'road' => 'drive'
      ]
   }
}
Enter fullscreen mode Exit fullscreen mode

Because the usedOn method abstracted the specifics there is no need for specific methods.

Traits are a hack to overcome the single class inheritance limitation. First consider an OO solution, that will make the code more robust an easier to understand.

Collapse
 
janmpeterka profile image
Jan Peterka • Edited

Why do you say Traits are a hack?

Not coming from PHP, so maybe I'm missing something, but Traits are mixins, which, in my books, is a standard way to design code in OOP, not a hack. And combined with interfaces, you get clearly defined expected behaviour and its implementation.

(I'm writing in ruby, where modules are used very commonly)

Collapse
 
xwero profile image
david duymelinck • Edited

The equivalent of traits in Ruby are mixins, the modules you mentioned, and that is a more correct name for the concept. Mixins are not an OO concept, so from a purist standpoint traits are a hack. The key feature in OO is inheritance and mixins don't follow that pattern.

Composition is a concept where classes are added to another class, but the methods of the added classes are not becoming a part of the composed class. So mixins are not a form of composition.

But most languages have multi paradigm features; OO, functional, imperative. And that is why you can mix and match what fits best for your codebase. It is best to follow one paradigm as much as possible, and when that paradigm makes the code convoluted look for solutions other paradigms offer.

The overuse of mixins in every language is a code smell.

Thread Thread
 
janmpeterka profile image
Jan Peterka • Edited

The overuse of mixins in every language is a code smell.

why? and when is it overuse? I'm honestly curious about why you hold this opinion.

It is best to follow one paradigm as much as possible, and when that paradigm makes the code convoluted look for solutions other paradigms offer.

again, why? is there some rationale how this leads to better (and in what way) code?

You are right in that I used the wrong word, composition is something different in context of programming, sorry for confusion, will edit my post accordingly, and thanks for pointing it out:)

Thread Thread
 
xwero profile image
david duymelinck

If you look at the example I provided in my first comment. Do you think the traits solution in the post a better way or not?

For your question about overuse. What do you think is better, having all you models use a uuid trait, or a parent class with the uuid properties and methods and all your models extend that class?

Php has become, for the most part, an object oriented language. If you don't follow the rules as much as possible that causes friction. It is like crossing the street when there are no cars and crossing the street when a car is approaching.

Traits introduce a lot of pain for the one benefit it has, reusability. You have to be aware of properties and methods with the same name, they can hide dependencies, when a class has multiple traits you have to open each one to see what their content is.
When you use parent classes, all those problems don't exist. Except from figuring out which class contains which method if it is not overridden in a child class.

This is an extreme example of a trait, and it is not even meant to be reusable. It should be a class and used as a composition part.

Thread Thread
 
janmpeterka profile image
Jan Peterka

I'm not sure of I understand your solution. Does it mean both drive and sail are defined on Vehicle, and we "allow" method dynamically on subclasses? If so, I have problems with it - I don't like object to have methods we are not supported to use. So, I would be finding nearest common ancestor for classes that implement sail. Can that be solved by inheritance? I don't think so, so using mixin is our best option.

Mixins are part of OOP, as a way to do multiple inheritance, and multiple inheritance is a common feature of OO languages. Yes, traits and modules are not the same, but they serve the same purpose, and I even like them better -inheritance tells me what the object is (Boat is Vehicle), and module/mixin/trait tells how it behaves (eg. Driveable). This is of course connected to topic of interfaces and how to implement them. But IMHO compartmentalized code (which modules/traits/mixin make easy) is a good code design (if not overused)

note: my opinions are strongly based on ruby and rails conventions, and in different languages different techniques may be preferred for any reason (technical limitations of language, conventions, specifics of how given language feature is implemented).

and my code design opinions are mostly based on (my understanding of) book Practical Object-Oriented Design in Ruby by Sandi Metz. these should be in general independent of used language.

Thread Thread
 
janmpeterka profile image
Jan Peterka

You have to be aware of properties and methods with the same name

And you don't in inheritance? To avoid this, you need good design and good naming, no matter how you set methods on object.

they can hide dependencies

That's true, but again - that's avoidable with good design. In Rails there's one way of writing code I like (I even described it on one article here - dev.to/janmpeterka/concerning-god-...). You can adhere to SRE and write loosely coupled code, again no matter how to add methods to class.

when a class has multiple traits you have to open each one to see what their content is.

Yes, but to bring the opposite to extreme, you would have one file for all code. Of course, if you overuse modules, or if you overuse inheritance, you get a mess. But as long as your trait is a cohesive part of behavior, you get a benefit of having all related code in one and only place.

Thread Thread
 
xwero profile image
david duymelinck • Edited

With inheritance it is less common that there will be name problems because they are overwritten. If you add several independently created mixins the chance of things with the same name becomes more likely.

I agree good design is a requirement. I believe mixins are more prone to bad behaviour.

Sure bad design is possible in both cases. Programming is making decisions that can give you headaches or make you fly.
I agree to make the coupling as loose a possible, but there are other ways like design patterns, abstract classes. or just redefining what the goal of the code is.

And that last option is what I wanted to demonstrate with my example. Instead of using very specific methods, I made the method more abstract and made de output two variables, what is the surface and what does it do on that surface. So I pushed the responsibility of showing the behaviour out of the class. That information should be enough for some other class to use.

Collapse
 
nothingimportant34 profile image
No Name

This is terrible use of traits and sadly Laravel encourages this behavior. You are enforcing a contract from a trait ($this->model), which you should never do. Interfaces are, naturally, way to go in this case.