Principio de Sustitución de Liskov en diseño orientado a objetos (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());
}
}