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

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 Positions, que contém um curso e a porcentagem de andamento desse curso.

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.

8 Comentários

  1. Bonejah 03/06/2015 at 14:03 #

    Muito bom o post!

  2. Cleyton Santos 11/06/2015 at 09:29 #

    Sempre bom dar uma passada no blog, parabéns pelo Post.

  3. Caio Duarte 11/06/2015 at 18:48 #

    @Maurício, parabéns pelo post. A título de curiosidade, o código da solução (o builder criado) é aberto? Fiquei curioso em ver como foi montado.

    Abraços

  4. Raphael 16/06/2015 at 14:11 #

    O problema do Builder é se existir alguma condição que precise ser definida uma ordem de chamadas na construção ou então, como garantir que quando o programador chamar o .build() ao final, ele realmente chamou todos os métodos necessários no Builder?

    Ou seja, como garantir que o Builder é consistente?

  5. Maurício Aniche 16/06/2015 at 14:32 #

    Oi Caio,

    A implementação do builder não tem segredo. É bem padrão mesmo!

    Oi Rapha,

    Vc pode fazer seus builders voltarem interfaces que não tenham o método “build()”, até que, quando tudo estiver preenchido, você devolve uma com o “build()”. Dá trabalho, mas dá pra fazer.

    Um abraço!

  6. Rogerio J. Gentil 17/06/2015 at 15:12 #

    Excelente publicação! Objetos inconsistente são muito frequentes em projetos que tenho visto e trabalhado…

  7. Raphael 19/06/2015 at 12:09 #

    POis é ANiche

    este cara aqui fez: http://www.jayway.com/2012/02/07/builder-pattern-with-a-twist/

    vc acha que o o trade off vale a pena?

  8. Elio Capelati 14/07/2015 at 15:23 #

    Essa semana conheci essa lib que ajuda a criar objetos imutáveis, e seus builders: http://immutables.github.io/
    Ela acaba fazendo muito mais que isso portanto vale a pena dar uma olhada, e ainda não requer nenhuma dependência de runtime.

Deixe uma resposta