sexta-feira, 26 de novembro de 2010

Padrão de Projeto "Chain of Responsibility"

Imagine a situação em que uma grande empresa possui uma ampla rede de representação de seus produtos constituída, basicamente, por escritórios de venda espalhados em diversas cidades. Cada escritório possui um conjunto de vendedores que vão até os varejistas oferecer os produtos da empresa. Varejistas podem estar em áreas populosas, como supermercados, ou em áreas remotas, como os comércios que abastecem bairros rurais. Ao chegar no varejista, o vendedor utiliza um sistema fornecido pela empresa para registrar os pedidos de produtos.

Na situação descrita, o objetivo é registrar os pedidos na base de dados da central de distribuição da empresa. Porém, por problemas de comunicação com a central, o computador portátil do varejista pode não conseguir conectar com a base central para registrar os pedidos. Quando isso ocorrer, o sistema deve tentar salvá-los na base de dados do escritório. Quando a base do escritório também não estiver acessível, os pedidos deverão ser salvos na base de dados local, no próprio computador portátil do vendedor. Futuramente, de alguma forma fora do escopo de nossa discussão, todos os dados poderão ser carregados na base central.

Várias soluções podem ser dadas para o problema acima, entre elas a descrita no padrão de projeto Chain of Responsibility. Antes de resolver o problema utilizando o padrão de projeto, apresentarei a estrutura do padrão e explicarei seu funcionamento.


O diagrama acima apresenta uma classe abstrata chamada Responsabilidade que possui uma operação responsável por executar algum serviço. Podemos notar pela nota ancorada à classe que a opereção é bastante simples: se existir um sucessor, chama a mesma operação no sucessor, senão, gera um erro informando que a operação não pode ser executada. Notem que o sucessor é uma referência para a própria classe Responsabilidade. Através dessa auto-referência, é possível montar uma cadeia de instâncias da classe responsabilidade, como em uma lista encadeada. Obviamente, a classe Responsabilidade não pode ser instanciada diretamente, pois é uma classe abstrata. Para instanciá-la e, assim, montar a cadeia, é necessário especializá-la em classes concretas, representadas no diagrama como as classes ResponsabilidadeConcreta1 e ResponsabilidadeConcreta2.

As classes concretas que especializam a Responsabilidade definirão as possíveis implementações do serviço. Conforme podemos observar na nota, elas se comportam da seguinte forma: se for possível executar o serviço por aquela classe, executa-o, senão, chama a operação de mesmo nome na classe geral (Responsabilidade). Como já foi explicado, a classe geral, basicamente, chama a mesma operação no sucessor, que pode ser instância de uma outra classe concreta. Com isso, a operação é sempre repassada até chegar em uma instância que possa realmente executar o serviço ou até não haver mais sucessor.

Voltando ao problema do início do post, fica fácil perceber que o padrão se encaixa ao problema descrito. No diagrama de classes criaremos a classe abstrata PedidoDAO e as classes concretas, que representam os nós da cadeia, serão PedidoDAOCentral, PedidoDAOEscritorio e PedidoDAOLocal. A primeira será responsável por salvar o pedido na central, a segunda por salvar no escritório e a última por salvar na base de dados local do computador portátil do vendedor. Segue o diagrama de classes correspondente:


Ainda será necessário montar a cadeia na ordem correta. O GoF não descreve no padrão a responsabilidade de montar a cadeia na ordem correta. Porém, podemos fazer uma alteração no diagrama para incluir a classe DAOBuilder, que possui a operação responsável por construir a cadeia de responsabilidades.


Segue, abaixo, o código Java da classe DAOBuilder:

public class DAOBuilder {
 public static PedidoDAO buildPedidoDAO() {
  PedidoDAOCentral central = new PedidoDAOCentral();
  PedidoDAOEscritorio escritorio = new PedidoDAOEscritorio();
  PedidoDAOLocal local = new PedidoDAOLocal();
  central.setSucessor(escritorio);
  escritorio.setSucessor(local);
  return central;
 }
}

Assim, quem for utilizar a cadeia precisará conhecer apenas as classes DAOBuilder e PedidoDAO. Veja abaixo um exemplo de código de utilização em linguagem Java:

Pedido pedido = new Pedido();
// TODO: configura os dados do pedido.
PedidoDAO dao = DAOBuilder.buildPedidoDAO();
dao.inserirPedido(pedido);

Mais informações podem ser encontradas no livro "Design Patterns: Elements of Reusable Object-Oriented Software", de Erich Gamma, Richard Helm, Ralph Johnson e John Vlissides. Devido ao número de autores, o livro é também conhecido como GoF (Gang of Four - Gangue dos Quatro).

sábado, 20 de novembro de 2010

Padrão de Projeto "State"

O objetivo deste post é apresentar o padrão de projeto state, iniciando a série de padrões de projeto que serão publicados aqui. Afim de ser didático, inciarei com um exemplo e ao final apresentarei a estrutura do padrão.

Suponha que objetos de uma classe transitem por estados em uma máquina de estados. Por exemplo, imagine que uma conexão com um servidor possa transitar pelos estados desconectada, conectada e aguardando resposta, supondo que, uma vez que uma requisição é enviada, a conexão permanece no estado aguardando resposta até que a resposta seja recebida do servidor. Os serviços fornecidos pela classe que representa essa conexão são: conectar, desconectar, enviar requisição e receber resposta. Tal classe se assemelharia àquela representada abaixo.


É fácil perceber que o comportamento da classe deve mudar dependendo do estado em que sua instância se encontra. Por exemplo, caso o objeto esteja no estado conectado ou aguardando resposta e alguém chame a operação "conectar", um erro deve ser gerado. Mas, caso o mesmo método seja chamado quando o objeto estiver no estado desconectado, a conexão deverá ser estabelecida e o estado do objeto deve ser alterado para conectado. O diagrama abaixo apresenta a máquina de estados da conexão, as operações que causam mudança de estado e as operação que devem ser executadas sem erro em cada estado.


Uma solução possível para a mudança de comportamento entre os estados é, em cada método da classe "Conexao", escolher a implementação correta utilizando cláusulas if/else ou switch/case. Porém, ao surgir um novo estado, o desenvolvedor deve tomar o cuidado de incluir a opção correta em cada método, aumentando a possibilidade de inserção de erros na implementação.

O catálogo de padrões de projeto GoF (Gang of Four), cujo o livro irei referenciar no final deste post, propõe o padrão de projeto State para solucionar problemas como o descrito acima. Antes de apresentar a estrutura do padrão, trabalhei no exemplo da classe Conexao para explicar o funcionamento do padrão.

A solução proposta pelo GoF consiste em associar a classe "Conexao" a uma interface que represente o estado atual da conexão e, para cada estado, realizar a interface em classes específicas que realmente terão as implementações dos estados. A interface terá, basicamente, todas as operações da classe "Conexao", porém recebendo um parâmetro a mais que é a referência para aquela classe. Caso a implementação de uma operação pressuponha mudança de estado, a classe que representa o estado anterior terá a incumbência de alterar o estado da conexão para o próximo estado válido. O diagrama abaixo apresenta tal estrutura de classes.


Com isso, a implementação da classe "Conexao" se torna bastante simplificada, sem a necessidade de ter que saber o que fazer em qual estado. Basta inicializar o atributo state com o estado inicial correto e, para cada chamada de operação, repassar a chamada para o estado. Para o código que utilizará a estrutura, nada muda, pois a única classe que precisa ser conhecida ainda é a classe "Conexao" e as operações chamadas ainda serão as mesmas. Segue abaixo o código Java da classe "Conexao".

public class Conexao {

 private ConexaoState state;

 public Conexao() {
  state = new Desconectado();
 }

 public void conectar() {
  state.conectar(this);
 }

 public void desconectar() {
  state.desconectar(this);
 }

 public void enviarRequisicao(Requisicao requisicao) {
  state.enviarRequisicao(requisicao, this);
 }

 public Resposta receberResposta() {
  return state.receberResposta(this);
 }

 public void setState(ConexaoState state) {
  this.state = state;
 }

}

Para exemplificar, segue um esboço do código Java da classe que representa o estado conectado, alterando, quando necessário, o estado da conexão ao final do método.

public class Conectado implements ConexaoState {

 public void conectar(Conexao conexao) {
  throw new ConexaoException("Estado inválido");
 }

 public void desconectar(Conexao conexao) {
  // TODO: Desconectar do servidor.
  conexao.setState(new Desconectado());
 }

 public void enviarRequisicao(Requisicao requisicao, Conexao conexao) {
  // TODO: Enviar requisicao para o servidor.
  conexao.setState(new Conexao());
 }

 public Resposta receberResposta(Conexao conexao) {
  throw new ConexaoException("Estado inválido");
 }

}

A implementação acima está de acordo com o GoF, mas alterações são permitidas para otimizar o funcionamento para um problema específico. Por exemplo, se algumas operações terão o mesmo funcionamento em diversos estados, podemos substituir a interface que define o estado por uma classe abstrata com a implementação padrão de tais operações. Se quisermos evitar que os estados sejam instanciados no momento em que a classe for alterada para cada um deles, podemos usar uma estrutura de dados, como uma hashtable, onde consultaremos as instâncias já existentes dos estados, evitando consumo desnecessário de memória e execução desnecessária do processo de garbage collection.

Abaixo segue a estrutura do padrão de projeto:


Onde:

Contexto - classe contendo a interface pública de interesse dos clientes.
Estado - interface que define o comportamento de um estado em particular.
EstadoConcreto - classe que implementa um estado em particular.

Mais informações podem ser encontradas no livro "Design Patterns: Elements of Reusable Object-Oriented Software", de Erich Gamma, Richard Helm, Ralph Johnson e John Vlissides. Devido ao número de autores, o livro é também conhecido como GoF (Gang of Four - Gangue dos Quatro).

quinta-feira, 18 de novembro de 2010

Quantas fases tem o ICONIX?

ICONIX Process é um processo de desenvolvimento de software que vem evoluindo desde o início da década de 90, tendo Doug Rosenberg como principal idealizador. Muita coisa mudou no mundo da Engenharia de Software desde aquela época. A UML foi consolidada como linguagem de modelagem de sistemas orientados a objetos e o Processo Unificado (cuja mais famosa implementação é o RUP - Rational Unified Process), destaca-se como o principal processo de desenvolvimento de software a utilizar a UML. Aqueles que conhecem o Processo Unificado são capazes de encontrar vários de seus conceitos implementados no ICONIX, porém de forma muito mais simplificada. É intenção do ICONIX ser simples e direto, cuidando somente dos aspectos mais relevantes para construir a ponte entre os requisitos ditados pelo cliente e o software que será construído.

No final da década de 90 e início dos anos 2000, um novo grupo de Metologias de Desenvolvimento de Software começou a surgir e em 2001, com a publicação do Manifesto Ágil, tais metodologias passaram a ser chamadas de Metodologias Ágeis. O ICONIX também evoluiu afim de incorporar características que o permitissem ser adaptado a projetos ágeis, ou seja, em um ciclo de vida iterativo e incremental.

Devido às diversas mudanças ocorridas no cenário da Engenharia de Software refletidas gradualmente no ICONIX Process, é comum encontrar na Internet artigos que apresentam uma descrição desatualizada do processo. Com isso, resolvi publicar, em língua portuguesa, uma visão um pouco mais atualizada do ICONIX para que sirva de guia para estudos atuais. É óbvio dizer que, com a evolução do processo, o presente post também ficará desatualizado com o passar dos anos.

Quando utilizado em um ciclo de vida iterativo e incremental, cada iteração do projeto a ser desenvolvido e que servirá de atualização ao incremento anterior, conterá as fases descritas abaixo. Não é minha intenção detalhar as fases, mas apenas apresentar uma visão introdutória.

Requisitos


Como entrada para o processo, é necessário ter em mãos os requisitos funcionais do sistema a ser confeccionado. O ICONIX não define claramente como os requisitos devem ser levantados ou documentados. Algumas abordagens utilizam a criação dos esboços das interfaces gráficas do sistema (storyboards) ou da iteração em questão. Outras abordagens utilizam descrição textual dos requisitos. Proponho aqui, se é que ainda não foi proposto, utilizar as histórias de usuário, que podem ser obtidas através do jogo do planejamento do XP (eXtreme Programming). Gostaria de deixar registrado como opinião pessoal que não acredito que esboço de interface gráfica seja o melhor ponto de partida para um processo baseado em UML. Acredito que elas devam ser identificadas posteriormente, após a análise de robustez, quando identificaremos as classes de fronteira que, em geral, são as interfaces gráficas de usuário.

Com os requisitos em mãos, pode-se identificar o Modelo de Domínio, ou seja, um diagrama de classes sem atributos nem operações, apresentado as principais entidades que serão manipuladas pelo sistema.

Através dos requisitos também será possível identificar os casos de uso do sistema. O ICONIX apresenta uma forma diferenciada de identificar os casos de uso, mas deixarei para apresentá-la em um post futuro. O que julgo necessário destacar aqui é que os casos de uso constituirão a ponte entre os requisitos e a implementação do sistema, portanto, para o ICONIX, é extremamente importante para o processo descobrir os casos de uso de forma a representar com fidelidade os requisitos do sistema.

Milestone 1: Ao final da fase de requisitos, verifique que o texto dos casos de uso (fluxo de eventos) represente com fidelidade os requisitos ditados pelo cliente.

Quando utilizado o ciclo de vida iterativo e incremental, a fase de requisitos deverá mapear apenas os requisitos que serão implementados na iteração.

Análise e projeto preliminar


Para cada caso de uso identificado na fase de requisitos, devemos criar um diagrama de robustez baseado no texto do caso de uso (fluxo de eventos). Pode, também, surgir a necessidade de reescrever o texto do caso de uso. O diagrama de robustez não é um diagrama padrão da UML. No ICONIX, esse diagrama é utilizado para descobrir as classes de análise e detalhar, em alto nível, o funcionamento básico do caso de uso. Dedicaremos um post futuro para tratar do diagrama de robustez, fazendo uma comparação com o diagrama de comunicação com classes de análise usado no Processo Unificado.

Em paralelo ao diagrama de robustez, devemos atualizar o modelo de domínio e adicionar os atributos às entidades identificadas. O modelo de domínio resultante se assemelhará tanto com um modelo de dados que acho válido dizer que a partir deste ponto estaremos prontos para gerar a base de dados do sistema. Vale ressaltar que não encontrei nenhuma referência à geração da base de dados neste ponto nos textos que li, mas a lógica me leva a concluir que este é o momento.

Conforme mencionei anteriormente, ainda com base na minha opinião pessoal, as classes de fronteira do diagrama de robustez nos darão uma ideia de quais são as principais telas da interface gráfica do sistema. Logo, acredito que este também é o momento de se criar o esboço da interface gráfica (storyboards).

Milestone 2: Ao final da fase de análise e projeto preliminar, verifique a consistência dos artefatos gerados, ou seja, do projeto preliminar. Se necessário, realimente a fase de requisitos com eventuais alterações identificadas aqui.

Projeto detalhado


Para cada caso de uso, devemos criar um diagrama de sequência de forma a detalhar a futura implementação do caso de uso. O diagrama de sequência deverá conter as classes que realmente serão implementadas, ou seja, as classes de projeto. As mensagens enviadas entre os objetos deverão corresponder aos métodos que realmente serão implementados.

Atualize o modelo de domínio com as operações encontradas no diagrama de sequência. Inclua também as novas classes de projeto identificadas, criando o diagrama de classes final do seu sistema (ou da iteração).

Milestone 3: Reveja o projeto do sistema (ou da iteração) de forma crítica, fazendo com que ele fique aderente à tecnologia que será utilizada para a implementação.

Implementação


Inicialmente, o ICONIX não trazia detalhes sobre a implementação. Se pensarmos bem, qual processo detalha a implementação? Porém, na última edição do livro intitulado "Use Case Driven Object Modeling with UML: Theory an Practice", Doug Rosenberg e Matt Stephens, é possível encontrar um guia detalhado de cuidados que devemos tomar ao implementar um sistema, tendo claramente uma grande influência das metodologias ágeis. Particularmente, baseado na publicações mais recentes, não considero um erro dizer que a implementação faz parte do ICONIX. Também não considero um erro dizer que não faz. Apresento abaixo uma figura extraída do site do ICONIX Process sugerindo a implementação como uma fase do ICONIX.



Para mais informações sobre o processo ICONIX e também sobre o livro de Doug Rosenberg e Matt Stephens, acesse o site: http://www.iconixprocess.com

segunda-feira, 15 de novembro de 2010

Como representar Class Templates (ou Classes Genéricas) em UML?

Templates são velhos conhecidos dos programadores C++ e ganharam popularidade entre os programadores Java com a introdução dos Generics na versão 5 da linguagem Java. De forma superficial, pode-se dizer que os Generics da linguagem Java e os Templates do C++ são nomes diferentes para o mesmo conceito. Os arquitetos da linguagem Java foram muito felizes na escolha do nome, pois os Class Templates nada mais são do que uma forma de representar classes de uma forma genérica.

A questão é: como representar as classes genéricas em UML? Assim como em C++, a UML também utiliza o nome Template, conforme consta na especificação. A figura a seguir mostrar parcialmente como representar em UML a interface java.util.Collection da API Java.


Em linguagem Java, o código fica:

public interface Collection<E> {
 public boolean add(E e);
 public Iterator<E> iterator();
}

A seguir, temos um exemplo de criação de um array de String como uma especialização da classe java.util.ArrayList, que é uma classe genérica da API Java.


Antes de apresentar o código Java, é importante dizer que a classe StringArray não será exatamente uma subclasse da classe genérica ArrayList, mas sim uma implmentação da classe genérica, ligando o tipo String ao tipo genérico E do ArrayList.

O código Java, então, fica como abaixo:

public class StringArray extends ArrayList<String> {
}

Note também que, no código Java, a classe StringArray não está especializando a classe genérica ArrayList<E>, e sim a classe ArrayList<String>, tendo o tipo String já ligado ao tipo genérico E. Uma outra opção para se escrever o tipo ligado ArrayList<String> é apresentado no diagrama abaixo:


Para mais informações sobre Templates na UML, faça download da especificação que pode ser encontrada no site: http://www.omg.org/uml