Rasgos en PHP: ¿hay ejemplos del mundo real / mejores prácticas? [cerrado]


148

Los rasgos han sido una de las mayores adiciones para PHP 5.4. Conozco la sintaxis y entiendo la idea detrás de los rasgos, como la reutilización del código horizontal para cosas comunes como el registro, la seguridad, el almacenamiento en caché, etc.

Sin embargo, todavía no sé cómo utilizaría los rasgos en mis proyectos.

¿Hay algún proyecto de código abierto que ya use rasgos? ¿Algún buen artículo / material de lectura sobre cómo estructurar arquitecturas usando rasgos?


8
Aquí está mi opinión: una publicación de blog sobre el tema que escribí sobre el tema. TL; DR: Básicamente, me temo que si bien son poderosos y se pueden usar para bien, la mayoría de los usos que veremos serán antipatrones completos y causarán mucho más dolor del que resuelven ...
ircmaxell

1
Eche un vistazo a la biblioteca estándar de scala y encontrará muchos ejemplos útiles de rasgos.
dmitry

Respuestas:


89

Mi opinión personal es que en realidad hay muy poca aplicación para los rasgos al escribir código limpio.

En lugar de usar rasgos para hackear el código en una clase, es mejor pasar las dependencias a través del constructor o de los configuradores:

class ClassName {
    protected $logger;

    public function __construct(LoggerInterface $logger) {
        $this->logger = $logger;
    }
    // or
    public function setLogger(LoggerInterface $logger) {
        $this->logger = $logger;
    }
}

La razón principal por la que me parece mejor que usar rasgos es que su código es mucho más flexible al eliminar el acoplamiento duro a un rasgo. Por ejemplo, simplemente podría pasar una clase de registrador diferente ahora. Esto hace que su código sea reutilizable y comprobable.


44
Usando rasgos, también puede usar otra clase de registrador, ¿verdad? Simplemente edite el rasgo, y todas las clases que usan el rasgo se actualizan.
Corrígeme

14
@rickchristie Claro, podrías hacer eso. Pero necesitaría editar el código fuente del rasgo. Por lo tanto, lo cambiaría para cada clase que lo use, no solo para la particular para la que desea un registrador diferente. ¿Y qué pasa si quieres usar la misma clase pero con dos registradores diferentes? ¿O si quieres pasar un simulador de registro mientras pruebas? No puede, si usa rasgos, puede, si usa inyección de dependencia.
NikiC

2
Puedo ver tu punto, también estoy pensando si los rasgos valen la pena o no. Quiero decir, en los marcos modernos como Symfony 2 tienes inyección de dependencia por todas partes, lo que parece superioir sobre los rasgos en la mayoría de los casos. Por el momento veo rasgos como no mucho más que "compilación asistida de copiar y pegar". ;)
Max

11
Por el momento veo rasgos como no mucho más que "compilación asistida de copiar y pegar". ;) : @Max: Eso es exactamente para lo que fueron diseñados los rasgos, así que eso es completamente correcto. Lo hace más "mantenible", ya que solo hay una definición, pero básicamente es solo c & p ...
ircmaxell

29
A NikiC le falta el punto: el uso de un rasgo no impide el uso de la inyección de dependencia. En este caso, un rasgo simplemente permitiría que cada clase que implementa el registro no tenga que duplicar el método setLogger () y la creación de la propiedad $ logger. El rasgo les proporcionaría. setLogger () escribiría una pista en LoggerInterface como lo hace el ejemplo, para que se pueda pasar cualquier tipo de registrador. Esta idea es similar a la respuesta de Gordon a continuación (solo parece que está insinuando en una superclase de Logger en lugar de una interfaz de Logger )
Ethan

205

Supongo que uno debería buscar idiomas que tengan Rasgos durante algún tiempo para aprender las buenas / mejores prácticas aceptadas. Mi opinión actual sobre Trait es que solo debe usarlos para el código que tendría que duplicar en otras clases que comparten la misma funcionalidad.

Ejemplo para un rasgo Logger:

interface Logger
{
    public function log($message, $level);    
}

class DemoLogger implements Logger
{
    public function log($message, $level)
    {
        echo "Logged message: $message with level $level", PHP_EOL; 
    }
}

trait Loggable // implements Logger
{
    protected $logger;
    public function setLogger(Logger $logger)
    {
        $this->logger = $logger;
    }
    public function log($message, $level)
    {
        $this->logger->log($message, $level);
    }
}

class Foo implements Logger
{
    use Loggable;
}

Y luego lo haces ( demo )

$foo = new Foo;
$foo->setLogger(new DemoLogger);
$foo->log('It works', 1);

Supongo que lo importante a tener en cuenta al usar rasgos es que realmente son solo piezas de código que se copian en la clase. Esto puede conducir fácilmente a conflictos, por ejemplo, cuando intenta cambiar la visibilidad de los métodos, por ejemplo

trait T {
    protected function foo() {}
}
class A { 
    public function foo() {}
}
class B extends A
{
    use T;
}

Lo anterior dará como resultado un error ( demo ). Del mismo modo, cualquier método declarado en el rasgo que también se haya declarado en la clase que usa no se copiará en la clase, por ejemplo

trait T {
    public function foo() {
    return 1;
}
}
class A { 
    use T;
    public function foo() {
    return 2;
}
}

$a = new A;
echo $a->foo();

imprimirá 2 ( demo ). Estas son cosas que querrás evitar porque hacen que los errores sean difíciles de encontrar. También querrá evitar poner cosas en rasgos que operan en propiedades o métodos de la clase que lo usa, p. Ej.

class A
{
    use T;
    protected $prop = 1;
    protected function getProp() {
        return $this->prop;
    }
}

trait T
{
    public function foo()
    {
        return $this->getProp();
    }
}

$a = new A;
echo $a->foo();

funciona ( demo ) pero ahora el rasgo está íntimamente acoplado a A y se pierde toda la idea de la reutilización horizontal.

Cuando siga el Principio de segregación de interfaz , tendrá muchas clases e interfaces pequeñas. Eso convierte a Traits en un candidato ideal para las cosas que mencionó, por ejemplo, preocupaciones transversales , , pero no para componer objetos (en un sentido estructural). En nuestro ejemplo de Logger anterior, el rasgo está completamente aislado. No tiene dependencias en clases concretas.

Podríamos usar agregación / composición (como se muestra en otra parte de esta página) para lograr la misma clase resultante, pero el inconveniente de usar agregación / composición es que tendremos que agregar los métodos proxy / delegador manualmente a cada clase, entonces eso debería ser capaz de iniciar sesión Los rasgos resuelven esto muy bien al permitirme mantener la placa repetitiva en un lugar y aplicarla selectivamente donde sea necesario.

Nota: dado que los rasgos son un concepto nuevo en PHP, todas las opiniones expresadas anteriormente están sujetas a cambios. Todavía no he tenido mucho tiempo para evaluar el concepto. Pero espero que sea lo suficientemente bueno como para darte algo en qué pensar.


41
Ese es un caso de uso interesante: use una interfaz que defina el contrato, use el rasgo para cumplir ese contrato. Bueno
Max

13
Me gusta este tipo de programadores verdaderos, que proponen ejemplos de trabajo reales con una breve descripción para cada uno. Thx
Arthur Kushman

1
¿Qué pasa si alguien usa una clase abstracta en su lugar? Reemplazando la interfaz y el rasgo, uno puede crear una clase abstracta. Además, si la interfaz es tan necesaria para la aplicación, la clase abstracta también puede implementar la interfaz y definir los métodos como lo hizo el rasgo. Entonces, ¿puedes explicar por qué todavía necesitamos rasgos?
sumanchalki

12
@sumanchalki La clase abstracta sigue las reglas de la herencia. ¿Qué sucede si necesita una clase que implemente Loggable y Cacheable? Necesitaría la clase para extender AbstractLogger, que luego necesita extender AbstractCache. Pero eso significa que todos los Loggables son cachés. Ese es un acoplamiento que no quieres. Limita la reutilización y arruina su gráfico de herencia.
Gordon

1
Creo que los enlaces de demostración están muertos
Pmpr

19

:) No me gusta teorizar y debatir sobre lo que se debe hacer con algo. En este caso rasgos. Te mostraré para qué me parecen útiles los rasgos y puedes aprender de él o ignorarlo.

Rasgos : son excelentes para aplicar estrategias . En resumen, los patrones de diseño de estrategias son útiles cuando desea que los mismos datos sean manejados (filtrados, ordenados, etc.) de manera diferente.

Por ejemplo, tiene una lista de productos que desea filtrar en función de algunos criterios (marcas, especificaciones, lo que sea) u ordenados por diferentes medios (precio, etiqueta, lo que sea). Puede crear un rasgo de clasificación que contenga diferentes funciones para diferentes tipos de clasificación (numérico, cadena, fecha, etc.). Luego puede usar este rasgo no solo en su clase de producto (como se indica en el ejemplo), sino también en otras clases que necesitan estrategias similares (para aplicar una clasificación numérica a algunos datos, etc.).

Intentalo:

<?php
trait SortStrategy {
    private $sort_field = null;
    private function string_asc($item1, $item2) {
        return strnatcmp($item1[$this->sort_field], $item2[$this->sort_field]);
    }
    private function string_desc($item1, $item2) {
        return strnatcmp($item2[$this->sort_field], $item1[$this->sort_field]);
    }
    private function num_asc($item1, $item2) {
        if ($item1[$this->sort_field] == $item2[$this->sort_field]) return 0;
        return ($item1[$this->sort_field] < $item2[$this->sort_field] ? -1 : 1 );
    }
    private function num_desc($item1, $item2) {
        if ($item1[$this->sort_field] == $item2[$this->sort_field]) return 0;
        return ($item1[$this->sort_field] > $item2[$this->sort_field] ? -1 : 1 );
    }
    private function date_asc($item1, $item2) {
        $date1 = intval(str_replace('-', '', $item1[$this->sort_field]));
        $date2 = intval(str_replace('-', '', $item2[$this->sort_field]));
        if ($date1 == $date2) return 0;
        return ($date1 < $date2 ? -1 : 1 );
    }
    private function date_desc($item1, $item2) {
        $date1 = intval(str_replace('-', '', $item1[$this->sort_field]));
        $date2 = intval(str_replace('-', '', $item2[$this->sort_field]));
        if ($date1 == $date2) return 0;
        return ($date1 > $date2 ? -1 : 1 );
    }
}

class Product {
    public $data = array();

    use SortStrategy;

    public function get() {
        // do something to get the data, for this ex. I just included an array
        $this->data = array(
            101222 => array('label' => 'Awesome product', 'price' => 10.50, 'date_added' => '2012-02-01'),
            101232 => array('label' => 'Not so awesome product', 'price' => 5.20, 'date_added' => '2012-03-20'),
            101241 => array('label' => 'Pretty neat product', 'price' => 9.65, 'date_added' => '2012-04-15'),
            101256 => array('label' => 'Freakishly cool product', 'price' => 12.55, 'date_added' => '2012-01-11'),
            101219 => array('label' => 'Meh product', 'price' => 3.69, 'date_added' => '2012-06-11'),
        );
    }

    public function sort_by($by = 'price', $type = 'asc') {
        if (!preg_match('/^(asc|desc)$/', $type)) $type = 'asc';
        switch ($by) {
            case 'name':
                $this->sort_field = 'label';
                uasort($this->data, array('Product', 'string_'.$type));
            break;
            case 'date':
                $this->sort_field = 'date_added';
                uasort($this->data, array('Product', 'date_'.$type));
            break;
            default:
                $this->sort_field = 'price';
                uasort($this->data, array('Product', 'num_'.$type));
        }
    }
}

$product = new Product();
$product->get();
$product->sort_by('name');
echo '<pre>'.print_r($product->data, true).'</pre>';
?>

Como nota final, pienso en rasgos como los accesorios (que puedo usar para alterar mis datos). Métodos y propiedades similares que pueden eliminarse de mis clases y colocarse en un solo lugar, para un mantenimiento fácil, un código más corto y más limpio.


1
Si bien esto mantiene limpia la interfaz pública, la interna puede volverse realmente compleja con esto, especialmente si extiende esto a otras cosas, como los colores, por ejemplo. Creo funciones simples o métodos estáticos para mejorar aquí.
Sebastian Mach

Me gusta el término strategies.
Rannie Ollit

4

Estoy entusiasmado con los Rasgos porque resuelven un problema común al desarrollar extensiones para la plataforma de comercio electrónico Magento. El problema se produce cuando las extensiones agregan funcionalidad a una clase principal (como, por ejemplo, el modelo de usuario) al extenderla. Esto se hace apuntando el autocargador Zend (a través de un archivo de configuración XML) para usar el modelo de usuario de la extensión y hacer que ese nuevo modelo extienda el modelo central. ( ejemplo ) ¿Pero qué pasa si dos extensiones anulan el mismo modelo? Obtiene una "condición de carrera" y solo se carga una.

La solución en este momento es editar las extensiones para que una extienda la clase de anulación del modelo de la otra en una cadena, y luego establecer la configuración de la extensión para cargarlas en el orden correcto para que la cadena de herencia funcione.

Este sistema con frecuencia causa errores, y al instalar nuevas extensiones es necesario verificar conflictos y editar extensiones. Esto es un dolor y rompe el proceso de actualización.

Creo que usar Rasgos sería una buena manera de lograr lo mismo sin que este molesto modelo anule la "condición de carrera". De acuerdo, todavía podría haber conflictos si múltiples Rasgos implementan métodos con los mismos nombres, pero me imagino que algo así como una simple convención de espacio de nombres podría resolver esto en su mayor parte.

TL; DR Creo que Traits podría ser útil para crear extensiones / módulos / complementos para grandes paquetes de software PHP como Magento.


0

Podría tener un rasgo para un objeto de solo lectura como este:

  trait ReadOnly{  
      protected $readonly = false;

      public function setReadonly($value){ $this->readonly = (bool)$value; }
      public function getReadonly($value){ return $this->readonly; }
  }

Puede detectar si se usa ese rasgo y determinar si debe o no escribir ese objeto en una base de datos, archivo, etc.


Entonces la clase a la que llamaría useeste rasgo entonces llamaría if($this -> getReadonly($value)); pero esto generaría un error si no usehicieras este rasgo. Por lo tanto, este ejemplo es defectuoso.
Luceos

Bueno, primero debes verificar si el rasgo está en uso. Si el rasgo ReadOnly está definido en un objeto, puede verificar si es de solo lectura o no.
Nico

Hice una prueba de concepto genérica para tal rasgo en gist.github.com/gooh/4960073
Gordon

3
Debería declarar una interfaz para ReadOnly para ese propósito
Michael Tsang
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.