Curso Principios SOLID - 2. Principio Abierto/Cerrado

La O de SOLID representa el principio abierto/cerrado (open/closed principle) y establece que los objetos o entidades deberían estar abiertos a su extensión pero cerrados para su modificación. Esto quiere decir que una clase debería ser fácilmente extensible sin tener la necesidad de modificar el código existente, lo cual nos permite añadir funcionalidad con la seguridad de que no afectará al código existente.

Una nueva funcionalidad puede implicar añadir nuevas clases y métodos, pero eso no debería hacer que el código que ya se ha escrito tenga que ser modificado.

¿Cómo saber si se está incumpliendo el principio open/closed?

Igual que en el principio anterior, no hay un método que nos indique con exactitud si estamos violando el principio abierto/cerrado, pero sí que hay algunos indicadores que nos dirán si lo estamos haciendo, como por ejemplo si tenemos que modificar siempre las mismas clases cada vez que hay un nuevo requisito.

Ejemplo práctico

Tenemos dos figuras diferentes que son un triángulo y un círculo. Cuando se cree una instancia de dichos objetos se van a pasar sus tres lados en caso de ser un triángulo y su radio en caso de ser un círculo. Además, vamos a tener una clase que se va a dedicar a calcular el perímetro de cada una de las figuras.

<?php

class Triangle
{
    public $sideOne, $sideTwo, $sideThree;
    public function __construct($sideOne, $sideTwo, $sideThree)
    {
        $this->sideOne = $sideOne;
        $this->sideTwo = $sideTwo;
        $this->sideThree = $sideThree;
    }
}

class Circle
{
    public $radius;
    public function __construct($radius)
    {
        $this->radius = $radius;
        $this->radius = $radius;
    }
}

class PerimeterCalculator
{
    public $shapes;

    public function __construct(array $shapes = [])
    {
        $this->shapes = $shapes;
    }

    public function sum()
    {
        $sum = 0;

        foreach ($this->shapes as $shape) {
            if ($shape instanceof Triangle) {
                $sum += $shape->sideOne + $shape->sideTwo + $shape->sideThree;
            }

            if ($shape instanceof Circle) {
                $sum += 2 * pi() * $shape->radius;
            }
        }

        return $sum;
    }
}

$shapes = [
    new Triangle(1, 2, 3),
    new Circle(2),
];

$perimeterCalculator = new PerimeterCalculator($shapes);

¿Y si quisiera crear una nueva figura y poder calcular su perímetro?

<?php

class Square
{
    public $side;

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

Además, el método sum de la clase PerimeterCalculator tengo que modificarlo para añadirle la clase Square:

public function sum()
{
    $sum = 0;
    foreach ($this->shapes as $shape) {
        if ($shape instanceof Triangle) {
            $sum += $shape->sideOne + $shape->sideTwo + $shape->sideThree;
        }

        if ($shape instanceof Circle) {
            $sum += 2 * pi() * $shape->radius;
        }

        if ($shape instanceof Square) {
            $sum += $shape->side * 4;
        }
    }

    return $sum;
}

Para hacer esto, nos vamos a ayudar de las interfaces, que son un recurso muy útil a la hora de aplicar los principios SOLID.

<?php

interface ShapeInterface
{
    public function perimeter();
}

class Triangle implements ShapeInterface
{
    public $sideOne, $sideTwo, $sideThree;

    public function __construct($sideOne, $sideTwo, $sideThree)
    {
        $this->sideOne = $sideOne;
        $this->sideTwo = $sideTwo;
        $this->sideThree = $sideThree;
    }

    public function perimeter()
    {
        return $this->sideOne + $this->sideTwo + $this->sideThree;
    }
}

class Circle implements ShapeInterface
{
    public $radius;

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

    public function perimeter()
    {
        return 2 * pi() * $this->radius;
    }
}

class Square implements ShapeInterface
{
    public $side;

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

    public function perimeter()
    {
        return $this->side * 4;
    }
}

class PerimeterCalculator
{
    public $shapes;
    public function __construct(array $shapes = [])
    {
        $this->shapes = $shapes;
    }

    public function sum()
    {
        $sum = 0;

        foreach ($this->shapes as $shape) {
            if ($shape instanceof ShapeInterface) {
                $sum += $shape->perimeter();
            } else {
                throw new Exception("Object is not a shape");
            }
        }

        return $sum;
    }
}

$shapes = [
    new Triangle(1, 2, 3),
    new Circle(2),
    new Square(4),
];

$perimeterCalculator = new PerimeterCalculator($shapes);