Saga do programador

21Jul/098

Nosso modelo de objetos não deve ser uma cópia do nosso modelo de dados

Esta semana aconteceu uma discussão bem interessante na lista interna da Caelum sobre Active Records. E no meio desta discussão surgiu o seguinte tema: "Nosso modelo de objetos não deve ser uma cópia do modelo de dados."

Acho que isso a maioria das pessoas já sabia, mas o que tenho visto por aí em projetos que trabalhei é exatamente o contrário: um modelo de objetos exatamente igual ao modelo de dados.

Por que ainda utilizamos uma abordagem assim?
Em minha opinião, o principal problema é que muitos desenvolvedores ainda não entenderam corretamente Orientação a Objetos. A prova disso são os sistemas escritos de forma totalmente procedural.

Tentarei exemplificar algumas destas diferenças. Este exemplo é uma adaptação de um problema real que eu presenciei. Vamos imaginar um sistema de cursos onde nós temos as tabelas:

Curso
id
nome
data Inicio
data Fim
idInstrutor

Instrutor
id
nome

Esta é uma simplificação, mas vai ajudar a explorar o problema.
A abordagem comum que eu tenho visto é ter um modelo de objetos que represente as tabelas do banco de dados, pois é necessário persistir as informações destes objetos. Para persistência, em java podemos utilizar o Hibernate/JPA, em Rails podemos utilizar o framework Active Record (que aliás, te induz a fazer exatamente o que eu descrevi acima) e em C# podemos utilizar o NHibernate.

Vamos ver algum código:

public class Curso{
    private int id;
    private String nome;
    private Date dataInicio;
    private Date dataFim;
    private Instrutor instrutor;
 
     ..gets e sets
}
 
public class Instrutor{
    private int id;
    private String nome;
 
    .. gets e sets
}

Em um primeiro momento, não há nada de errado com esta abordagem, certo? Apesar de nosso modelo de objetos refletir exatamente nossas tabelas no banco de dados, ele parece razoável.

Neste sistema, ao cadastrar um curso, temos que assegurar que não existe sobreposição de datas entre cursos com o mesmo instrutor. Exemplo:

Curso: Orientação a Objetos
Instrutor: Zé Bedeu
período: 18/07/2009 até 10/08/2009

Curso: Java
Instrutor: Zé Bedeu
período: 25/07/2009 até 20/08/2009

Reparem que estes dois cursos vão se sobrepor em algum momento. Até o dia 24/07/2009 o Zé Bedeu está ministrando o curso de OO e a partir de 25/07 ele também estará ministrando o curso de Java. Isso pode até fazer sentido, mas no nosso sistema de cursos isso não pode acontecer.

Então o que podemos fazer para evitar isso?
Podemos criar uma verificação para não deixar o mesmo instrutor ser alocado para outro curso se ele já tem aulas previstas dentro daquele período.

Para validar esta regra de negócio, poderíamos escrever um teste:

@Test
public void naoDeveSerPossivelTerDoisCursosParaUmInstrutorEmPeriodosSobrePostos(){
    // para simplicar, vamos utilizar strings para as datas
    // utilizando um buider.
    Curso oo = Curso.de("Orientacao a Objetos")
                               .comInstrutor("Ze Bedeu")
                               .comecandoEm("18/07/2009")
                               .ate("25/07/2009")
                               .Build();    
 
    Curso java = Curso.de("Java")
                               .comInstrutor("Ze Bedeu")
                               .comecandoEm("25/07/2009")
                               .ate("20/08/2009")
                               .Build();
 
     boolean existeSobrePosição =
                           DataUtils.existeSobrePosicaoDeDatas(
                                          oo.getDataInicio,
                                          oo.getDataFim,java.getDataInicio,
                                          java.getDataFim);
    assertFalse(existeSobrePosicao,
                   "não deve haver dois cursos com períodos sobrepostos");
}

Nosso teste faz a validação da regra de negócio, mas tem um problema nesta abordagem. Quem faz a verificação de datas é um objeto totalmente separado do nosso domínio. Reparem que o objeto DataUtils tem um método estatico que recebe 4 parâmetros. Podemos melhorar este exemplo atribuindo a responsabilidade de validar sobreposição para a classe curso, já que é ele quem tem as informações do período.

public class Curso{
    private int id;
    private String nome;
    private Date dataInicio;
    private Date dataFim;
    private Instrutor instrutor;
 
     ..gets e sets
 
    public boolean estahNoMesmoPeriodo(Curso outroCurso){
      return  DataUtils.existeSobrePosicaoDeDatas(
                                           this.getDataInicio(),
                                           this.getDataFim(),
                                           outroCurso.getDataInicio(),
                                           outroCurso.getDataFim());
 
    }
}

Vamos refatorar nosso teste:

@Test
public void naoDeveSerPossivelTerDoisCursosParaUmInstrutorEmPeriodosSobrePostos(){
    // para simplicar, vamos utilizar strings para as datas
    // utilizando um buider.
    Curso oo = Curso.de("Orientacao a Objetos")
                               .comInstrutor("Ze Bedeu")
                               .comecandoEm("18/07/2009")
                               .ate("25/07/2009")
                               .Build();
 
    Curso java = Curso.de("Java")
                               .comInstrutor("Ze Bedeu")
                               .comecandoEm("25/07/2009")
                               .ate("20/08/2009")
                               .Build();
 
    boolean existeSobrePosição = oo.estahNoMesmoPeriodo(java);
    assertFalse(existeSobrePosição,
                    "não deve haver dois cursos com períodos sobrepostos");
}

Começou a melhorar, repare que agora o próprio curso sabe validar se está no mesmo período de outro. Isso é começar delegar responsabilidades para os objetos de domínio, e evitar de se ter objetos fantoches. Nosso modelo de objetos deve ser rico e não anêmico.

Mas ainda temos algo pra melhorar. Existe um conceito que sempre falamos aqui até agora e ainda não foi explorado. Este conceito é o "Período". Mas o que é o Período?
O período nada mais é que uma representação do tempo transcorrido entre duas datas.

Um curso ocorre em um Período. Se vocês observarem a classe curso, temos data inicial e data final. O que temos na verdade é um período, e este conceito está implícito nesta classe. Em uma boa modelagem de objetos, nós temos que tornar os conceitos explícitos. Para isso podemos refatorar a classe curso e criar a classe período. Podemos também refatorar o método existeSobrePosicaoDeDatas da classe DataUtils para utilizar um período.

class Periodo{
    private Date dataInicio;
    private Date dataFim;
}
 
public class Curso{
    private int id;
    private String nome;
    private Periodo periodo;
    private Instrutor instrutor;
 
     ..gets e sets
 
    public boolean estahNoMesmoPeriodo(Curso outroCurso){
     return  DataUtils.existeSobrePosicaoDeDatas(
                                this.getPeriodo(),
                                outroCurso.getPeriodo());
 
    }
}

Agora a classe Curso na verdade tem um Período. Mas qual a vantagem de se ter o Período ao invés da data Inicio e data Fim? A vantagem é que período agora é uma classe, pode ter responsabilidades, pode tirar vantagens dos conceitos de OO. Além disso este período é altamente reutilizável por várias classes, qualquer uma que precisar do conceito de período pode utilizá-lo.

Mas ainda podemos melhorar mais. Na verdade quem tem a reponsabilidade de verificar se um período sobrepoe outro é o próprio período. A classe período é que sabe quem são as datas de início e de fim. Logo podemos fazer mais um refactoring atribuir a responsabilidade a classe período.

class Periodo{
    private Date dataInicio;
    private Date dataFim;
 
   public Periodo(Date dataInicio, Date DataFim){
      this.dataInicio = dataInicio;
      this.dataFim = dataFim;
   }
 
    public boolean estahNoMesmoPeriodo(Periodo outroPeriodo){
      /// código de validação
    }
 
    public int quantidadeDeDias(){
      // código
    }
    .. gets
}
 
public class Curso{
    private int id;
    private String nome;
    private Periodo periodo;
    private Instrutor instrutor;
 
     ..gets e sets
 
    public boolean estahNoMesmoPeriodo(Curso outroCurso){
     return  this.getPeriodo().estahNoMesmoPeriodo(
                outroCurso.getPeriodo());
    }
}

Após este refactoring, podemos observar algumas coisas:

  • A classe período tem um construtor que recebe 2 datas e não tem sets. Não faz sentido você instanciar um objeto período sem data de inicio e data de fim, pois ele (o objeto período)ficararia em um estado inválido.
  • A classe período agora é quem sabe validar sobreposição de datas.
  • A classe período agora também sabe dizer quantos dias tem no período, já que ela conhece o início e o fim.
  • A classe curso agora somente delega a responsabilidade para a classe período validar as datas.
  • E o mais importante, eliminamos a necessidade da classe DataUtils. Uma classe como essa é sintoma de um código procedural. A classe dataUtils tem somente procedimentos que lidam com dados separados. A grande sacada de Orientação a Objetos é colocar junto Dados + Operações sobre estes dados.

Agora voltando ao assunto do início do tópico. A classe período não tem necessidade de ser persistida, ela é apenas um Value Object. Os dados da classe período podem ser persistidos junto com a tabela curso. Mas isso não nos limita a ter uma classe que representa um conceito do domínio.
Com JPA vc pode fazer isso facilmente utilizando anotações @Embeddable.

A utilização de uma Fluent Interface nos testes foi de propósito, pois pretendo voltar ao assunto logo logo aqui no blog.

Valeu pessoal,

Luiz Costa

Sobre Luiz Costa

Desenvolvedor e blogueiro
Comentários (8) Trackbacks (0)
  1. Ótimo post Luiz. Também já passei por uma situação destas em uma empresa que trabalhei. Eu cuidei de uma rede social parecida com um orkut durante um tempo. Cada um usuario tinha um perfil, e podia ter varios relacionamentos de amizade com outros perfis, ou seja, existia um relacionamento muitos-para-muitos nesta tabela. Por isso no existia no BD uma tabela de Amizade. A tabela Amizade guardava os Id´s dos amigos “origem” e “destino” da amizade. Ateh aki tudo bem, o problema foi quando eu olhei o codigo da classe perfil. Nesta hora eu descobri que perfis possuiam uma lista de objetos Amizades e nao uma lista de amigos, ou seja, quem criou a aplicacao trouxe todo o modelo relacional para o mundo OO. E com isso introduziu no modelo um objeto sem sentido chamado Amizade. Na primeira oportunidade que tive refatorei todo este codigo, Tornei Amizade um Embendable, e fiz a classe perfil ter uma lista de “Perfis Amigos”.
    Realmente existe ainda muita confusao quanto a esta questao.

    Abracos

  2. Fala Luiz!
    Muito bom o post. Essa questão de delegar responsabilidades para os objetos corretos sempre foi e sempre será uma questão em que temos que tomar muito cuidado. Concordo com o Serginho quando ele diz que isso ainda gera muita confusão.
    Abs

  3. Belo post!
    Tenho desenvolvido em .NET, utilizado bastante NHibernate, e me policiando para aplicar orientação a objetos de verdade.
    Em rails, estou apenas iniciando.. não consigo visualizar uma maneira legal de fazer essa implementação com Active Record… Poderia ilustar?

    Obrigado!

  4. @Adriano
    Obrigado pelo comentário.

    Quando utilizamos Active Record, significa que os nossos objetos são responsáveis por se persistir. Isso por si só já te induz a ter um modelo de objetos igual ao seu modelo de dados. No caso do exemplo do post, período é uma classe que faz sentido apenas no modelo de objetos, não é necessário refletir isso no BD. A utilização de Active Record não nos limita a não ter uma classe como esta. Talvez algumas implementações de AR podem tornar isso um pouco mais complicado. Em uma implementação de AR sem nenhum framework, poderia ser feito algo assim, em c#:

    public class Curso
    {
    public Periodo periodo {get; set;}
    … outros atributos

    public bool Save()
    {
    persistencia.Save(this); // aqui faz-se o mapeamento necessário
    }
    }

    Já no ActiveRecord do Rails, eu não tenho muita certeza, mas acho que dá pra utilizar os métodos de callback (Before_save) para mudar o estado do objeto antes de salvar.
    Bem como disse antes, dá até pra fazer, mas acho que nestes casos vc precisa de algo que faça um mapeamento melhor para a base de dados. Talvez o ideal seja utilizar um DataMapper.

  5. Valeu pela resposta.
    Dei uma procurada por DataMapper e rails, e, pelo jeito, tem essa gem que implementa:

    http://datamapper.org

  6. otimo post..

  7. Graças a Deus eu não programo mais!!!!!


Deixar um comentário


Sem trackbacks