Mostrando postagens com marcador design patterns. Mostrar todas as postagens
Mostrando postagens com marcador design patterns. Mostrar todas as postagens

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!

19 de abr. de 2011

Lei de Demeter, Train Wrecks e Tell, Don't Ask

Salve,

O objetivo deste tópico é abordar um princípio de OOD (Object-Oriented Design - Projeto Orientado a Objetos) chamado Lei de Demeter e junto aproveitar para falar do anti-padrão Train Wreck e sua possível cura, o princípio "Tell,Don't Ask" (Diga, não Peça).

Lei de Demeter

A Lei de Demeter é fácil de implementar e de entender, talvez o difícil de compreender seja o porquê, em outras palavras, compreender a motivação. A Lei de Demeter também é conhecida como Princípio do Menor Conhecimento.

De forma resumida, a Lei diz o seguinte: "Uma classe ou método deve ter um conhecimento limitado de outras classes e métodos, devendo comunicar-se apenas com as classes imediatamente mais próximas".

Formalmente diz que um método M de uma classe C só deve chamar métodos de:
C
Um objeto criado por M
Um objeto passado como parâmetro para M
Um objeto dentro de uma instância de C
Recentemente, em um projeto de exemplo usado em aula, dá para ilustrar bem a aplicação e a violação da Lei de Demeter. Por exemplo, temos uma classe GestorJanela, que tem internamente uma instância de JDesktopPane.

class GestorJanela {
    // ...

    public JDesktopPane getAreaTrabalho() {
        return areaTrabalho;
    }

}

A partir de uma ação ou método dentro de uma Janela eu posso trazer uma Janela para frente usando o seguinte código:

GestorJanela.getInstance().getAreaTrabalho().getDesktopManager().activateFrame(janela);

Este código viola a Lei de Demeter porque além de conhecer o GestorJanela, ele também conhece a implementação de JDesktopPane ao usar getAreaTrabalho, DesktopManager e do método activateFrame. Se qualquer parte deste código mudar todos os lugares onde é chamado também deverão mudar. Por exemplo, se o GestorJanela passar a usar outro Painel ao invés do JDesktopPane para embutir as janelas, todos os chamadores quebrarão.


Train Wreck

Este mesmo código também é um exemplo do anti-padrão Train Wreck. Ele é encontrado quando existem métodos getIsso().getAquilo().getAquiloOutro().facaAlgo()

O que fazer então? Delegar! Quem deve subir a Janela é o GestorJanela (o nome não é por acaso). A implementação ficaria assim:

class GestorJanela {
    // ...

    public void trazerJanelaParaFrente(JInternalFrame janela) {
        areaTrabalho.getDesktopManager().activateFrame(janela);

        // ainda viola o princípio aqui, mas não temos como 
        // alterar o comportamento de JDesktopPane
        // para não expor o DesktopManager, fazendo parte do Swing 
        // (em outras palavras, a culpa não é nossa)
    }

}

E os chamadores fariam assim:

GestorJanela.getInstance().trazerJanelaParaFrente(janela);

Dá para melhorar este código removendo o Train Wreck:

GestorJanela gestor = GestorJanela.getInstance();

gestor.trazerJanelaParaFrente(janela);

Um colega perguntou: "Vale a pena adicionar mais código executando a mesma funcionalidade?". A resposta é: "Vale". Para manutenções e revisões de código, a implementação deve tão simples quanto possível, permitindo entendê-la com uma passada de olhos. Se são necessárias duas ou mais passadas, então talvez esteja na hora de refatorar e dividir as funcionalidades/responsabilidades.


Tell, Don't Ask

O código de exemplo também é um bom exemplo do princípio Tell, don't Ask. Não seria Tell, don't Ask se fosse implementado assim:

GestorJanela.getInstance().setJanelaTopo(janela);

Métodos get e set são úteis para expor propriedades, não para definir comportamento. Logo, em geral, o hipotético código:

loginController.getUsuarioLogado().getPermissao().setHabilitado(false);

é melhor escrito assim:

loginController.desabilitaPermissao();

Diga, não peça.

Bibliografia:
Martin, Robert C. Código Limpo.
Wikipédia. Law of Demeter.

28 de abr. de 2010

Design Pattern: Façade

Salve todos

Resolvi escrever este artigo depois de perceber alguns misconceptions, neste caso, sobre o design pattern Façade (é em francês com cedilha mesmo, significando fachada).


Definições da literatura

GOF Design Patterns by ... (ah, todo mundo sabe, o quarteto aquele):

Fornecer uma interface unificada para um conjunto de interfaces em um subsistema. Façade define uma interface de nível mais alto que torna o subsistema mais fácil de ser usado.



Core J2EE Patterns by Deepak Alur:

O Session Façade remove as interações de objeto de negócios subjacentes e proporciona uma camada de serviços que expõe apenas as interfaces necessárias, uniforme e de granulação grossa para os clientes.



Patterns Of Enterprise Application Architecture by Martin Fowler:

A Remote Facade is a coarse-grained facade [Gang of Four] over a web of fine-grained objects. None of the fine-grained objects have a remote interface, and the Remote Facade contains no domain logic. All the Remote Facade does is translate coarse-grained methods onto the underlying fine-grained objects.

Tradução livre: Um Façade Remoto é um façade de granulação grossa (coarse grained) acima de uma rede de objetos de granulação fina (fine grained). Nenhum dos objetos de granulação fina tem uma interface remota, e a interface remota não contém a lógica do domínio. Tudo que o Façade Remoto faz é traduzir métodos de granulação grossa para os objetos de granulação fina sob ele. 



Obs.: Existem várias discussões sobre a granulação de interfaces, como http://articles.techrepublic.com.com/5100-10878_11-5064520.html, sendo um aspecto importante do design. Não o li todo ainda, mas achei este muito interessante: http://www.ibm.com/developerworks/webservices/library/ws-soa-granularity/



Ilustrando (literalmente)

Acho que as ilustrações abaixo permitirão uma visão melhor:


usage of the facade pattern in UML

Parece claro que um Façade simplifica as chamadas utilizando uma interface uniforme e a frente de outros objetos que representam a lógica do domínio. Subentendido fica que os objetos do domínio compartilham informações entre si, e mesmo o façade pode "coreografar" (no mesmo sentido do SOA, mas não com a mesma implementação), obtendo a informação e entregando um resultado íntegro, construído de várias partes do negócio, para o cliente.

Estas mesmas partes, a granulação fina, podem depender de outras, penso afinal, se os objetos do meu domínio não trocam mensagens, o projeto não é orientado a objetos, é procedural*.

*Obs.: Há um code smell para isto http://c2.com/cgi/wiki?ShotgunSurgery e geralmente carece de grande refatoração, dificultando futuras manutenções.



Papel do Façade em uma aplicação distribuída

É custoso em vários sentidos um frontend utilizar chamadas remotas, especialmente síncronas. Não tenho explicação melhor que esta, do Fowler:

Within a single address space fine-grained interaction works well, but this happy state does not exist when you make calls between processes. Remote calls are much more expensive because there's a lot more to do: Data may have to be marshaled, security may need to be checked, packets may need to be routed through switches. If the two processes are running on machines on opposite sides of the globe, the speed of light may be a factor. The brutal truth is that any inter-process call is orders of magnitude more expensive than an in-process call - even if both processes are on the same machine. 


Tradução livre: Dentro de um simples espaço de endereço a interação com granulação fina funciona bem (i.e. mesma JVM, mesmo Host), mas este estadofeliz não existe quando você faz chamadas entre processos. Chamadas remotas são muito mais custosas por que há muito o que fazer: Dados podem precisar de serialização, talvez tenha-se que checar a  segurança, pacotes podem necessitar de roteamento através de switches, etc. Se os dois processos estão rodando em máquinas dispostas em lados opostos do globo, a velocidade da luz pode ser um fator significante. A verdade brutal é que qualquer chamada inter-processos é, em ordem de magnitude, mais custoso que uma chamada dentro do mesmo processo - mesmo se os dois processos estiverem na mesma máquina. 



Então ...

Patterns são assim mesmo, tem uma pitada de subjetividade e, obviamente, nem todo mundo tem o mesmo entendimento.

Se alguém quer acrescentar (ou subtrair) algo, fique a vontade, este tópico neste espaço é para todos juntos chegarmos a um consenso, com um pouco de cada, aproveitando a inteligência coletiva.

Márcio Torres