Kodokon kodokon.com

Herencia, interfaces, traits y clases abstractas

Elige la herramienta de diseño adecuada - un contrato, un esqueleto de algoritmo o la reutilización horizontal - conociendo los compromisos de cada una.

9 min · 3 preguntas

Abrir esta lección en Kodokon

PHP te ofrece cuatro herramientas de diseño que se solapan en parte: la herencia concreta, las clases abstractas, las interfaces y los traits. La elección se reduce a una pregunta: ¿qué quieres compartir? Un contrato: interfaz. Un contrato más un esqueleto de implementación: clase abstracta. Código compartido entre clases sin vínculo jerárquico: trait. La herencia concreta, por su parte, es cada vez más difícil de justificar: acopla con fuerza, y PHP solo permite un único padre.

PHP
<?php

declare(strict_types=1);

abstract class Exporter
{
    public function export(array $rows): string
    {
        return $this->head() . $this->body($rows);
    }

    abstract protected function head(): string;

    abstract protected function body(
        array $rows
    ): string;
}

final class CsvExporter extends Exporter
{
    protected function head(): string
    {
        return "id;name\n";
    }

    protected function body(array $rows): string
    {
        $lines = array_map(
            fn (array $r): string => implode(';', $r),
            $rows,
        );

        return implode("\n", $lines);
    }
}

$csv = new CsvExporter();
echo $csv->export([[1, 'Ada'], [2, 'Linus']]);
Método plantilla: el padre fija el algoritmo, los hijos aportan los detalles.

La interfaz es el contrato puro: firmas y constantes, sin estado ni implementación. Su ventaja decisiva sobre la clase abstracta: una clase puede implementar varias de ellas, y cualquier clase existente puede adoptar el contrato a posteriori. La clase abstracta, en cambio, lleva estado y código compartido, pero consume tu única ranura de herencia. En la práctica: tipa tus parámetros con interfaces (Cache, no ArrayCache) para poder intercambiar implementaciones, tanto en las pruebas como en producción.

PHP
<?php

declare(strict_types=1);

interface Cache
{
    public function get(string $key): ?string;

    public function set(
        string $key,
        string $value
    ): void;
}

final class ArrayCache implements Cache
{
    private array $items = [];

    public function get(string $key): ?string
    {
        return $this->items[$key] ?? null;
    }

    public function set(
        string $key,
        string $value
    ): void {
        $this->items[$key] = $value;
    }
}

$cache = new ArrayCache();
$cache->set('lang', 'PHP');
echo $cache->get('lang');
El código cliente depende de Cache: la implementación sigue siendo intercambiable.

Los traits resuelven el reparto horizontal: inyectar los mismos métodos en clases sin ningún ancestro común. Cuando dos traits aportan un método con el mismo nombre, PHP se niega a elegir por ti y exige una resolución explícita: insteadof designa la versión que se conserva, as crea un alias para mantener accesible la otra. Es verboso a propósito - el conflicto debe permanecer visible en el código.

PHP
<?php

declare(strict_types=1);

trait FileLogger
{
    public function log(string $message): void
    {
        echo "[file] $message\n";
    }
}

trait AuditLogger
{
    public function log(string $message): void
    {
        echo "[audit] $message\n";
    }
}

final class PaymentService
{
    use FileLogger, AuditLogger {
        FileLogger::log insteadof AuditLogger;
        AuditLogger::log as audit;
    }
}

$service = new PaymentService();
$service->log('Payment received');
$service->audit('Payment received');
Conflicto de traits: insteadof decide, as conserva un alias.

Prueba de conocimientos

Comprueba que has retenido los puntos clave de esta lección.

  1. ¿Qué ventaja decisiva tiene la interfaz sobre la clase abstracta?
    • Puede contener propiedades compartidas
    • Una clase puede implementar varias interfaces, pero heredar de una sola clase
    • Sus métodos se ejecutan más rápido
  2. Dos traits usados por la misma clase declaran cada uno un método log(). ¿Qué ocurre?
    • PHP usa el método del primer trait listado
    • Error fatal, salvo que se resuelva explícitamente con insteadof
    • PHP fusiona los dos métodos en uno
  3. ¿Qué puede contener una clase abstracta que una interfaz no puede?
    • Constantes
    • Propiedades y métodos implementados
    • Firmas de métodos públicos