Jump to content

Traits or inheritance for reducing duplicated code?


NotionCommotion

Recommended Posts

For solely to reduce duplicated code and when injection isn't applicable, should one use inheritance or traits?

<?php
abstract class BaseClass
{
    private $id, $name;
    public function __construct(int $id, string $name) {
        $this->id=$id;
        $this->name=$name;
    }

    public function getId():int
    {
        return $this->id;
    }

    public function getName():string
    {
        return $this->name;
    }
}
<?php
class A extends BaseClass{}
class B extends BaseClass{}
class C extends BaseClass
{
    private $foo;
    public function __construct(int $id, string $name, Foo $foo) {
        parent::__construct($id, $name);
        $this->foo=$foo;
    }
    public function getFoo():Foo {
        return $this->foo;
    }
}
<?php
trait MyTrait
{
    private $id, $name;
    public function __construct(int $id, string $name) {
        $this->id=$id;
        $this->name=$name;
    }

    public function getId():int
    {
        return $this->id;
    }

    public function getName():string
    {
        return $this->name;
    }
}
<?php
class A
{
    use MyTrait;
}
class B
{
    use MyTrait;
}
class C
{
    use MyTrait;
    private $foo;
    public function __construct(int $id, string $name, Foo $foo) {
        $this->id=$id;
        $this->name=$name;
        $this->foo=$foo;
    }
    public function getFoo():Foo {
        return $this->foo;
    }
}

 

Edited by NotionCommotion
Link to comment
Share on other sites

38 minutes ago, requinix said:

Traits are copy and paste. If there is any meaning to the code besides you wanting to simply save yourself some copying and pasting then a trait is not the right answer.

Just for copy and past reasons.  I am still undecided on whether I prefer inheritance or traits for this scenario.

There is no reason why Classes A, B, and C shouldn't have access to the $id and $name property so a trait is better.

But then I can just make $id and $name protected so it doesn't matter.

But then again using inheritance implies some base functionality and better defines what is additional (i.e. Foo), so maybe inheritance is better.

Or maybe it really doesn't matter and I should just be consistent...

Link to comment
Share on other sites

Foo/Bar examples aren't helpful in exploring a question like this.  I also think you are leaving out a key ingredient when you aren't pairing inheritance with one or more interfaces. 

Setter/Getter examples are also no helpful when you have __set and __get magic methods available.

Where I have seen traits used frequently, is in providing oft repeated exception throwing/handling code.  This often includes logging, so you almost always see classes injecting a logger object and then logging out information when exceptions/throwables are caught/thrown.   You could also put these routines in a base class, but it's extra work and possibly extra inheritance for something that is necessary but not intrinsic to a specific class hierarchy.

Traits and classes are not mutually exclusive nor in my opinion should they be used that way.  OOP languages were created in order to help model paradigms and simplify the passing of data, messaging and events between things.  Typically it's best to start with OOP, DI and other design patterns, and only when things become particularly odious, or are highly disconnected from the data and behavior you are modeling in the core class hierarchy, would I look for alternatives like a Trait.

 

Link to comment
Share on other sites

2 hours ago, gizmola said:

Foo/Bar examples aren't helpful in exploring a question like this.  I also think you are leaving out a key ingredient when you aren't pairing inheritance with one or more interfaces. 

Just looking for ways to limit cutting and pasting identical code and then making a change to one version and failing to update all the rest. You are likely correct that I am leaving out a key ingredient regarding interfaces and are still trying to better understand their place.

1 hour ago, gizmola said:

Setter/Getter examples are also no helpful when you have __set and __get magic methods available.

I am mixed when it comes to __set and __get.  When first learning about them, I used them exclusively, but then found (at least for me) that they often are more trouble than they are worth for when simple setters and getters can be used.

Link to comment
Share on other sites

14 hours ago, requinix said:

It is true, traits cannot be hinted - they are designed to be, and really do work very much like, language-assisted copy and paste.

Thanks for the verification - it didn't make much sense that you would be able to hint on them given the way they're used, but I don't have a ton of experience with traits...

Link to comment
Share on other sites

Echoing  Requinix on this, but they aren't all that much different than an include/require of some code in a class body.  Obviously better than that, from a documentation point of view, in that at least you see the declaration of the trait at the top of the class, but otherwise, it's about the same.

In regards to interfaces, I would say it's best practice and certainly used in large projects and frameworks that class designers start with one or more interfaces to lay out the api contract.

For example, take a look at the Laravel Cache system.  Let's say you have a type of cache that doesn't come out of the box. How do you make your ACME cache work with Laravel?

Laravel has a contracts directory where the interfaces are defined, and for cache we find this interface for a "Store"

https://github.com/laravel/framework/blob/5.3/src/Illuminate/Contracts/Cache/Store.php

You can see that this interface describes the cache api with methods like get, put, increment, decrement, forget, etc.  

When you look at the implementation of a specific cache store with bundled support like Redis for example, this is what you find in RedisStore:

<?php
namespace Illuminate\Cache;
use Illuminate\Contracts\Cache\Store;
use Illuminate\Contracts\Redis\Database as Redis;
class RedisStore extends TaggableStore implements Store
{
    /**
     * The Redis database connection.
     *
     * @var \Illuminate\Redis\Database
     */
    protected $redis;
    /**
     * A string that should be prepended to keys.
     *
     * @var string
     */
    protected $prefix;
    /**
     * The Redis connection that should be used.
     *
     * @var string
     */
    protected $connection;
    /**
     * Create a new Redis store.
     *
     * @param  \Illuminate\Redis\Database  $redis
     * @param  string  $prefix
     * @param  string  $connection
     * @return void
     */
    public function __construct(Redis $redis, $prefix = '', $connection = 'default')
    {
        $this->redis = $redis;
        $this->setPrefix($prefix);
        $this->setConnection($connection);
    }
    /**
     * Retrieve an item from the cache by key.
     *
     * @param  string|array  $key
     * @return mixed
     */
    public function get($key)
    {
        if (! is_null($value = $this->connection()->get($this->prefix.$key))) {
            return $this->unserialize($value);
        }
    }
  
    /**
     * Store an item in the cache for a given number of minutes.
     *
     * @param  string  $key
     * @param  mixed   $value
     * @param  float|int  $minutes
     * @return void
     */
    public function put($key, $value, $minutes)
    {
        $value = $this->serialize($value);
        $this->connection()->setex($this->prefix.$key, (int) max(1, $minutes * 60), $value);
    }

So it should be fairly obvious to someone looking to add this capability to their Laravel project that an Acme cache will need an AcmeStore class that implements Illuminate\Contracts\Cache\Store.  

I looked around a bit at the Laravel Docs and found this section that outlines the same thing I've discussed using MongoDB as an example:  https://laravel.com/docs/5.1/cache#adding-custom-cache-drivers

As a dependency injection framework, there's going to be some sort of service system that takes care of instantiating a service, which is why this discusses how to register the store.

Here is a MongoDB Laravel cache implementation I found on Packagist:  https://github.com/alfa6661/laravel-mongodb-cache/blob/master/src/MongoStore.php

When you look at this code, you'll see that it extends the Laravel DatabaseStore class rather than implementing the store interface, but that simply has to do with the fact that Mongo is a type of database and has some database-esque features that most pure cache servers don't have.  When you look at the DatabaseStore Class you can see that it too implements the Store interface:

class DatabaseStore implements Store
{

I think looking at how Laravel and Symfony have done things could be helpful in seeing the language features and techniques class designers use to create reusable and extendable class hierarchies.

Another interesting example I could suggest to look at would be the Symfony finder component.  It's a component library that facilitates searching for and working with files and directories on a local or network file system.  The finder class implements 2 Standard PHP Library interfaces:

class Finder implements \IteratorAggregate, \Countable

This allows a finder object to be used in a statement like:  if (count($finder) > 0).

Implementing the IteratorAggregate interface facilitates iterating through an object using foreach().  If you are interested look at the implementation public function getIterator() in the class source.

This is all far more interesting and important than Traits.  You'll also probably start to notice that the interfaces are used to typehint dependency injected objects in related classes, which keeps those classes loosely coupled and yet still with some type resiliency.  I can pass an object of a class that implements the interface, but if an object that does not implement the interface is passed, a fatal error will be generated.

Link to comment
Share on other sites

This thread is more than a year old. Please don't revive it unless you have something important to add.

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...

Important Information

We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.