Como desenvolvedores o que sempre buscamos é aprimorar a nossa escrita de código, não apenas no sentido de performance, buscando algoritmos mais eficientes, mas também na legibilidade. Afinal, não escrevemos código apenas para a máquina, mas para pessoas, seja outro desenvolvedor ou você mesmo no futuro. Por isso, é fundamental desenvolver utilizando boas práticas, que incluem desde arquiteturas específicas ou simplesmente alguns princípios que quando bem aplicados levam seu código para outro nível.
O que será abordado nesta publicação é o Object Calisthenics, suas regras não só de forma teórica mas também com exemplos trazendo primeiro um código 'bagunçado' e logo em seguida o refatorar aplicando essas regras.
Além desse conceito, ao implementar esse princípio, também serão explicados outros conceitos como:
- Guardianship Clause;
- Fail-Fast;
- Early Return;
- Primitive Obsession
Conceito
Primeiro, o que é o Object Calisthenics afinal? Criado por Jeff Bay no seu livro ThoughtWorks Anthology, é um conjunto de princípios que buscam aprimorar a qualidade do código e tornar mais fácil a manutenção e os testes especificamente na programação orientada a objeto.
São as 9 regras:
- Only One Level Of Indentation Per Method
- Don’t Use The ELSE Keyword
- Wrap All Primitives And Strings
- First Class Collections
- One Dot Per Line
- Don’t Abbreviate
- Keep All Entities Small
- No Classes With More Than Two Instance Variables
- No Getters/Setters/Properties
Vamos entender com mais detalhes cada uma delas.
Antes de prosseguir é importante lembrar que não necessariamente é obrigatório seguir todas as 'regras', vão existir exceções e situações em que o que é definido na regra, pode ser considerado 'negociável'.
1. Only One Level Of Indentation Per Method / 2. Don’t Use The ELSE Keyword
Vamos ver as 2 primeiras regras (1. Apenas 1 nível de indentação por método e 2. Não use ELSE) juntas pois elas se completam.
Vamos analisar esse código:
public Employee(String name, int age, String role, String department) {
if (!name.equals("")) {
if (age >= 18) {
if ("accountant".equals(role) || "seller".equals(role)) {
if ("sales".equals(department) || "administrative".equals(department)) {
this.age = age;
this.name = name;
this.role = role;
this.department = department;
} else {
throw new IllegalArgumentException("Invalid department");
}
} else {
throw new IllegalArgumentException("Invalid role");
}
} else {
throw new IllegalArgumentException("Invalid age");
}
} else {
throw new IllegalArgumentException("Invalid name");
}
}
Este código é bem simples, ele é o método construtor da classe Employee
que faz um conjunto de validações dos valores antes de instanciar de fato a classe e prosseguir com o código. Porém mesmo sendo um código simples, já se percebe de cara a quantidade de if's presentes nesse processo de validação, tornando o código complicado de se ler.
Mas as validações são necessárias para instanciar essa classe, então como podemos arrumar isso sem arruinar a funcionalidade do código?
Neste caso é utilizar o princípio do Guardianship Clause
juntamente com o Fail-Fast
ou o Early Return
. Podemos definir esses 3 termos de forma simples:
- Guardianship Clause são cláusulas que verificam condições necessárias ou inesperadas no inicio de um método.
- Fail-Fast significa algo como errar logo/imediatamente, essa abordagem prioriza a detecção e tratamento imediato de erros e falhas.
- Early Return pode ser traduzido como retornar o mais cedo possível, aqui é priorizado a situação esperada do método/função.
Então vamos começar aplicando isso no nosso código, usando a abordagem do Fail-Fast
na verificação de if (!name.equals(""))
que verifica se name
é um nome válido.
public Employee(String name, int age, String role, String department) {
if (name.equals("")) {
throw new IllegalArgumentException("Invalid name");
}
if (age >= 18) {
if ("accountant".equals(role) || "seller".equals(role)) {
if ("sales".equals(department) || "administrative".equals(department)) {
this.age = age;
this.name = name;
this.role = role;
this.department = department;
} else {
throw new IllegalArgumentException("Invalid department");
}
} else {
throw new IllegalArgumentException("Invalid role");
}
} else {
throw new IllegalArgumentException("Invalid age");
}
}
Note que como estamos usando o Fail-Fast, ao invés de verificar se name
tem um valor válido, estamos verificando se ele tem um valor inválido para assim tratar logo isso, nesse caso retornando um erro e impedindo que a função prossiga já que o dado não é válido.
Com essa mudança fomos de 4 blocos de if-else para 3, agora é só aplicar a mesma lógica nos 3 ifs restantes:
public Employee(String name, int age, String role, String department) {
if (name.equals("")) {
throw new IllegalArgumentException("Invalid name");
}
if (age < 18) {
throw new IllegalArgumentException("Invalid age");
}
if (!"accountant".equals(role) && !"seller".equals(role)) {
throw new IllegalArgumentException("Invalid role");
}
if (!"sales".equals(department) && !"administrative".equals(department)) {
throw new IllegalArgumentException("Invalid department");
}
this.age = age;
this.name = name;
this.role = role;
this.department = department;
}
}
E agora temos o código funcionando da mesma forma porém dessa vez muito mais legível, sem else
, e atendendo também a regra de apenas 1 nível de indentação.
3. Envolva seus tipos primitivos e strings
Muitas vezes acabamos caindo no chamado Primitive Obsession, essa obsessão por tipos primitivos acontece quando usamos tipos primitivos para representar objetos que possuem comportamentos específicos. Essa regra busca eliminar esse problema, encapsulando os tipos primitivos em objetos.
Vamos entender na prática, na forma como código se encontra agora, note como cada atributo tem um comportamento próprio que não são comportamentos primitivos de uma String. Porém as validações dos valores do atributo acontecem dentro do constructor
do objeto Employee
, ao deixar essas verificações espalhadas pela classe, começamos a violar o princípio de responsabilidade única além de tornar o código menos reutilizável e dificultoso de ser incrementado.
Sendo assim, podemos resolver isso criando uma classe pra cada atributo assim a classe é responsável por fazer a sua própria validação ao ser instanciada:
class Age {
private int value;
public Age(int age){
if (age < 18) {
throw new IllegalArgumentException("Invalid age");
}
this.value = age;
}
public int getValue(){
return this.value;
}
}
class Name {
private String value;
public Name(String name){
if (name.equals("")) {
throw new IllegalArgumentException("Invalid name");
}
this.value = name;
}
public String getValue(){
return this.value;
}
}
class Role {
private String value;
public Role(String role){
if (!"accountant".equals(role) && !"seller".equals(role)) {
throw new IllegalArgumentException("Invalid role");
}
this.value = role;
}
public String getValue(){
return this.value;
}
}
class Department {
private String value;
public Department(String department){
if (!"sales".equals(department) && !"administrative".equals(department)) {
throw new IllegalArgumentException("Invalid department");
}
this.value = department;
}
public String getValue(){
return this.value;
}
}
Dessa forma, podemos atualizar a classe Employee:
class Employee {
private Name name;
private Age age;
private Role role;
private Department department;
public Employee(Name name, Age age, Role role, Department department) {
this.age = age;
this.name = name;
this.role = role;
this.department = department;
}
}
4. First class collections
Essa regra diz que se você tem uma coleção de elementos, tenha uma classe dedicada a essa coleção, ao invés de usar diretamente a lista como um atributo de outra classe. Mais uma vez vamos esclarecer isso com um exemplo prático, já temos uma classe para o Employee, vamos criar uma classe para a organização:
class Company {
private List<Employee> employees;
private LegalName legalName;
private CNPJ cnpj;
public Company(List<Employee> employees, LegalName legalName, CNPJ cnpj) {
this.employees = employees;
this.legalName = legalName;
this.cnpj = cnpj;
}
public List<Employee> getEmployees() {
return employees;
}
// demais getters...
}
Para aplicar o First Class Collections, vamos encapsular a coleção dentro de uma classe, assim os comportamentos dessa coleção ficarão dentro da sua classe dedicada, então, criando a classe Employees
, podemos ter métodos exclusivos do conjunto de instâncias de Employee
, como, getters
de funcionários de um cargo (Role
) ou departamento específicos.
class Employees {
private List<Employee> value;
public Employees(List<Employee> employees) {
if (employees.isEmpty()){
throw new IllegalArgumentException("Empty employee List");
}
this.value = employees;
}
public List<Employee> getValue() {
return value;
}
public ByRole(Role role){
// restante do código
}
public ByDepartment(Department department){
// restante do código
}
// outros métodos da **lista** como adicionar um novo employee, remover employees de um departamento especifico, etc...
}
Agora podemos atualizar a classe Company
:
class Company {
private Employees employees;
private LegalName legalName;
private CNPJ cnpj;
public Company(Employees employees, LegalName legalName, CNPJ cnpj) {
this.employees = employees;
this.legalName = legalName;
this.cnpj = cnpj;
}
public Employees getEmployees() {
return employees;
}
}
5. One Dot Per Line
Vamos adicionar ao Department
um chefe do departamento:
class Employee {
// restante do código ...
public Department getDepartment() {
return this.department;
}
// restante do código ...
}
class Department {
// restante do código ...
private DepartmentHead departmentHead;
// restante do código ...
public DepartmentHead getDepartmentHead() {
return this.departmentHead;
}
}
class DepartmentHead {
private Name name;
private Age age;
private Role role;
// constructor da classe
public Name getName() {
return this.name;
}
}
Dessa forma para por exemplo, ter acesso ao nome do chefe do departamento que um Employee
X faz parte, teríamos que usar algo como:
employee.getDepartment().getDepartmentHead().getName().getValue()
.
Quantos pontos aqui né? Da maneira atual, geramos uma espécie de cadeia de chamadas o que resulta na exposição da estrutura da classe sem necessidade pra isso, além de dificultar a leitura do trecho do código.
Podemos resolver isso fazendo com que os objetos tenham métodos que expressem claramente suas intenções, assim ao invés usar o método getDepartment
para obter o objeto de departamento do Employee
e só depois buscar o nome do DepartmentHead
, usamos métodos que vão retornar os valores específicos para isso, vamos esclarecer isso no código:
class Employee {
// restante do código ...
// antes:
// public Department getDepartment() {
// return this.department;
// }
public String getDepartmentHead() {
return this.department.getDepartmentHead();
}
// restante do código ...
}
class Department {
// restante do código
public String getDepartmentHead() {
// código antigo: return this.departmentHead;
return this.departmentHead.getName();
}
}
class DepartmentHead {
// restante do código
public String getName() {
// código antigo: return this.name;
return this.name.getValue();
}
}
Feito isso, agora para obter o mesmo valor que tentamos antes, precisamos de:
// antes: employee.getDepartment().getDepartmentHead().getName().getValue();
employee.getDepartmentHead();
6. Don’t Abbreviate
Essa regra é bem simples e nem precisa de código para entender, não abrevie!
Mas embora o conceito dela seja simples, muitas vezes violamos essa regra sem nem mesmo perceber. Talvez você já se viu em uma situação em que usou a abreviação como uma forma de fugir da repetição de código, mas aqui já entramos em outro ponto, o problema não está na repetição em si, e sim em algo mais profundo na estrutura do código. Ao seguir essa regra, conseguimos perceber falhas de design que antes poderiam passar despercebidas.
7. Keep All Entities Small
Essa regra diz para mantermos nossas classes pequenas, com no máximo 50 linhas, e naturalmente isso não é nem um pouco fácil e muitas vezes pode se tornar até inviável a depender da complexidade da entidade, isso frequentemente é discutido na comunidade dev, alguns preferem usar essa regra tendo em mente 150 linhas, outros 200, enfim.
Mas como havia dito no início dessa publicação, essas regras são um conjunto de princípios para conseguir alcançar boas práticas, e não uma fórmula perfeita que se seguida rigidamente entrega o melhor software do mundo.
Assim, não tem problema a sua classe passar de 50 linhas, desde que as linhas que constroem essa classe, sejam de fato necessárias, então colocar essa regra em prática é se certificar que o que pode ser reduzido na classe, foi reduzido mas sempre de forma coerente.
8. No Classes With More Than Two Instance Variables
Essa regra diz que não devemos ter mais de 2 instâncias de variável em uma classe, o objetivo dela é reforçar a coesão e composição da classe, pois se uma classe tem vários atributos, isso é um sinal de que ela está assumindo mais responsabilidades do que ela deveria.
Para isso vamos analisar os atributos da classe Employee
:
class Employee {
private Name name;
private Age age;
private Role role;
private Department department;
// restante do código ...
}
De forma geral, para solucionar esse problema, o que fazemos é quebrar o objeto em objetos menores pra melhor representar o conceito que a classe representa. No nosso exemplo, o nome e a idade são informações pessoais do colaborador, enquanto Cargo e departamento são informações empresariais, então vamos agrupar e corrigir isso:
class PersonalInfo {
private Name name;
private Age age;
public PersonalInfo(Name name, Age age) {
this.name = name;
this.age = age;
}
// getters e setters
}
class JobInfo {
private Role role;
private Department department;
public JobInfo(Role role, Department department) {
this.role = role;
this.department = department;
}
// getters e setters
}
class Employee {
private PersonalInfo personalInfo;
private JobInfo jobInfo;
public Employee(PersonalInfo personalInfo, JobInfo jobInfo) {
this.personalInfo = personalInfo;
this.jobInfo = jobInfo;
}
// restante do código ...
}
9. No Getters/Setters/Properties
Essa regra busca acabar com a exposição de dados internos através dos getters, setters ou propriedades que ao invés de encapsular o comportamento, apenas expõem os dados, violando o o comportamento orientado a objeto.
Vamos ao exemplo no nosso código:
class JobInfo {
private Role role;
// restante do código
public void setRole(Role newRole) {
this.role = newRole;
}
// restante do código ...
}
Da forma atual, para atualizar a informação do cargo, usaríamos o setRole
, o que funcionaria, sim, porém vamos lembrar que estamos falando de uma classe, um objeto com seus comportamentos, então algo como mudança de cargo, são ações do contexto, como por exemplo, ser promovido:
class JobInfo {
private Role role;
// restante do código ...
public void promoteToManager(){
this.role = new Role("manager");
}
// restante do código ...
}
Conclusão
O Object Calisthenics não é uma receita perfeita que deve ser seguida à risca, mas um conjunto de exercícios (o nome calistenia não é a toa xd) que nos faz enxergar pontos de melhoria e desenvolver bons costumes quando escrevemos nosso código.
No final das contas, o objetivo é escrever um código que não somente a máquina vai entender e executar, mas que outros desenvolvedores possam compreender, evoluir e manter ao longo do tempo.
Top comments (0)