Cuando aprendemos programación orientada a objetos leemos y nos enseñan cientos de ejemplos de cómo crear y usar getters y setters. Nos dicen lo maravillosos que son para acceder a nuestros atributos de clase "privados". Seguramente te suene algo parecido al siguiente código, ¿verdad?:

<?php

class Dog
{
    private string $name;

    private float $weight;

    public function getName(): string
    {
        $this->$name;
    }

    public function setName(string $name)
    {
        $this->name = $name;
    }

    public function getWeight(): float
    {
        return $this->weight;
    }

    public function setWeight(float $weight)
    {
        $this->weight = $weight;
    }
}

$sky = new Dog();
$sky->setName('Sky');
$sky->setWeight(10.5);

Maravilloso. Tenemos un precioso Husky de 10.5 kg de peso. Pero, ¡cuidado! por qué...

Cuando añades getters y setters para tus atributos básicamente lo que haces es convertirlos a públicos. Estás exponiendo al mundo las entrañas de tus objetos.

¿Habría alguna diferencia si eliminaras esos setters y getters y los convirtieras en atributos públicos? Ninguna. La gracia de convertir los atributos de clase a privados es evitar que cualquiera de fuera pueda modificar el estado de nuestros objetos de manera irresponsable.

Según la Wikipedia se denomina encapsulamiento al ocultamiento del estado, es decir, de los datos miembro de un objeto de manera que solo se pueda cambiar mediante las operaciones definidas para ese objeto

Cabe recordad que la programación orientada a objetos, básicamente, se define en agrupar un conjunto de datos con operaciones que operan sobre esos datos, y a esto le llamamos clases.

Las clases tienen atributos y comportamiento. Preguntar (getters) a un objeto sobre sus datos significa que algo del mundo exterior necesita algo de nuestros objetos para realizar alguna operación.

Veámoslo con un ejemplo. Tenemos un usuario y queremos mandarle un e-mail felicitándole su cumpleaños cuando se registra en nuestra web si tiene la increíble suerte de registrarse el mismo día en el que nació:

class User
{
    private string $name;

    private \DateTimeImmutable $birthday;

    public function __construct(string $name, \DateTimeImmutable $birthday)
    {
        $this->name = $name;
        $this->birthday = $birthday;
    }

    public function getBirthDay(): \DateTimeImmutable
    {
        return $this->birthday;
    }
}

final class CreateUser
{
    public function __construct(Notification $notification)
    {
        $this->notification = $notification;
    }

    public function __invoke(string $name, \DateTimeImmutable $birthday): User
    {
        $user = new User($name, $birthday);

        $today = new \DateTimeImmutable();
        if ($user->getBirthDay()->format('m-d') === $today->format('m-d')) {
            $this->notification->send('Happy Birthday!');
        }
    }
}

Alguien externo, el servicio CreateUser, quiere conocer algún detalle interno de nuestra clase User para realizar una operación. Necesita saber la fecha del cumpleaños del usuario para saber si es hoy y en ese caso mandarle una notificación.

Cuando realizas una operación sobre o con la ayuda atributos de una clase fuera de ésta significa que te estás acoplando y tú diseño está roto. No has encapsulado lo suficiente y tus clases se acoplan entre sí.

Ahora el servicio CreateUser está acoplado a un detalle de la clase User, el formato de fecha del cumpleaños. Si cómo almaceno la fecha de cumpleaños de un usuario cambia (por ejemplo, ya no es un DateTimeImmutable sino un string o un ValueObject) voy a tener que ir a todas y cada una de las partes de mi código dónde se decide si hoy es la fecha de cumpleaños de un usuario o no. Nuestro código no escala.

Modelo de Dominio Anémico

Tener clases únicamente llenas de getters y setters es un claro síntoma de un Modelo de Dominio Anémico. Nuestras clases además de estar fuertemente cohesionadas deben ser ricas en semántica. Iteremos el código visto anteriormente:

class User
{
    private string $name;

    private \DateTimeImmutable $birthday;

    public function __construct(string $name, \DateTimeImmutable $birthday)
    {
        $this->name = $name;
        $this->birthday = $birthday;
    }

    public function isMyBirthday(): bool
    {
        $today = new \DateTimeImmutable();

        if ($user->getBirthDay()->format('m-d') === $today->format('m-d')) {
            return true;
        }

        return false;
    }
}

final class CreateUser
{
    public function __construct(Notification $notification)
    {
        $this->notification = $notification;
    }

    public function __invoke(string $name, \DateTimeImmutable $birthday): User
    {
        $user = new User($name, $birthday);

        if ($user->isMyBirthday()) {
            $this->notification->send('Happy Birthday!');
        }
    }
}

Ahora nuestra clase presenta una cohesión mayor porque los datos (fecha de cumpleaños) y las reglas de negocio (¿es mi cumpleaños?) están en el mismo lugar: la clase User.

¿Estás diciendo que mi pobre perrito Sky sufre de anemia 🙁 ? Iteraremos una vez más pues:

class Dog
{
    private DogName $name;

    private Weight $weight;

    public function __construct(AnimalName $name, Weight $weight)
    {
        $this->name = $name;
        $this->weight = $weight;
    }

    public function eat(Food $food)
    {
        // The dog increases its weight when it is sleeping
        $this->weight += $food->weight();
    }

    public function sleep(int $howLong)
    {
        //The dog reduces its weight when it is sleeping
        $this->weight -= $howLong * 0.01;
    }

    public function play(Toy $toy)
    {
        echo "$this->name is playing with $toy";
    }
}

No abuses de la capa de servicios

Un error muy extendido es trasladar toda la lógica de negocio a servicios dejando un montón de entidades de dominio llenas de atributos y sus correspondientes getters y setters pero desprovistas de comportamiento. Estás violando los principios fundamentales de POO.

Intenta escribir la lógica de negocio en tus modelos de dominio en una primera iteración y sólo cuando sea necesario mueve tu lógica a la capa de servicios de dominio.

Recuerda...

  • La lógica de negocio (el comportamiento) y los atributos (los datos) están en un mismo lugar. Las clases tienen que presentar una alta cohesión entre sus atributos y sus métodos
  • Máxima encapsulación: por ejemplo ahora la clase Dog tiene las reglas de cómo aumenta o cómo disminuye su peso sin necesidad de exponer sus atributos al mundo exterior para que algo externa a ella decida sobre estas reglas o sus atributos
  • Cumplimos con la S (Single Responsibility Principle) de SOLID: se ve más claro con la iteración que hemos realizado sobre la clase User. Al mover la regla de negocio de "¿es mi cumpleaños?" desde el servicio CreateUser a la clase User estamos eliminando "preocupaciones" o motivos de cambio de la clase CreateUser que no le corresponden: si la implementación de la fecha de cumpleaños de un usuario cambia (ahora pasa de \DateTimeInmutable a un string) la clase CreateUser ya no tendrá ningún motivo para cambiar
  • Nuestras clases son ricas en semántica: se parecen a objetos del mundo real. Un perro come, juega, duerme... No son únicamente una bolsa de getters y setters, para eso utiliza DTOs
  • No abuses de la capa de servicios. Tu modelo de dominio guía el diseño de tus clases y no a la inversa. Mueve la lógica de negocio a la capa de servicios cuando realmente sea necesario

Y si te apetece leer más te recomiendo:

Tanto si estás de acuerdo como si no regístrate y deja tus comentarios