1 de mai. de 2011

Primitive Obsession, Value object, Uniform Access Principle, Factory Method e Open/Close Principle

Salve,

Este tópico é para abordar um Code Smell (mau cheiro no código) chamado Primitive Obsession (obsessão primitiva). Este Code Smell é encontrado regularmente nas propriedades de classes e não é difícil identificá-lo. A refatoração comum é extrair uma classe, normalmente um Value Object (objeto de valor) imutável, criando um modelo mais rico, fazendo uso de princípios adequados de projeto orientado a objetos, como o UAP, Uniform Access Principle (Princípio do Acesso Uniforme) e OCP, Open/Close Principle (Princípio Aberto/Fechado).

Primitive Obsession

Imagine a situação onde seja necessário modelo um domínio de encomendas, tendo em mente que o valor cobrado no frente é relacionado com o peso da encomenda, pode ser? Naturalmente é comum modelarmos assim:

class Encomenda {
    int peso;
}

Obs.: deixei de lado Getter's e Setter's para favorecer a legibilidade do exemplo;

Mais tarde existirá a intenção de cobrar o frete por peso, por exemplo R$ 2,15 por Kilo. Mas o peso da encomenda usa qual unidade de medida? E se for necessário usar 0,5Kg? Pois então, o modelo deve ser refinado e talvez teremos um modelo assim:

class Encomenda {
    double pesoKilos;
}

Mais tarde é confirmado que a interface gráfica mostrará uma caixa de texto para entrar com o peso em kilos ou gramas. Se o usuário entrar com, por exemplo, 700 gramas, no evento será necessário converter estes 700 gramas para kilos, dividindo por 1000, para depois atribuir a encomenda. Então, após perceber a "gambi", talvez o modelo fique assim:

class Encomenda {
    double pesoKilos;
    int unidade; 
}

Como os outros colegas não entendem o que é essa tal de unidade em inteiro você decide então colocar um (desodorante) comentário:

class Encomenda {
    double pesoKilos;
    int unidade; // use 0 para kilo e 1 para gramas
}

Enfim, é muito comum querermos representar grupos de dados com tipos primitivos e isto é muito comum para quem vem de outras linguagens, a maioria procedural, as quais não proviam uma maneira uniforme e significativa de representar grupos ou estruturas de dados.

E como é a maneira OO de fazer? Ora, criando classes.

Aos que perguntam "bah, quantas classes vou criar?", digo: quantas forem necessárias. Programar em linguagens orientadas a objetos implica em criar classes (tipos), é fato.


Value Object (VO)

Objetos de valor são importantes para representar informações que não necessitam de uma identidade (exemplo, contrastando uma classe Compromisso com uma classe Data). Normalmente são imutáveis e fornecem métodos para tratar seu estado uniformemente. O caso do peso da encomenda é um forte candidato a ser um VO. Extraindo o peso para uma classe estaremos fazendo uma importante refatoração, para "curar" a nossa obsessão primitiva.

De início, poderíamos fazer algo mais ou menos assim:

class Encomenda {
    Peso peso;
}

class Peso {
    int valor;
}

Mas e agora, como vamos tratar esta representação de Peso. Bem, podemos fazer várias melhorias, como representar e converter facilmente unidades de medida, como Kilos, gramas e outros. O Kiko por exemplo, fez uma observação importante. Ele disse que poderíamos até mais tarde representar libras ou outra unidade estrangeira.

Primeiro temos de definir a unidade básica interna de armazenamento e depois fornecer acessores a este estado. Eu prefiro usar gramas, o que seria por exemplo, uma unidade mínima. A classe, agora completa para focar a solução, seria mais ou menos assim:

class Peso {
    private int gramas;

    public Peso(int gramas) {
        this.gramas = gramas;
    }

    public Peso(double kilos) {
        this.gramas =  (int) (1000 * kilos);
    }

    public int getGramas() {
        return gramas;
    }

    public double getKilos() {
        return gramas * 1000;
    }
    
}


Uniform Access Principle

Particularmente, ainda acho que esta classe deve ser refinada, mas ela já mostra a aplicação do Princípio do Acesso Uniforme. Este princípio baseia-se na premissa de que qualquer usuário da classe não precisa saber se a propriedade que está usando é um valor armazenado ou calculado. Se o método getKilos fosse converteEmKilos() estaríamos violando este princípio.

Do modo que está, o usuário da classe pode acessar o peso em gramas e em kilos uniformemente e transparentemente.

Eu não inclui métodos setGramas e setKilos, mas se o fizesse, o método setKilos converteria para gramas, como no construtor, e o usuário da classe ainda não teria conhecimento de como o peso internamente é armazenado (e nem é interessante). Incluir métodos set também implica em tornar o objeto mutável, a não ser que a cada set seja retornado um novo objeto Peso.


Factory Methods

Comentei que era possível fazer uma melhoria, penso que poderíamos substituir os construtores por métodos de fábrica (uma implementação simplificada do padrão de projeto Factory Method). Por quê? Imagine o caso:

Em uma instanciação da classe Peso como esta, como sabemos se é em gramas ou kilos?

Peso peso = new Peso(pesagem);

Pois é, apenas com essa linha não saberemos. Teremos que seguir a variável pesagem para saber se ela é do tipo int ou double para saber se a instância será em gramas ou kilos respectivamente.

Aplicando os métodos de fábrica, torna-se os construtores e privados e adiciona-se métodos estáticos com nomes amigáveis:

class Peso {
    private int gramas;

    private Peso(int gramas) { this.gramas = gramas; }
    
    private Peso(double kilos) { this.gramas =  (int) (1000 * kilos); }

    public static Peso comGramas(int gramas) { 
        return new Peso(gramas);
    }
    
    public static Peso comKilos(double kilos) {
        return new Peso(kilos);
    }
    
    public int getGramas() {
        return gramas;
    }

    public double getKilos() {
        return gramas / 1000;
    }
    
}

Então a chamada anterior ficaria assim:

Peso peso = Peso.comGramas(pesagem);

Desta maneira não é necessário seguir variáveis ou ler comentários para entender esta linha.



Open/Close Principle

Mas e se amanhã quisermos adicionar uma nova unidade de medida? Obviamente teremos que abrir a classe e implementar, a não ser que programemos com o princípio aberto/fechado em mente. O Princípio Aberto/Fechado diz que uma classe deve estar fechada para modificação mas deve estar aberta para extensão.

Existem vários modos de "abraçar" este princípio, um deles é usando o Design Pattern Strategy (padrão de projeto estratégia). Exemplo:


class Peso {
    private int gramas;

    private Peso(int gramas) { this.gramas = gramas; }

    private Peso(double kilos) { this.gramas =  (int) (1000 * kilos); }

    public static Peso comGramas(int gramas) {
        return new Peso(gramas);
    }

    public static Peso comKilos(double kilos) {
        return new Peso(kilos);
    }

    public static Peso comPeso(IConversorPeso conversor, double peso) {
        return new Peso(conversor.paraGramas(peso));
    }

    public int getGramas() {
        return gramas;
    }

    public double getKilos() {
        return gramas / 1000;
    }

    public double getPesoEm(IConversorPeso conversor) {
        return conversor.deGramas(gramas);
    }

    public interface IConversorPeso {

        public double deGramas(int gramas);
        public int paraGramas(double peso);

    }
    
}

Sabendo que 1 grama = 0,00220462262 libras, então poderíamos fazer isto:

class Libras implements IConversorPeso {

    public double deGramas(int gramas) { return gramas * 0.00220462262; }

    public int paraGramas(double peso) { return (int) (peso / 0.00220462262); }
}

Eu posso passar para Peso qualquer implementação de IConversorPeso e assim estender a funcionalidade da classe Peso sem alterá-la, aderindo ao Princípio Aberto/Fechado.


Caso de teste

Sabendo que 10 Kilogramas = 10000 gramas = 22,0462262 libras

Libras libras = new Libras();

System.out.println(libras.deGramas(10000));


Resultado Final

Obtendo o peso em Kilos instanciando com 500 libras:

Libras libras = new Libras();

Peso peso = Peso.comPeso(libras, 500);

System.out.println(peso.getKilos());


Conclusão

Minha implementação tem falhas de arredondamento (usa double para gramas atenuaria) mas acho que demonstra os temas. Espero que seja útil e que eu tenha me feito entender.

Abraços!