Principio de Sustitución de Liskov en diseño orientado a objetos (SOLID)

Author
Por Darío Rivera
Publicado el en SOLID

En un anterior post vimos una introducción a los Principios SOLID a manera meramente teórica. El día de hoy veremos todo acerca del tercer principio SOLID en la lista, The Liskov Substitution Principle. Empecemos por dar una definición de este principio.

Definición

Este principio dice que los objetos de un programa pueden ser reemplazados por sus subtipos sin alterar el correcto funcionamiento del programa. Es decir, un argumento de una función o método que acepte una clase o interfaz, debería funcionar igual con cualquier subtipo de la misma clase o interfaz. Para que esto sea posible, es necesario que la signature de los métodos implementados o sobreescritos sean iguales y no agreguen o cambien el TIPO del resultado de la función (incluyendo excepciones).

Ejemplo

Para ilustrar este principio, vamos a ver un ejemplo práctico de una correcta implementación y algunos errores comunes que pueden romper dicho principio. Veamos entonces el tipo principal de este ejemplo como el código cliente en donde se utiliza una clase Parser.

class Client
{
    public static function printContent(Parser $parser)
    {
        echo $parser->output();
    }
}

Para cumplir con el principio de sustitución de Liskov, el método printContent debería poder aceptar cualquier subtipo de Parser y el código seguiría funcionando igual. Veamos entonces que Parser es una clase con comportamiento propio en este caso.

class Parser
{
    protected array $data;

    public function __construct(array $data)
    {
        $this->data = $data;
    }

    public function output()
    {
        $result = '';

        foreach($this->data as $row)
        {
            $result .= $this->line($row);
        }

        return new Result($result);
    }

    public function line(array $row)
    {
        return implode(' ', $row) . PHP_EOL;
    }
}

Esta clase parsea un array multidimensional que simula una tabla de datos como la siguiente:

[
    [1, 'Steave', 'Developer'],
    [2, 'Andreas', 'Tester'],
]

Apoyemos nuestro ejemplo con esta clase Result que solamente está encargada de recibir un string y retornarlo.

class Result
{
    private string $content;    

    public function __construct(string $content)
    {
        $this->content = $content;
    }

    public function getContent(): string
    {
        return $this->content;
    }
}

Cada subtipo de la clase Parser será una forma de generar la cadena de contenido, por ejemplo, CSV o una tabla en markdown.

class CSV extends Parser
{
    public function line(array $row)
    {
        return implode(';', $row) . PHP_EOL;
    }
}
class MarkdownTable extends Result
{
    public function line(array $row)
    {
        return '|' . implode('|', $row) . '|' . PHP_EOL;
    }
}

De este modo el código "cliente" podría hacer uso del tipo Parser del la siguiente forma.

$parser = new Parser([
    [1, 'Steave', 'Developer'],
    [2, 'Andreas', 'Tester'],
]);

// subtype CSV
$csv = new CSV([
    [1, 'Steave', 'Developer'],
    [2, 'Andreas', 'Tester'],
]);

// subtype MarkdownTable
$marrkdown = new MarkdownTable([
    [1, 'Steave', 'Developer'],
    [2, 'Andreas', 'Tester'],
]);

Client::printContent($parser);
Client::printContent($csv);
Client::printContent($markdown);

Errores comunes

Un error común suele presentarse al agregar precondiciones a las implementaciones. Por ejemplo, podemos verificar si al menos existen dos filas para determinar el encabezado de la tabla de markdown

class MarkdownTable extends Parser
{
    public function output()
    {
        if (count($this->data) < 2) {
            throw new \Exception('Missing table header');
        }

        return parent::output();
    }

    public function line(array $row)
    {
        return '|' . implode('|', $row) . '|' . PHP_EOL;
    }
}

Entregar otro tipo de datos diferente en un subtipo también rompe el principio de sustitución de Liskov

class MarkdownTable extends Parser
{
    public function output()
    {
        return MarkdownWrapper(parent::output());
    }
}

Acerca de Darío Rivera

Author

Application Architect at Elentra Corp . Quality developer and passionate learner with 10+ years of experience in web technologies. Creator of EasyHttp , an standard way to consume HTTP Clients.

LinkedIn Twitter Instagram

Sólo aquellos que han alcanzado el éxito saben que siempre estuvo a un paso del momento en que pensaron renunciar.