DEV Community

Daniel Neto
Daniel Neto

Posted on • Edited on

Object Calisthenics em PHP

Object calisthenics são um conjunto de boas práticas em orientação a objetos, propostas por Jeff Bay, que visam melhorar a “forma” dos nossos objetos. Ao todo são nove regras que nos ajudam a aumentar a qualidade nossos projetos, melhorando aspectos como manutenibilidade, extensibilidade, testabilidade e coesão.

Os exemplos a seguir são elaborados em PHP, mas a regras se aplicam a qualquer linguagem com suporte a orientação a objetos.

Resumo da ópera:

  • Apenas um nível de indentação por método
  • Não use else
  • Envolva seus tipos primitivos
  • Envolva suas coleções
  • Use apenas uma chamada por linha
  • Nunca utilize abreviações
  • Mantenha classes e pacotes pequenos
  • Tenha no máximo dois atributos por classe
  • Não use getters e setters

Então vamos lá colocar nossos objetos para se exercitarem.

Apenas um nível de indentação por método

A primeira regra nos orienta a ter apenas um nível de indentação por método, ou seja, não devemos utilizar estruturas aninhadas.

Veja a seguir que o método hasAccess verifica se o usuário tem acesso liberado caso tenha sido confirmado e o seu último acesso tenha acontecido nos últimos 90 dias:

public function hasAccess(): bool
{
    if ($this->isConfirmed) {
        $today = new DateTimeImmutable();

        if ($this->lastAccess->diff($today)->days >= 90) {
            return false;
        } else {
            return true;
        }
    } else {
        return false;
    }
}
Enter fullscreen mode Exit fullscreen mode

Podemos evitar o aninhamento de estruturas extraindo trechos em métodos privados que façam sentido, utilizando a estratégia de early return ou fail first. Melhorando o trecho apresentado anteriormente, ele fica assim:

public function hasAccess(): bool
{
    if (!$this->isConfirmed) {
        return false;
    }

    $today = new DateTimeImmutable();

    return $this->lastAccess->diff($today)->days >= 90;
}
Enter fullscreen mode Exit fullscreen mode

Neste exemplo representamos apenas if aninhados, mas a regra também se aplica a outras estruturas condicionais, laços de repetição e ao que mais poder ser aninhado.

Não use else

Essa regra parece intimidadora e não fazer sentido, mas na verdade é muito fácil aplicá-la e realmente melhora a legibilidade do código — pudemos ver isso no exemplo anterior.

Sua aplicação é tão simples quanto é descrito: simplesmente não usamos else, em lugar nenhum. Em vez disso, podemos fazer uso das estratégias usadas anteriormente, como early return e fail first. O exemplo apresentado anteriormente também aplica essa regra.

Envolva seus tipos primitivos

Quando as propriedades de uma entidade possuem algum comportamento (regra ou validação), é uma boa prática encapsulá-las em classes independentes, o que aumenta a coesão e possibilita o reaproveitamento de código.

Veja no exemplo a seguir, temos uma classe que possui um atributo email, que é validado ao ser atribuído no método construtor:

class User
{
    private string $email;

    public function __construct(string $email)
    {
        $this->setEmail($email);
    }

    private function setEmail(string $email): void
    {
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            throw new InvalidArgumentException('Invalid e-mail address');
        }

        $this->email = $email;
    }

    public function getEmail(): string
    {
        return $this->email;
    }
}
Enter fullscreen mode Exit fullscreen mode

refatorando isso, temos:

class Email
{
    public function __construct(private string $email)
    {
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            throw new InvalidArgumentException('Invalid e-mail address');
        }
    }

    public function __toString(): string
    {
        return $this->email;
    }
}
Enter fullscreen mode Exit fullscreen mode

e:

class User
{
    private Email $email;
    private string $name;

    public function getEmail(): string
    {
        return $this->email;
    }
}
Enter fullscreen mode Exit fullscreen mode

Desta forma a class User não fica sabendo das regras relacionadas ao comportamento de Email, que também pode ser reaproveitado em outras entidades.

Envolva suas coleções

A quarta regra nos recomenda envolver coleções em classes específicas que contenham apenas o comportamento relacionado à sua manipulação. Assim, também teremos maior coesão e encapsulamento das nossas classes.

Observe o seguinte cenário, em que a classe User, além de tratar seus próprios atributos, precisa lidar com o comportamento da lista de emails:

class User
{
    private SplObjectStore $emails;

    public function __construct()
    {
        $this->emails = new SplObjectStore();
    }

    public function someMethod(): void
    {
        // do something...
    }

    public function addEmail(Email $email): void
    {
        $this->emails->attach($email);
    }

    public function removeEmail(Email $email): void
    {
        $this->emails->dettach($email);
    }

    public function countEmails(): int
    {
        return $this->emails->count();
    }
}
Enter fullscreen mode Exit fullscreen mode

Ao extrair a coleção para sua própria classe, temos:

class Emails
{
    private SplObjectStore $emails;

    public function __construct()
    {
        $this->emails = new SplObjectStore();
    }

    public function addEmail(Email $email): void
    {
        $this->emails->attach($email);
    }

    public function removeEmail(Email $email): void
    {
        $this->emails->dettach($email);
    }

    public function countEmails(): int
    {
        return $this->emails->count();
    }
}
Enter fullscreen mode Exit fullscreen mode

Por fim, a classe User apenas faz uso da lista, sem se preocupar com a sua implementação:

class User
{
    private Emails $emails;

    public function __construct()
    {
        $this->emails = new Emails();
    }

    public function someMethod(): void
    {
        // do something...
    }
}
Enter fullscreen mode Exit fullscreen mode

Utilizar esta técnica em linguagens dinâmicas, que não suportam tipos genéricos, caso do PHP, é uma ótima forma de garantir a consistência de tipo dos itens manipulados pela coleção.

Use apenas uma chamada de atributo ou método por linha

Essa regra nos diz que devemos utilizar apenas uma chamada de atributo ou método por linha — “ponto” na marioria das linguagens, “→” no PHP.

Se o nosso código estiver realizando várias chamadas em sequência, isso é um forte indício de que uma parte precisa conhecer muito a outra para realizar alguma ação, violando o encapsulamento das entidades.

No próximo exemplo, temos uma classe que recebe um usuário e realiza alguma ação se ele tiver mais que 18 anos. Para isso, o atributo birthdate da classe User precisa ser acessado:

class User
{
    public function birthdate(): DateTimeInterface
    {
        return $this->birthdate;
    }
}
Enter fullscreen mode Exit fullscreen mode
class SomeAction
{
    public function execute(User $user): void
    {
        $today = new DateTimeImmutable();
        $age = $user->birthdate()->diff($today)->y;

        if ($age < 18) {
            return;
        }

        // do something...
    }
}
Enter fullscreen mode Exit fullscreen mode

O trecho $user->birthdate()->diff($today)->y possui três níveis de chamada, violando a regra. Para melhorar isso, a classe User pode prover um método que retorne a idade, assim a classe SomeAction pode utilizá-lo sem depender da sua implementação:

class User
{
        public function age(): int
    {
        $today = new DateTimeImmutable();

        return $this->birthdate->diff($today)->y;
    }
}
Enter fullscreen mode Exit fullscreen mode
class SomeAction
{
    public function execute(User $user): void
    {
        if ($user->age < 18) {
            return;
        }

        // do something...
    }
}
Enter fullscreen mode Exit fullscreen mode

Nunca utilize abreviações

Usar abreviações, para muitos, é algo corriqueiro e “inocente”. Mas é uma má prática. Nós, desenvolvedores de software, precisamos lidar com uma grande carga cognitiva todos os dias, então quanto mais simples as coisas forem, melhor. E usar abreviações é algo que dificulta, e muito, as coisas!

Imagine ter que lidar com regras de negócio complexas e ainda ter que decifrar o que cada atributo ou método faz porque o nome está abreviado… isso é algo que pode, e deve, ser evitado. Novos membros da sua equipe, e até mesmo você, quando for mexer no seu próprio código tempos depois, irá agradecer se não usar abreviações.

Portanto, sempre dê nomes completos e elucidativos para atributos e métodos. Evite coisas do tipo:

class User
{
    private string $fName,
    private string $lName;
    private DateTimeInterface $bd;
}
Enter fullscreen mode Exit fullscreen mode

Prefira isto:

class User
{
    private string $firstName;
    private string $lastName;
    private DateTimeInterface $birthDate;
}
Enter fullscreen mode Exit fullscreen mode

Mantenha classes e pacotes pequenos

Originalmente essa regra diz que devemos manter classes com no máximo 10 linhas e pacotes (namespaces) com no máximo 10 classes. Essa é uma das regras mais desafiadoras e difíceis de ser seguida.

Mesmo que não seja sempre seguida à risca, é importante sempre procurar manter as classes e pacotes com o menor tamanho possível. A extração de tipos primitivos e coleções pode ser seu aliado na aplicação desta regra.

Tenha no máximo dois atributos por classe

De forma semelhante a regra anterior, essa regra diz que devemos ter, no máximo, dois atributos por classe.

Mesmo que nem sempre seja possível aplicar isso à risca, é uma boa prática manter a menor quantidade de atributos numa classe. Novamente, a extração de tipos primitivos e coleções pode nos ajudar. Veja este exemplo:

class User
{
    private string $firstName;
    private string $lastName;
    private DateTimeInterface $birthDate;

      public function fullName(): string
    {
                return "{$this->firstName} {$this->lastName}";
        }
}
Enter fullscreen mode Exit fullscreen mode

Os atributos firstName e lastName têm como finalidade representar um nome, então faz sentido envolver esses tipos primitivos numa classe específica e enxugar a classe User:

class Name
{
    private string $firstName;
    private string $lastName;

    public function __toString(): string
    {
        return "{$this->firstName} {$this->lastName}";
    }
}
Enter fullscreen mode Exit fullscreen mode
class User
{
    private Name $name;
    private DateTimeInterface $birthDate;

      public function fullName(): string
    {
                return "$this->fname";
        }
}
Enter fullscreen mode Exit fullscreen mode

Refatorações e análises constantes da nossa base de código é o caminho!

Não use getters e setters

Para encerrar com chave de ouro: essa regra que aparentemente bizarra.

“Como assim? Eu aprendi que em orientação a objetos devemos encapsular nossos atributos usando getters e setters!”

É verdade, devemos prezar pelo encapsulamento, e este é um dos pilares de object calisthenics. Acontece que o uso irrestrito de getters e setters não nos provê absolutamente nenhum encapsulamento. Ter todos atributos públicos ou ter todos atributos privados com métodos getters e setters é exatamente a mesma coisa.

O que fazer então? Devemos utilizar métodos específicos para situações específicas, apenas quando for necessário. Neste exemplo vemos o que, talvez, estamos acostumados a fazer:

class Video
{
    private bool $visible;

    public function getVisible(): bool
    {
        return $this->status;
    }

    public function setVisible(bool $visible): void
    {
        $this->visible = $visible;
    }
}
Enter fullscreen mode Exit fullscreen mode

Uma pequena alteração nos nomes dos métodos já é o suficiente para enriquecer nosso domínio e deixar as regras mais claras:

class Video
{
    private bool $visible;

    public function isPublic(): bool
    {
        return $this->visible;
    }

    public function publish(): void
    {
        $this->visible = true;
    }
}
Enter fullscreen mode Exit fullscreen mode

Seguindo essa linha de raciocínio, se nenhuma entidade externa precisar saber o estado de um Video, o método isPublic simplesmente deve deixar de existir.

De maneira semelhante, se outra ação precisar ser executada sobre o atributo $visible, outro método com nome esclarecedor deve ser criado. Se estivéssemos utilizando o método setVisible, toda a regra de negócio seria resolvida externamente, e o encapsulamento completamente violado.

Considerações finais

Vimos algumas regras mais simples, outras mais difíceis de serem aplicadas, mas a regra máxima que devemos seguir é: utilize o bom senso para definir quando alguma delas deve ou não ser utilizada. Realizar revisões e refatorações constantemente deve ser um costume, para que nós, como profissionais, e os nossos produtos, estejam em constante evolução.

Se encontrou algum erro, tem alguma sugestão ou dúvida, não deixe de se reportar nos comentários ;-)

Top comments (0)