Object Calisthenics com PHP (pt-br)

tadeubdev

Tadeu Barbosa

Posted on December 27, 2020

Object Calisthenics com PHP (pt-br)

Imagem por Ben @benofthenorth

Eu quero compartilhar os meus estudos recentes com PHP neste post, mais especificamente com "Object Calisthenics". São nove regras, introduzidas por Jeff Bay, que nos ajudam a codar melhor.

Um nível de indentação por método.

Essa regra nos diz que devemos criar classes e métodos com não mais que um nível de indentação. Por exemplo:

function adicionaDescricaoAFoto(string $descricao, array $fotos)
{
  foreach ($fotos as $foto) {
    if ($foto->ehEditavel()) {
      if ($foto->pertenceAUmGrupo()) {
        $descricao += "\n-----\n";
        $grupos = $photo->getGroups();
        foreach ($grupos as $grupo) {
          $descricao += $grupo->descricao;
        }
        $foto->descricao = $descricao;
      } else {
        $foto->descricao = $descricao;
        $foto->save();
      }
    } else {
      continue;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Você pode ver como esse código pode ser confuso para outras pessoas lerem? Claro, talvez esse código não seja tão confuso ou complexo quanto os códigos que vemos no nosso dia a dia. Já vi muito código que pareciam um "Hadouken".

Alt Text

Se o seu código se parece com esse acima... tem algo muito errado com ele!

Uma forma de começar a escrever códigos melhores, é separando os métodos em outros. O código a seguir me parece bem mais legível, veja se concorda:

function adicionaDescricaoAFoto(string $descricao, array $fotos)
{
  foreach ($fotos as $foto) {
    if ($foto->ehEditavel()) {
      $this->salvaDescricaoNaFoto($descricao, $fotos);
    } else {
      continue;
    }
  }
}

function salvaDescricaoNaFoto(string $descricao, Foto $fotos) {
  if ($fotos->belongsToAnGroup()) {
    $descricao += "\n-----\n";
    $grupos = $foto->pegaGrupos();

    foreach ($grupos as $grupo) {
      $descricao += $grupo->descricao;
    }

    $foto->descricao = $descricao;
    $foto->save();
  } else {
    $fotos->descricao = $descricao;
    $fotos->salva();
  }
}
Enter fullscreen mode Exit fullscreen mode

Não use ELSEs.

Eu simplesmente amo essa regra! ❤ Não sei como ou onde, mas a algum tempo atrás eu ouvi falar dessa regra e a sigo desde então. Isso tem um pouco a ver com Programação Funcional (pretendo falar sobre isso mais tarde).

Nós não precisamos usar ELSEs em nossos softwares! Se realmente estamos usando Orientação a Objetos, significa que podemos escrever códigos sem quaisquer ELSEs. O código fica bem mais claro e legível. No código a seguir, por exemplo, se renomearmos removendo o primeiro else...

function adicionaDescricaoAFoto(string $descricao, array $fotos)
{
  foreach ($fotos as $foto) {
    if ($foto->ehEditavel() === false) continue;

    $this->salvaDescricaoNaFoto($descricao, $fotos);
  }
}

function salvaDescricaoNaFoto(string $descricao, Foto $fotos) {
  if ($fotos->belongsToAnGroup() === false) {
    $fotos->descricao = $descricao;
    $fotos->salva();
    return;
  }

  $descricao += "\n-----\n";
  $grupos = $foto->pegaGrupos();

  foreach ($grupos as $grupo) {
    $descricao += $grupo->descricao;
  }

  $foto->descricao = $descricao;
  $foto->save();
}
Enter fullscreen mode Exit fullscreen mode

Você poderia evitar de usar um monte de IFELSEs no código. Duas outras regras que também são relacionadas ao Object Calisthenics, apesar de não estarem ligadas diretamente e serem realmente positivas, são: fail first (falhe primeiro) e early return (retorne quanto antes). Essas duas regras dizem a respeito de retornarmos algo nos métodos quanto antes possível, a mesma coisa para as falhas.

function pegaTipoDoUsuario(Usuario $usuario, DateTimeInterface $data)
{
  $tipo = 0;

  if ($usuario->data_registro < $data->twoMonthsAgo) {
    $tipo = 1;
  } elseif ($usuario->data_registro < $data->oneMonthAgo) {
    $tipo = 2;
  } elseif ($usuario->data_registro < $data->twoWeeksAgo) {
    $tipo = 3;
  } else {
    $tipo = 4;
  }

  return $type;
}
// um jeito melhor
function getUserType(Usuario $usuario, DateTimeInterface $data)
{
  if ($user->data_registro < $data->twoMonthsAgo) {
    return 1;
  }

  if ($user->data_registro < $data->oneMonthAgo) {
    return 2;
  }

  if ($user->data_registro < $data->twoWeeksAgo) {
    return 3;
  }

  return 4;
}
Enter fullscreen mode Exit fullscreen mode

Em caso de falhas:

function buscaAssistenteParaUsuario(Usuario $usuario)
{
  if ($usuario->temAcesso()) {
    $assistentes = Assistents::getAssistentesParaSetor($usuario->setor_id);
    if (sizeof($assistentes) > 0) {
      $assistenteIndex = rand(0, sizeof($assistentesIndex) - 1);
      $assistente = $assistentes[$assistenteIndex];
      return $assistente->id;
    } else {
      throw new AssistenteException(
        "Não foram encontrados assistentes para o setor {$usuario->setor_id}"
      );
    }
  }
  throw new UsuarioException("O usuário atual não possui acesso a quaiquer setores!");
}
// um jeito melhor
function buscaAssistenteParaUsuario(Usuario $usuario)
{
  if ($usuario->temAcesso() === false) {
    throw new UsuarioException(
      "O usuário atual não possui acesso a quaiquer setores!"
    );
  }

  $assistentes = Assistentes::getAssistentesParaSetor($usuario->setor_id);
  if (sizeof($assistentes) === 0) {
    throw new AssistenteException(
        "Não foram encontrados assistentes para o setor {$usuario->setor_id}"
      );
  }

  $assistenteIndex = rand(0, sizeof($assistentesIndex) - 1);
  $assistente = $assistentes[$assistenteIndex];
  return $assistente->id;
}
Enter fullscreen mode Exit fullscreen mode

O código parece mais legível! Concorda?

Envolva todos os primitivos e strings nas classes.

Sempre que puder substitua primitivos e strings de uma classe para outra. Por exemplo, nós temos uma classe de Usuários com as propriedades: número de telefone e email.

class Usuario
{
  private $nome;
  private $email;
  private $telefone;

  public function __construct(string $nome, string $email, string $telefone)
  {
    $this->name = $name;
    $this->email = $email;
    $this->telefone = $telefone;

    if (fiter_var($email, FILTER_VALIDATE_EMAIL) === false) {
      throw new \InvalidArgumentException(
        "Você precisa inserir um endereço de email válido!"
      );
    }

    if (fiter_var($telefone, FILTER_SANITIZE_NUMBER_INT) === false) {
      throw new \InvalidArgumentException(
        "Você precisa inserir um número de telefone válido"
      );
    }
  }
}

$usuario = User("Tadeu Barbosa", "tadeufbarbosa@gmail.com", "5531900000000");
Enter fullscreen mode Exit fullscreen mode

Ao invés disso você pode fazer:

class Usuario
{
  private $name;
  private $email;
  private $telefone;

  public function __construct(string $name, Email $email, Telefone $telefone)
  {
    $this->name = $name;
    $this->email = $email;
    $this->telefone = $telefone;
  }
}

class Email
{
  // toda a lógica da classe
}

class Telefone
{
  // toda a lógica da classe
}


$email = new Email("tadeufbarbosa@gmail.com");
$telefone = new Telefone("5531900000000");

$user = User("Tadeu Barbosa", $email, $telefone);
Enter fullscreen mode Exit fullscreen mode

Coleções de primeira classe.

Classes de coleções precisam conter somente funções para aquela coleção e nada mais que isso. Se você criar uma classe para manipular uma coleção, ela não deve ter outras responsabilidades além dessas.

class Fotos
{
  private $fotos = [];

  public function adiciona(string $foto) {/***/}
  public function remove(int $fotoIndex) {/***/}
  public function conta(): int {/***/}
}
Enter fullscreen mode Exit fullscreen mode

Um ponto por linha.

Este post contém exemplos em PHP, e o PHP usa setas e não pontos. Logo essa regra se encaixa na quantidade de setas que usamos. Deixe eu exemplificar:

class Cao
{
  public function __construct(Raca $raca)
  {
    $this->raca = $raca;
  }
}

class Raca
{
  public function __construct(string $cor
  {
    $this->cor = $cor;
  }
}

$corDaRaca = $cao->raca->cor;

// beeter way

class Cao
{
  public function __construct(Raca $raca)
  {
    $this->raca = $raca;
  }

  public function corDaRaca()
  {
    return $this->raca->cor;
  }
}

$corDaRaca = $cao->corDaRaca();
Enter fullscreen mode Exit fullscreen mode

Essa regra pode ser ignorada caso você esteja usando: Fluent Interfaces. Por exemplo de algo muito comum no Laravel:

User::query()->where("subescrito", 1)
     ->where("ativo", 1)
     ->whereDate("ultimo_acesso", Carbon::today()->subMonth())
     ->get();
Enter fullscreen mode Exit fullscreen mode

Não abrevie.

Esta regra é totalmente necessária! Eu já vi muito código, e já escrevi muito também, que levava alguns minutos para serem entendidos. Você já usou: $x, $y, $value, $i, $data ou coisas do tipo? Talvez se usarmos nomes mais descritivos, será necessário menos tempo para que nosso código seja compreendido.

class A
{
  private $data = [];

  public function fm()
  {
    foreach ($this->data as $i => $data) {
      $this->cm($data);
      $this->data[$i]--;
    }
  }
}
// uma melhor forma
class Animal
{
  private $comidas = [];

  public function estaComFome()
  {
    foreach ($this->comidas as $comidaIndex => $comida) {
      $this->coma($comida);
      $this->comidas[$comidaIndex]--;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Mantenha todas as classes com no máximo "50" linhas.

Acredite ou não, eu já trabalhei com arquivos que possuíam mais ou menos umas 4000 linhas.

Alguns dizem que cinquenta linhas chega a ser insano, então eles preferem entender esta regra como "mantenha todas as classes com no máximo 100/200 linhas" . O que importa, realmente, é manter as classes pequenas. Se a classe possui mais de 100/200 linhas, algo está errado, ela possui responsabilidades de mais. Você deveria separar esta classe em outras.

Os métodos também podem tomar muitas linhas de código. As vezes enquanto estamos escrevendo algo muito complexo, podemos tomar muita linhas de código. Mas assim que concluirmos a lógica devemos repensar no código organizá-lo. Mover algumas coisas para outros lugares, outros métodos ou talvez até outras classes!

Muitas vezes precisamos escrever algo rápido por conta de alguma requisição de um cliente ou gestor, ou líder. Daí a gente acha que vai conseguir entender aquele código no futuro... mas isso é enganar a nós mesmos.

As classes não devem possuir mais de duas variáveis de instância.

Como a última regra, pra mim, esta outra também pode ser entendida de uma forma um pouquinho diferente. Talvez pudéssemos ter cinco ou seis variáveis dentro de uma classe. Vou compartilhar um código parecido com o que aprendi recentemente num curso que fiz.

class Estudante
{
  public $nome;
  public $email;
  public $telefone;
  public $endereco;
  public $cidade;
  public $pais;
  public $cursos;
  public $notas;
  ...
}

$estudante = new Estudante(
  "Tadeu Barbosa",
  "tadeufbarbosa@gmail.com",
  "+5531900000000",
  "...",
  "...",
  "...",
  "...",
  "..."
);

// um jeito melhor
class Estudante
{
  public $pessoa;
  public $endereco;
  public $cursos;
}

$pessoa = new Pessoa(
  "Tadeu Barbosa",
  "tadeufbarbosa@gmail.com",
  "+5531900000000"
);
$endereco = new Endereco("Lorem Ipsum dolor", "..", "..");
$cursos = new Cursos([...], [...]);
$estudante = new Estudante($pessoa, $endereco, $cursos);
Enter fullscreen mode Exit fullscreen mode

Não use getters e setters.

Tenho uma certa dificuldade em entender e aplicar esta regra, sinceramente. Mas a regra diz que você não deve dar acesso à logica de uma classe a quem for a usar.

class Jogo
{
  protected $score;

  public function getScore(): int
  {
    return $this->score;
  }

  public function setScore(int $score)
  {
    $this->score = $score;
  }
}

$jogo = new Jogo();
$jogo->setScore($jogo->getScore() + 300);

// um jeito melhor
class Jogo
{
  protected $score;

  public function getScore(): int
  {
    return $this->score;
  }

  public function adcScore(int $score)
  {
    $this->score += $score;
  }

  public function removeScore(int $score)
  {
    $this->score -= $score;
  }
}

$jogo = new Jogo();
$jogo->adcScore(300);
$jogo->removeScore(100);
Enter fullscreen mode Exit fullscreen mode

Espero que esta publicação possa te ajudar de alguma forma! ;D

💖 💪 🙅 🚩
tadeubdev
Tadeu Barbosa

Posted on December 27, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related