OO na prática: o problema de objetos não consistentes

OO na prática: o problema de objetos não consistentes
maniche
maniche

Compartilhe

Programar orientado a objetos é sempre um desafio. E o desafio está justamente na dificuldade de criarmos classes que são fáceis de serem usadas, mantidas e reutilizadas.

Uma classe fácil de ser usada, dentre várias coisas, é aquela onde as pré-condições são simples. Por pré-condições, entenda tudo aquilo que você precisa fazer antes de conseguir invocar o método que quer. Por exemplo, para invocar b(), você é obrigado a invocar a() antes, passando um número positivo pra ele.

Veja o trecho de código abaixo, extraído do código-fonte do Alura, nossa plataforma de ensino à distância. Quando o aluno entra em uma trilha (Path), ele vê cada um dos cursos dessa trilha, bem como seu andamento. Nossa equipe optou por criar a classe PathPosition, que contém um Path e uma lista de _Position_s, que contém um curso e a porcentagem de andamento desse curso.

Banner da Escola de Programação: Matricula-se na escola de Programação. Junte-se a uma comunidade de mais de 500 mil estudantes. Na Alura você tem acesso a todos os cursos em uma única assinatura; tem novos lançamentos a cada semana; desafios práticos. Clique e saiba mais!

Não se preocupe com os detalhes do código, pense apenas no alto nível, afinal, a parte boa de um projeto de classes OO é justamente que você não precisa saber sobre os detalhes de implementação de cada classe. Você precisa apenas entender quais são os comportamentos que ela provê.

 public class PathPosition { private final List<Position> positions; private final Path path;

public PathPosition(Path path) { this.path = path; positions = path.getCoursesEvenPrivate().stream(). map(PositionWithoutEnroll::new). collect(toList()); }

public int getTotalPositions() { return positions.size(); }

public boolean isCompleted() { return positions.stream().allMatch(Position::hasFinished); }

// continua aqui } 

Sendo assim, é fácil usar a classe. Basta instanciá-la e invocar seus comportamentos (isCompleted() para saber se terminou a trilha e getTotalPositions() para saber quantos posições tem nessa trilha):

 Path trilhaDeJava = trilhas.pega("java"); PathPosition p = new PathPosition(trilhaDeJava);

boolean terminou = p.isCompleted(); 

Apesar de parecer tudo certo, acredite, o código acima não funciona. Se você conseguisse olhar o código dessa classe por completo, perceberia que as instâncias dessa classe podem ser inconsistente. Uma instância inconsistente é aquela que está em um estado inválido, ou seja, contém atributos com valores não válidos (nulos, números negativos onde só deveriam ser positivos, e etc). E isso, óbvio, faz com que a classe possa responder de maneira incorreta.

A lista de Position, existente na classe PathPosition, só fica correta depois do programador invocar várias vezes o método merge(). Esse é a pré-condição da classe:

 public void merge(PositionWithEnroll position) { // faz alguma coisa aqui na lista de Position } 

Ou seja, para que ela esteja pronta para uso, precisamos invocar o método merge():

 Path trilhaDeJava = trilhas.pega("java"); PathPosition p = new PathPosition(trilhaDeJava);

// até aqui, "p" é inconsistente! // não podemos confiar no isCompleted() boolean mentirinha = p.isCompleted();

for(PositionWithEnroll enroll : posicoes) { p.merge(enroll); }

// agora sim, pode usar "p" // o isCompleted agora falará a verdade boolean verdadinha = p.isCompleted(); 

Mas, se você tem uma instancia de PathPosition na mão, como saber se ele está pronto pra uso ou não? Esse é o problema de objetos inconsistentes: você nunca sabe se ele contém dados válidos e se você pode usar toda a classe.

Lembre-se que o usuário deve ser capaz de invocar qualquer método a qualquer momento. Portanto, que você for desenhar sua classe, proíba-a de estar em um estado inconsistente. Para isso, use e abuse de construtores: Se sua classe precisa de alguns dados desde o começo, peça-os no construtor. Uma classe CPF sem um número de CPF é inconsistente. Ou seja, peça o número no construtor. Também valide os dados passados: Se o usuário passar um valor negativo onde não deveria, sua classe deve recusar esse valor.

Nesse caso em particular, nossa solução foi criar um PathPositionBuilder, que cria uma instância de PathPosition, já consistente. Mudamos também o modificador de visibilidade do construtor para que ninguém consiga instanciar um PathPosition diretamente.

Novamente, faça com que suas classes sejam sempre consistentes. Isso facilita e muito a sua utilização. Discuto mais sobre orientação a objetos no meu novo livro "Orientação a Objetos e SOLID para Ninjas: Projetando Classes Flexíveis", que publiquei pela Casa do Código. Também falo sobre isso no curso online do Alura sobre SOLID.

Veja outros artigos sobre Programação