Princípios, Padrões e Práticas para um Design Ágil – Parte 1

O artigo apresenta conceitos importantes relacionados à arquitetura de software, destacando desde a sua concepção até a entrega do software, e abordando os tipos de arquitetura existentes, com ênfase na arquitetura distribuída e na arquitetura em camadas.

Por que eu devo ler este artigo:

Serve para arquitetos, analistas e desenvolvedores que desejam conhecer mais sobre arquitetura de software e sobre como fazer uma separação de responsabilidades efetiva na criação das camadas físicas e lógicas do sistema.

O artigo apresenta conceitos importantes relacionados à arquitetura de software, destacando desde a sua concepção até a entrega do software, e abordando os tipos de arquitetura existentes, com ênfase na arquitetura distribuída e na arquitetura em camadas.

O tema é útil para todo o desenvolvedor que deseja aprimorar suas técnicas de programação (independente do framework escolhido) e para todo arquiteto que deseja conhecer mais sobre tipos de arquitetura e como aplicar uma arquitetura modular, visando boas práticas de design OO.

Princípios, padrões e práticas para um design ágil são importantes para construir um software de qualidade. Ao iniciar o desenvolvimento de um novo sistema, muitas vezes ficamos em dúvida sobre qual o estilo de arquitetura utilizar. Para auxiliar nesta decisão, o artigo comenta sobre várias opções de arquitetura, dando ênfase à arquitetura distribuída e suas camadas lógicas.

Com a grande demanda de profissionais por parte das empresas, muitos deles acabam sendo contratados sem ter o devido conhecimento sobre boas práticas de desenvolvimento de software, infraestrutura de desenvolvimento, protocolos de comunicação, sistemas gerenciadores de banco de dados e muitas vezes até conhecimento básico sobre sistemas operacionais. Além disso, dentro das empresas existe uma demanda cada vez maior por projetos de software de alta complexidade, com prazos de entrega na maioria das vezes fora da realidade. O que faz com que muitos projetos sejam entregues sem testes, com falhas de implementação, qualidade duvidosa e muitas vezes sem atender ao negócio do cliente.

Como consequência, após implantado, o sistema passa a apresentar sérios problemas como degradação da performance, perda de dados, cálculos incorretos, entre outros problemas, trazendo grandes prejuízos à empresa e fazendo com que os usuários percam a confiança no sistema e por fim os próprios desenvolvedores.

Direcionado para este cenário, o presente artigo visa demonstrar boas práticas de desenvolvimento de software e explicar os princípios fundamentais para se criar um bom design. Veremos também a aplicação de alguns padrões de projeto pouco difundidos, mas que são de extrema importância no dia a dia do desenvolvedor.

Estilos de Arquitetura

Quando vamos planejar o desenvolvimento de um software, geralmente um dos primeiros itens que o arquiteto de software começa a analisar é qual o melhor estilo de arquitetura que se aplica ao contexto do projeto. Neste ponto ele leva em consideração diversos fatores, como os requisitos do software, custo, infraestrutura e conhecimento técnico da equipe.

Entender o estilo arquitetural adotado para o desenvolvimento traz diversos benefícios para os desenvolvedores e para a empresa como um todo, entre eles: fornecer uma linguagem comum entre os desenvolvedores e os demais envolvidos no projeto. Por exemplo, quando o estilo de arquitetura é SOA, os desenvolvedores podem até conversar com pessoas de outras áreas, que não possuem conhecimento técnico (como detalhes da linguagem de programação, servidores de aplicação, etc.), sobre o método envolvido na automação dos processos de negócio, que as mesmas entenderão sobre o que é um serviço, governança e, dependendo do usuário, até termos mais específicos, como escalabilidade.

A Tabela 1 apresenta um resumo dos principais estilos de arquitetura.

Estilo de Arquitetura

Descrição

Client-Server

Também conhecida como arquitetura de duas camadas. Este tipo de arquitetura descreve a interação entre um ou mais clientes e um servidor, através de uma conexão de rede. A aplicação mais comum para esse tipo de arquitetura é um Banco de Dados no lado do servidor com a lógica da aplicação representada em Stored Procedures, e o lado do Cliente representado por aplicações conhecidas como Fat-Clients (Clientes Pesados), que muitas vezes contêm a lógica de negócio embutida no front-end. Como exemplo, podemos citar aplicações desenvolvidas com Delphi, Visual Basic, Oracle Forms, entre outros.

Arquitetura baseada em Componentes

Quando utilizamos uma arquitetura baseada em componentes, decompomos o design da aplicação em componentes lógicos com um fraco acoplamento, de forma que cada um deles seja individual e reutilizável, com uma localização transparente e interface de comunicação bem definida. Uma das grandes vantagens desse tipo de arquitetura é que ela promove a reusabilidade dos componentes e facilita a manutenção da aplicação.

Domain Driven Design

De acordo com o próprio criador, Eric Evans, DDD não é uma tecnologia e nem uma metodologia. DDD é um estilo de arquitetura orientado a objetos, focado em modelar o domínio do negócio e a lógica do domínio com o uso de técnicas, práticas e padrões de projeto.

Arquitetura em Camadas

Esse é o estilo de arquitetura mais conhecido e mais utilizado para o desenvolvimento de aplicações. Ele permite a separação dos módulos do sistema em camadas (layers) diferentes.

As principais vantagens desse estilo são a facilidade de manutenção, o aumento da extensibilidade, reusabilidade e escalabilidade.

3-Tiers/N-Tiers

Esse estilo segrega as funcionalidades em segmentos separados de maneira similar ao estilo da arquitetura em camadas, mas com cada segmento alocado em camadas físicas (tiers) separadas, conforme veremos em detalhes no tópico “Arquitetura Distribuída”.

Ele é ideal quando o projeto demanda escalabilidade. Para isso, as camadas lógicas (layers) da aplicação são alocadas em máquinas diferentes.

Geralmente, ao mapear as camadas lógicas (layers) para as físicas (tiers), podemos criar um cluster ou um farm na mesma camada física para aumentar a performance e a confiabilidade da aplicação.

Arquitetura Orientada a Serviços (SOA)

SOA já deixou de ser apenas uma palavra no meio de TI para se tornar um modelo consagrado adotado em muitas empresas. Uma arquitetura orientada a serviços nada mais é do que uma aplicação que expõe e consome as funcionalidades do sistema como serviços por meio de contratos e mensagens.

Algumas das características de uma Arquitetura Orientada a Serviços são: a independência da Plataforma, comunicação baseada em serviços entre os componentes de software, integração de múltiplas funcionalidades em um único componente de interface do usuário e a exposição dos serviços por meio de repositórios ou catálogos.

Arquitetura Orientada a Objetos

Ao aplicar esse estilo, o sistema é dividido em objetos individuais, reutilizáveis e autossuficientes. Cada objeto contém os dados e o comportamento pelos quais é responsável.

Uma arquitetura orientada a objetos geralmente busca espelhar objetos do mundo real de modo a tornar simples a modelagem da aplicação.

Tabela 1. Estilos de Arquitetura.

Cluster: É um conjunto de computadores interligados que trabalham em conjunto como se fosse um único computador. Estes computadores são utilizados para suportar aplicações que têm necessidade de alta disponibilidade e alta capacidade de processamento.

Farm: Um Servidor Farm, também conhecido como Data Center, é um conjunto de servidores mantido (geralmente) por uma empresa para atender as necessidades computacionais da corporação, como o processamento de grandes volumes de informação, atender aos sistemas corporativos e prover servidores de contingência no caso de um problema no computador principal.

Repositório: Um Repositório é um local onde todos os clientes de um domínio específico publicam seus serviços, de maneira que todos os usuários passam a conhecer os serviços disponíveis e como acessá-los. Para tanto, um repositório deve ter uma interface bem definida para publicação dos serviços, funções para localização, controle e maneira uniforme de acesso.

Uma vez que o estilo de arquitetura é definido, o passo seguinte é definir como as funcionalidades do sistema serão divididas, de forma a manter em conjunto os componentes relacionados (alta coesão), e fazer com que estes possuam o mínimo de conhecimento sobre os outros componentes (baixo acoplamento). Para alcançar o baixo acoplamento e uma alta coesão é importante entender o conceito de Separação de Responsabilidades.

Separação de Responsabilidades (Separation of Concerns)

O termo Separação de Responsabilidades foi criado pelo pai do algoritmo de caminho mínimo, o cientista Edsger W. Dijkstra em 1974, e tem como objetivo dividir a aplicação em funcionalidades distintas com a menor similaridade possível entre elas. Para aplicar com sucesso este princípio, o mais importante é minimizar os pontos de interação para obter alta coesão e baixo acoplamento entre os componentes do sistema. Para tanto, é recomendado primeiro dividir suas responsabilidades e organizá-las em elementos bem definidos, sem repetição de código ou de funcionalidade.

O tipo de separação de conceito mais difundido é o da separação horizontal, onde dividimos a aplicação em camadas lógicas de funcionalidades, como por exemplo, o protocolo TCP/IP (modelo OSI), que é separado pelas camadas de aplicação, transporte, rede, enlace e física. Neste modelo, cada camada tem sua própria responsabilidade e não precisa conhecer as camadas adjacentes. Voltando ao nosso mundo, em uma aplicação Java EE, as camadas mais conhecidas são a de Apresentação, Serviço, Domínio e Infraestrutura, como ilustra a Figura 1.

Figura 1. Separação Horizontal de Conceitos (Camadas lógicas de uma aplicação).

Podemos também aplicar o tipo de separação vertical, onde dividimos a aplicação em módulos ou funcionalidades que são relacionadas a um subsistema ou grupo de operações dentro de um sistema. Veja um exemplo na Figura 2.

Figura 2. Separação Vertical de Conceitos.

Separar as funcionalidades de uma aplicação em módulos deixa claro as responsabilidades e dependências de cada funcionalidade, o que ajuda na execução dos testes e na manutenção do software. Na Figura 2 fazemos um agrupamento por módulos; neste caso a separação foi feita pelos módulos de RH, Contábil e Marketing.

Além disso, podemos utilizar o conceito de separação vertical em conjunto com o conceito de separação horizontal. Veja um exemplo na Figura 3.

Figura 3. Aplicação da Separação de Conceitos Vertical e Horizontal.

Um papel importante da separação de conceitos é o da separação de comportamento, que envolve a divisão dos processos do sistema em unidades lógicas de código reutilizáveis e gerenciáveis. Ao organizar o comportamento, alcançamos os seguintes benefícios:

Como vimos, é importante entender a separação de conceitos pois ao modelarmos uma aplicação devemos ter conhecimento das responsabilidades de cada um de seus componentes. Além disso, devemos ter uma ideia de como será feita esta separação e como será feita a distribuição destes componentes na arquitetura disponível pelo cliente. Para tanto, é preciso levar em consideração outros fatores, como infraestrutura e recursos disponíveis, conforme veremos a seguir.

Arquitetura Distribuída

Em uma arquitetura distribuída, uma aplicação é dividida em partes menores que executam simultaneamente em computadores diferentes. Estas partes são chamadas de Tiers, e se comunicam umas com as outras geralmente através de uma rede corporativa (LAN), utilizando protocolos baseados em TCP/IP ou UDP.

Na Figura 4 destacamos uma arquitetura distribuída em 3 camadas de uma típica aplicação Java EE, arquitetura essa que se tornou padrão em muitas empresas, sendo formada pelas camadas físicas: Thin Client (Client Tier); Application Server (Application Tier), neste exemplo representado por um servidor de aplicações como o JBoss, e o Database Server (Infra Tier), representado pelos servidores de banco de dados.

Figura 4. Arquitetura de uma aplicação 3-Tier.

Para muitos desenvolvedores, existe uma confusão com os termos tier e layer, pois a tradução de ambas as palavras é a mesma: camada. A diferença é que quando falamos em tier, estamos nos referindo à camada física da aplicação, como servidores, dispositivos móveis, desktops, JVMs, CPUs, etc. Enquanto que ao falarmos em layers, estamos referenciando as camadas lógicas da aplicação, camadas que residem em uma ou mais camadas físicas, não existindo uma regra definida, podendo uma camada lógica estar distribuída em diversas camadas físicas. Por exemplo, uma aplicação web em cluster, cuja aplicação é distribuída em mais de um servidor (tier) para prover alta disponibilidade do sistema.

Cada um dos tiers fornece um conjunto independente de serviços que podem ser consumidos pela camada cliente ou por uma camada de conexão. Além disso, podemos dividir uma camada física (tier) em várias camadas lógicas (layers) para fornecer funções de nível mais granular. Na Figura 4, por exemplo, separamos a camada do Application Server em quatro camadas lógicas: Apresentação, Serviço, Domínio e Infraestrutura. Este modelo pode variar de empresa para empresa, com layers e tiers adicionais. Por exemplo: poderíamos separar a camada de apresentação da nossa aplicação colocando-a em um servidor web à parte (como o Tomcat); neste caso, adicionaríamos mais uma camada física (tier) e nosso cenário passaria a ser o de uma arquitetura N-Tier, conforme ilustra a Figura 5.

Figura 5. Arquitetura de uma aplicação N-Tier.

As camadas (layers) são responsáveis pelo processamento do sistema em si, e cada uma delas agrupa todos os componentes (classes, pacotes) que possuem funcionalidades em comum ou relação entre si.

Esta separação nos dá vários benefícios, entre eles, promove a coesão entre os componentes relacionados e evita o acoplamento com componentes de outras camadas, pois o acesso às outras camadas é realizado de maneira controlada. Além disso, promove a reusabilidade dos componentes (“teoricamente” poderíamos trocar uma camada por outra), reduz a dependência com componentes de outras camadas e a complexidade do sistema e, por fim, facilita a identificação de problemas.

Conforme vimos na Figura 4, a camada do Application Server é o local onde fica hospedado nossa aplicação. Para o exemplo ilustrado, separamos nossa aplicação em quatro camadas lógicas, são elas: camada de apresentação, camada de serviços, camada de domínio e camada de infraestrutura.

Camada de Apresentação (Presentation Layer)

A Camada de Apresentação representa a interface de acesso do sistema e é responsável por apresentar as informações ao usuário, sejam elas em uma página HTML ou em um dispositivo móvel. Esta camada é responsável também por interpretar as requisições, como cliques de mouse, ações do teclado, requisições HTTP;, originadas por um usuário ou por interações de outros sistemas, através de serviços ou APIs.

Ao criar o design da camada de apresentação existem boas práticas que devemos considerar para assegurar que o modelo que estamos definindo atenda aos requisitos do cliente, como:

Core: Esta sigla é uma referência ao livro Core J2EE Patterns: Best Practices and Design Strategies, famoso catálogo de referências da Sun para arquiteturas com tecnologias Java EE.

Padrão de Projeto: Front Controller

Documentado no livro Core J2EE Patterns (ver Livros), este padrão foi proposto para a camada de apresentação, e devemos utilizá-lo “quando queremos criar um ponto de acesso centralizado para tratar as requisições originadas desta camada”, conforme demonstra a Figura Q1.

Figura Q1. Diagrama de Classes do Front Controller.

O padrão Front Controller trás diversos benefícios, entre eles: evitar a duplicação da lógica de controle; auxiliar a separação da lógica de processamento do sistema da camada de apresentação; e ajudar a controlar os pontos de acesso do sistema.

Além disso, o Front Controller ajuda a reduzir a quantidade de código com lógica embutida diretamente na camada de apresentação, diminuindo, por exemplo, o volume de código scriptlet em uma página JSP.

Camada de Serviços (Service Layer)

No livro Patterns of Enterprise Application Architecture (referenciado como [PEAA]), Martin Fowler define a camada de serviços como sendo a fronteira do sistema que fornece um conjunto de operações disponíveis para a camada de apresentação.

Esta camada encapsula a lógica de negócio da aplicação (Business Layer), controla as transações e coordena o retorno das operações de acordo com sua implementação.

É nessa camada que é definido todo o trabalho que o software deve realizar e onde são direcionadas as chamadas para o modelo de domínio. Muitos desenvolvedores acabam colocando toda a regra de negócio nesta camada por meio de Scripts de Transação (Transaction Scripts [PEAA]; para mais detalhes, veja o quadro “Padrão de Projeto: Transaction Script”) e acabam perdendo as vantagens que um Modelo de Domínio (Domain Model [PEAA]) pode oferecer, criando o chamado Modelo Anêmico.

Padrão de Projeto: Transaction Script

Documentado por Martin Fowler, este padrão tem o objetivo de organizar a lógica de negócio da aplicação em procedimentos (procedures). Um Transaction Script deve conter todas as regras necessárias para satisfazer uma transação, de forma que não haja dependência de um Transaction Script com outros objetos.

É um padrão útil para pequenas aplicações por seguir as regras de programação procedural. Um Transaction Script organiza a lógica do sistema em procedures, podendo, por exemplo, fazer chamadas diretas ao banco de dados.

Seguindo este padrão, se nós desenvolvermos um sistema de pagamento e precisarmos implementar uma funcionalidade para pagamento via cartão de crédito, então, toda a lógica de verificação de crédito, como o saldo disponível, validade do cartão e cálculo das taxas seria implementada em um único procedimento.

Uma maneira de aplicar Transaction Scripts é utilizando o padrão de projeto Command [GoF], onde incluímos toda sua lógica em classes separadas, como ilustra a Figura Q2, que para cada tipo de operação existe um Command relacionado.

Figura Q2. Aplicando Transaction Script utilizando o padrão Command.

A vantagem desse padrão é a sua simplicidade, pois toda a lógica referente a uma transação está em um único lugar. Quanto às desvantagens, podemos citar a baixa reusabilidade do código, o alto índice de código repetido (violando o princípio DRY, que veremos no próximo artigo) e obviamente o fato de ser voltado à programação procedural.

Para evitar o modelo anêmico, a camada de serviço deve ser uma camada fina e não possuir regras de negócio, somente coordenar tarefas e delegar o trabalho para os objetos de domínio da camada abaixo.

Geralmente esta camada inclui os seguintes tipos de serviços:

Modelo Anêmico

Este é um anti-pattern documentado por Martin Fowler, e descreve a prática de se implementar a lógica do negócio fora dos objetos de domínio (domain model). Geralmente reconhecemos um modelo anêmico quando as classes do domínio possuem apenas os métodos getters e setters.

Camada de Domínio (Domain Layer)

Também conhecida como Camada de Negócio, essa camada é uma das mais importantes do sistema, pois é onde se concentra as informações sobre o domínio do negócio. Nela os objetos de domínio encapsulam o estado e o comportamento das entidades de negócio, como por exemplo, lógica para atualização de estoque em um sistema logístico.

Para representar os objetos do domínio, podemos aplicar o padrão Domain Model [PEAA] utilizando técnicas de design e análise orientada a objetos (OOAD). O modelo de domínio é formado por classes que correspondem ao ambiente do negócio que o software propõe auxiliar, tornando-o fácil de compreender.

A lógica de negócio que é implementada utilizando o padrão Domain Model é estruturada de maneira muito diferente de um design tradicional com EJBs. Ao invés da lógica de negócio ser concentrada em poucas, mas grandes classes, um modelo de domínio consiste em várias classes pequenas que possui tanto estado (através de atributos) quanto comportamento (através dos métodos). Para mais informações, veja o quadro “Padrão de Projeto: Domain Model”.

Nesta camada podemos ainda gerenciar o estado dos objetos de negócio (sessão), caso existam múltiplas requisições do usuário para realizar uma tarefa. Conforme mencionamos anteriormente, aqui residem as regras de negócio da aplicação, portanto, elas devem ser isoladas das outras camadas da aplicação; se possível, ser independente de frameworks utilizados em outras camadas, como EJB, JSP/JSF, Spring, Hibernate, dentre outros.

Padrão de Projeto: Domain Model

Este padrão de projeto foi documentado por Martin Fowler [PEAA], mas é discutido desde o surgimento da programação orientada a objetos. De acordo com a definição de Fowler, um Domain Model “é um modelo de objetos do domínio que incorpora tanto comportamento quanto dados”. Ou seja, é a parte principal do sistema, onde estarão as regras de negócio e as entidades do sistema.

Um Domain Model é uma representação visual dos objetos do “mundo real” através das classes do modelo, de acordo com tipo de negócio que elas representam. Para uma melhor representação do negócio, as classes do modelo de domínio não devem possuir acoplamento com classes ou lógica de infraestrutura, como por exemplo, lógica de segurança de acesso e logging. Na Figura Q3 é apresentado um exemplo de um modelo de domínio simples de um sistema de Ordens de Serviço para uma mecânica automotiva.

Figura Q3. Domain Model inicial de um sistema de garantias.

Camada de Infraestrutura (Infrastructure Layer)

Esta camada fornece meios técnicos para a aplicação suportar as camadas superiores, como o envio de mensagens, componentes para persistência de objetos da camada de domínio, desenho de componentes gráficos para a camada de apresentação e assim por diante.

Aqui centralizamos os componentes de acesso aos dados e outros não relacionados à lógica do negócio para tornar mais fácil a manutenção e a configuração da aplicação. Entre estes componentes, podemos utilizar componentes JMS para mensageria, frameworks Objeto/Relacional (ORM) para persistência dos objetos do domínio, sessões de JavaMail, Barramentos ESB, entre outros. Ou ainda, poderíamos criar componentes que podem servir de utilidade para as demais camadas do sistema, como segurança, comunicação e gerenciamento.

Geralmente componentes como estes podem vir a ser aplicados em mais de uma camada da aplicação, e quando isso acontece dizemos que o mesmo possui Responsabilidades Transversais.

Responsabilidades Transversais (Crosscutting Concerns)

Responsabilidades Transversais ou Ortogonais representam áreas que não estão relacionadas diretamente com uma ou mais camadas da aplicação. Na Figura 4 elas são representadas pelos itens de segurança, comunicação e gerenciamento, e contemplam as camadas de apresentação, serviço, domínio e infraestrutura.

O código referente a estas responsabilidades deve estar o mais abstraído possível da lógica de negócio da aplicação, ou seja, desacoplado do código relacionado às regras de negócio, pois misturar o código que implementa este tipo de função com o código do domínio da aplicação geralmente leva a um design que é difícil de manter e reutilizar.

Para auxiliar na gestão de um cenário como este, considere utilizar frameworks e técnicas como a programação orientada a aspectos (AOP), onde ao invés de efetuar chamadas diretamente a funções, são utilizados metadados para efetuar a inclusão de código (relacionado a estas responsabilidades) durante o tempo de execução.

Para qualquer estilo de arquitetura que o software siga, é preciso um mínimo de planejamento para a construção da aplicação, e isto envolve a criação do design da aplicação. Por mais simples que o software seja, ou que tenha sido construído sem planejamento, ou documentação, ou testes, ele tem um design por trás.

Conclusão

Na primeira parte deste artigo, abordamos conceitos importantes sobre arquitetura de software, apresentando de maneira sucinta os estilos de arquitetura existentes e seus benefícios. Após a descrição, detalhamos o conceito de arquitetura distribuída, que atualmente é o tipo de arquitetura mais adotado em ambientes de desenvolvimento com Java.

Seguindo esta linha, explicamos como é feita a distribuição da arquitetura através de camadas físicas (tiers) e lógicas (layers), e detalhamos como é feito o processo de separação das camadas lógicas (Apresentação, Serviço, Domínio e Infraestrutura) através do tópico “Separação de Responsabilidades”.

Além disso, analisamos também as melhores práticas envolvidas na criação de cada uma das camadas lógicas do sistema, as responsabilidades transversais que atuam em mais de uma destas camadas, e por fim, como evitar a criação de um modelo anêmico, através da aplicação do padrão Domain Model.

A segunda parte deste artigo apresenta alguns indícios de como identificar a qualidade do software e boas práticas relacionadas ao desenvolvimento, onde são abordados diversos princípios de Design OO, que quando aplicados corretamente aumentam a qualidade do código e do Design da aplicação.

Serve para arquitetos, analistas e desenvolvedores que desejam aprimorar as técnicas de design e engenharia considerando princípios de desenvolvimento ágil. O conteúdo do artigo apresenta também diversas dicas para o leitor reconhecer falhas de estrutura na aplicação, de forma a facilitar sua refatoração aplicando padrões de projeto.

O tema é útil para todo o desenvolvedor que quer melhorar a qualidade dos projetos de software, aprendendo a reconhecer um design ruim, e aplicar boas práticas de desenvolvimento e design Orientado a Objetos.

Na segunda parte deste artigo, vamos focar na análise para a construção do design de uma aplicação baseado em modelos ágeis, e dar dicas de como identificar sintomas que expressam más práticas de desenvolvimento, conhecidas como Design Smells. Para tratar estes sintomas, veremos alguns princípios da programação orientada a objetos, fruto da experiência de vários profissionais da área de Engenharia de Software, que servem como alicerce para a maioria dos padrões de projeto.

Na primeira parte deste artigo, analisamos conceitos importantes relacionados à arquitetura de software e seus tipos existentes, onde demos ênfase para a arquitetura distribuída, conceituando separação de responsabilidades, camadas físicas e lógicas, e a aplicação de alguns padrões de projeto para cada uma destas camadas.

Para esta segunda parte, vamos abordar as diferenças entre a criação de uma arquitetura seguindo métodos tradicionais, como o Waterfall, e a criação de um Design Ágil a partir de ciclos iterativos de duas a três semanas. Além disso, veremos meios de identificar um software mal planejado através de alguns sintomas conhecidos como Design Smells.

Para tratar estes sintomas, apresentaremos alguns princípios de Design OO (base de muitos padrões de projeto) que são o produto de décadas de experiência com a Engenharia de Software de não apenas um, mas de vários profissionais consagrados na área.

Design Ágil

Todos os anos o Standish Group – consultoria especializada em pesquisas na área de TI – prepara um relatório chamado Relatório do Caos (Chaos Report), que tem como objetivo apresentar o índice de sucesso nos projetos de software. No último estudo realizado, referente ao ano de 2009, o resultado foi alarmante.

O estudo apontou que apenas 32% dos projetos de software tiveram sucesso em sua implementação, enquanto 24% dos projetos falharam e nos 44% restantes houve algum tipo de desperdício, como atraso no projeto ou estouro no orçamento.

Entre os grandes vilões responsáveis pelas falhas nos projetos está a falta de definição clara dos requisitos e estimativas inapropriadas por parte dos analistas e arquitetos de software, que estimulados por metodologias de desenvolvimento de software como Waterfall, insistem em definir toda a arquitetura do sistema nas fases preliminares do projeto, resultando no chamado Big Design Upfront.

Waterfall:

O modelo Waterfall, ou modelo cascata, é um modelo de desenvolvimento de software sequencial, no qual uma fase somente inicia quando a anterior termina, passando pelas fases de Análise de Requisitos, Projeto, Implementação, Verificação e Manutenção.

Big Design Upfront:

É um termo utilizado para qualquer metodologia de desenvolvimento de software que define que o design da aplicação deve estar completo e perfeito antes do início do desenvolvimento da aplicação. Ele é geralmente associado ao método de desenvolvimento Waterfall.

Com a crescente popularidade das metodologias ágeis e a adoção de métodos como Scrum e XP, esta abordagem começou a cair em desuso, pois a construção do software passou a ser feita em pequenos incrementos, com ciclos iterativos de duas a três semanas, o que acabou levando a outra questão importante: como podemos assegurar que o software tenha uma estrutura que seja flexível, reutilizável, de fácil manutenção e que não fuja do escopo?

Talvez esta seja uma pergunta que não tenha resposta. O que sabemos é que os requisitos mudam, as regras de negócio mudam, o cliente muda, as pessoas mudam e, por fim, o sistema muda. Para evitarmos que estas constantes mudanças afetem nossa aplicação, ao iniciarmos o desenvolvimento de um sistema, se levarmos em consideração boas práticas de desenvolvimento, podemos chegar próximo ao software ideal, que atenda às necessidades do cliente com qualidade.

Para identificar se nosso sistema está com um bom design, Robert C. Martin, popularmente conhecido como Uncle Bob, apresentou alguns sintomas de design ruim, chamado de Design Smells (algo como Odores do Design). Estes sintomas permeiam a estrutura geral do software, e em seu livro Agile Software Development, Uncle Bob chegou a seguinte classificação:

o Viscosidade do Software: Ao precisar alterar um sistema, os desenvolvedores encontram maneiras alternativas de fazê-lo sem utilizar as interfaces do sistema, comprometendo e violando muitas vezes o design inicial da aplicação projetado pelo arquiteto. Ou ainda, quando as interfaces criadas pelos arquitetos para prover acesso ao sistema e que preservam a integridade da arquitetura da aplicação são mais difíceis de utilizar do que os hacks criados pelos desenvolvedores. Quando isso acontece dizemos que a viscosidade do software é alta. Em outras palavras, a viscosidade do software é alta quando é difícil para os desenvolvedores seguir a interface proposta pelo sistema. O correto é desenvolver um sistema cujas mudanças sejam fáceis de implementar e que ao final preservem o design da aplicação;

Hack

São as ações tomadas pelos desenvolvedores para “burlar” o design do sistema para atender um problema de desenvolvimento. Por exemplo: criar meios alternativos para extrair dados do banco de dados quando os métodos fornecidos pelo sistema são insuficientes.

O que a princípio parece ser a atitude correta e demonstra boa vontade do desenvolvedor, acaba fazendo com que grande parte das funcionalidades do software se torne inútil. Assim, quanto mais simples for o sistema melhor. O importante é atender os requisitos do cliente.

A metodologia XP (Extreme Programming) é baseada em quatro valores, que são Comunicação, Feedback, Coragem e Simplicidade. Este último relata justamente este problema, destacando que devemos tomar a decisão mais simples para resolver um problema de software. De forma que além de facilitar a manutenção do software posteriormente, facilita também a comunicação entre as pessoas envolvidas, pois reduz a complexidade da solução adotada;

Repetição Desnecessária: Esse odor é bem conhecido entre os desenvolvedores. Quem nunca sentiu vontade de copiar trechos de código apenas para criar uma funcionalidade adicional?

Quando um código começa a aparecer em diversas partes do sistema – em formas diferentes por meio de classes e métodos similares – é porque os desenvolvedores estão deixando de aplicar a abstração. Isso acarreta em um código difícil de entender e suscetível a bugs, pois no momento de efetuar uma alteração o desenvolvedor precisa alterar várias partes do sistema, dificultando sua manutenção.

A solução ideal e que nem sempre é uma das prioridades de quem desenvolve o sistema é identificar os pontos de redundância no código e aplicar a refatoração;

Opacidade: Opacidade é o grau de dificuldade que temos para entender um módulo. É muito comum, em diversas empresas, encontrar desenvolvedores que sejam especialistas em um determinado módulo do sistema, por exemplo, faturamento, folha de pagamento, etc., onde a lógica e o código utilizado na aplicação acabam se tornando muito pessoal ao desenvolvedor que as codificou. Nesta situação, quando o mesmo não aplica boas práticas, faz com que o código que ele produz seja compreendido muitas vezes apenas por ele, ou pior, depois de certo tempo sem dar manutenção nem o próprio desenvolvedor consegue compreender o que foi feito.

Uma boa técnica para desenvolver um código expressivo é utilizar uma das principais ideias do Domain Driven Design, a Linguagem Ubíqua (ou Onipresente), proposta por Eric Evans. Esta técnica consiste em aplicar uma linguagem comum entre os desenvolvedores e analistas de negócio, utilizando os conceitos do modelo de domínio como forma primária de comunicação, fazendo com que ela seja aplicada tanto nos discursos entre os técnicos e os stakeholders quanto na documentação do sistema. Como consequência, fazemos com que os mesmos termos utilizados no domínio do negócio sejam expressos também no código, o que torna a comunicação mais transparente entre os times durante as discussões sobre o modelo do domínio.

Princípios da Programação Orientada a Objetos

Os sintomas apresentados no tópico anterior podem ser identificados em qualquer parte do sistema e são causados geralmente pela violação de alguns princípios da Programação Orientada a Objetos. Estes princípios, ainda que antigos, são pouco difundidos, mas essenciais para criar um bom Design em qualquer projeto orientado a objetos.

Os princípios que abordaremos neste artigo são:

Princípio Aberto Fechado (Open-Closed Principle)

O primeiro princípio que vamos falar é o Principio Aberto-Fechado ou OCP (Open-Closed Principle). Sua definição diz que “as entidades de software de nossa aplicação (classes, funções, etc.) devem ser abertas para extensão e fechadas para alteração”.

Mas o que isso quer dizer? Quando fazemos uma pequena alteração em nosso código e esta alteração afeta outros módulos do sistema, quer dizer que o sistema está apresentando o Design Smell de Rigidez, e o princípio nos diz como devemos, através da abstração, refatorar nosso código para tratar este tipo de problema.

O OCP prega a flexibilidade no design para atender a definição de que as “entidades devem ser abertas para extensão e fechadas para alteração”. Uma maneira de atender este princípio seria aplicar o padrão de projeto Template Method [GoF], onde criamos uma classe abstrata e incluímos o comportamento que não desejamos que seja alterado (aplicando a palavra chave final ao método), e através de métodos abstratos abrimos para extensão apenas o comportamento que queremos, conforme a Figura 1.

Figura 1. Classe aberta para extensão e fechada para alteração.

Uma forma diferente de permitir que outros desenvolvedores estendam nossa classe e sobrescrevam o método que fechamos para alteração é utilizar o padrão de projeto Strategy [GoF] (ver quadro “Padrão de Projeto: Strategy”). Para entendermos o conceito, considere a classe Pagamento da Listagem 1 e imagine que ela é utilizada para efetuar pagamentos a funcionários de uma determinada empresa.

Listagem 1. Classe que viola o OCP.

public class Pagamento { public Pagamento(int tipoPagamento) { this.tipoPagamento = tipoPagamento; } private static final int VALOR_FECHADO = 1; private static final int VALOR_HORA = 2; private double horas; private double valor; private int tipoPagamento; public double calcular(){ switch(getTipoPagamento()){ case VALOR_FECHADO: return getValor(); case VALOR_HORA: return getValor()*getHoras(); default:return 0d; } } // Setters e Getters omitidos }

Padrão de Projeto: Strategy

O padrão Strategy, documentado pelo Gang of Four, consiste em encapsular uma família de algoritmos relacionados em classes que implementam uma interface em comum. De modo que os algoritmos sejam distribuídos em classes distintas, com o objetivo de torná-los intercambiáveis, permitindo assim que variem, independentemente dos clientes que os utilizem.

Na Figura Q1 apresentamos o diagrama de classes que descreve o padrão.

Figura Q1. O padrão Strategy.

Um dos benefícios do padrão Strategy é que retiramos a responsabilidade da classe Cliente de selecionar um comportamento específico ou de implementar algoritmos alternados. Além disso, a aplicação do padrão simplifica o código do Cliente, eliminando comandos de condição como if e switch. Em alguns casos, o padrão ajuda inclusive a aumentar a performance do Cliente, pois não precisa consumir tempo adicional selecionando um comportamento ou algoritmo específico.

Observe que o método calcular() da classe Pagamento atende a dois tipos de pagamento, para os funcionários que trabalham no regime CLT (VALOR_FECHADO) e para consultores que possuem empresa aberta (VALOR_HORA). Se neste cenário, por exemplo, precisarmos alterar a fórmula de um destes cálculos, quebraríamos o princípio aberto-fechado, pois teríamos que alterar a classe Pagamento, violando o conceito “fechado para alteração”. Para resolvermos esse problema e atender o princípio OCP, podemos alterar nossa classe para que fique de acordo com o diagrama da Figura 2.

Figura 2. Modelo que atende o OCP através do padrão Strategy.

O leitor mais atento irá notar que a Figura 2 é a aplicação do padrão Strategy [GoF]. Esse padrão nos permite definir uma família de algoritmos, encapsular cada um deles e torná-los intercambiáveis, o que possibilita alterar os algoritmos independentemente do cliente que o utiliza. Dessa forma, poderíamos incluir novos tipos de cálculo sem que seja necessário alterar a classe Pagamento.

O código de implementação deste modelo seria similar ao apresentado na Listagem 2. Em suma, para aplicar OCP basta que estendamos nosso modelo implementando uma interface ou criando subclasses de uma classe abstrata.

Listagem 2. Classe que atende o OCP.

// Definição da interface CalculoPagamento public interface CalculoPagamento { public double calcular(double horas, double valor); } public class Pagamento { public Pagamento(CalculoPagamento tipo) { this.tipoPagamento = tipo; } private double horas; private double valor; private CalculoPagamento tipoPagamento; public double calcular(){ // Referência à interface, não a implementação return tipoPagamento.calcular(horas, valor); } // Setters e Getters omitidos } // Implementação da classe CalculoPagamento public class CalculoConsultor implements CalculoPagamento{ public double calcular(double horas, double valor) { return valor * horas; } }

Princípio DRY (Don’t Repeat Yourself ou Não Repita Você Mesmo)

Don’t Repeat Yourself é um princípio fundamental quando queremos criar um código mais simples e reutilizável.

O principio DRY foi formulado por Andy Hunt e Dave Thomas no famoso livro The Pragmatic Programmer (O Programador Pragmático), e diz que devemos evitar duplicação de código abstraindo as partes que são comuns, colocando-as em um único local.

Quem nunca lidou com duplicação de código? Desenvolvedores mais antigos que iniciaram suas carreiras com linguagens de programação procedural ou desenvolvimento de sistemas de duas camadas (Cliente-Servidor), como Oracle Forms, Delphi e VB, sabem muito bem o que é isto. Quando um sistema tem muito código duplicado, com regras de negócio espalhadas por várias funções e métodos, certamente em um dado momento o desenvolvedor gastará preciosas horas com manutenção no código, o que é um grande pesadelo.

O tipo de duplicação mais fácil de reconhecer é a duplicação explícita, que é a existência de código idêntico em classes/métodos diferentes. Como já informamos, a duplicação acarreta no Design Smell “Repetição Desnecessária”. Existe também a duplicação sutil, que é um pouco mais difícil de reconhecer, pois é aquela que existe em estruturas ou passos de processamento aparentemente diferentes, mas que são essencialmente os mesmos, por exemplo, criar variações de uma classe, como: Pessoa, PessoaComEndereco, PessoaSemEndereco, etc.

Quando são identificados estes sintomas podemos aplicar técnicas de refatoração para tratar a duplicação. Mas a solução para isso não é apenas pegar o código que aparece em mais de um lugar e colocar em uma única classe. Como vimos nos tópicos “Separação de Conceitos” e “Arquitetura Distribuída”, no primeiro artigo da série, é preciso ter certeza de que cada componente (pacote, classe, método) do seu sistema esteja em um local único e de fácil identificação, de maneira que quem desenvolva o sistema saiba exatamente para onde ir quando precisar da informação ou do comportamento.

Código duplicado pode aparecer em vários locais e em níveis diferentes do software. Podemos ter duplicação no design da aplicação, em pacotes do projeto, em pequenos trechos de código e até mesmo na documentação. Ou seja, o princípio não se aplica somente na duplicação de código, mas também em funcionalidades e requisitos. Um exemplo clássico de uma especificação Java EE que violou o princípio DRY é a especificação dos EJBs 2.x, que obrigava os desenvolvedores a duplicar código. Descendo um pouco a granularidade, se os construtores de uma classe possuem duplicação, você pode utilizar o padrão “Construtores Encadeados” (Chain Constructors), documentado no livro “Refactoring to Patterns [RTP]” (ver Livros), conforme ilustra a Listagem 3.

Listagem 3. Uso de Construtores Encadeados.

// Duplicação de Código nos construtores public class Pessoa { public Pessoa(String nome, char estadoCivil, int idade, int peso) { this.nome = nome; this.estadoCivil = estadoCivil; this.idade = idade; this.peso = peso; } public Pessoa(String nome, char estadoCivil, int idade) { this.nome = nome; this.estadoCivil = estadoCivil; this.idade = idade; } public Pessoa(String nome, char estadoCivil) { this.nome = nome; this.estadoCivil = estadoCivil; } private String nome; private char estadoCivil; private int idade; private int peso; } // Aplicando o padrão Construtores Encadeados public class Pessoa { public Pessoa(String nome, int idade) { this(nome, idade, 'I', 0); } public Pessoa(String nome, char estadoCivil, int idade) { this(nome, idade, estadoCivil, 0); } public Pessoa(String nome, int idade, char estadoCivil, int peso) { this.nome = nome; this.idade = idade; this.estadoCivil = estadoCivil; this.peso = peso; } private String nome; private char estadoCivil; private int idade; private int peso; }

Na Listagem 3 apresentamos a classe Pessoa com três construtores. Na primeira versão desta classe repetimos nos construtores o código de atribuição para as variáveis. Agora imagine um cenário onde seja necessário adicionar a variável peso à classe Pessoa. Teríamos que alterar todos os construtores para inicializar o valor da variável. Caso o desenvolvedor se esqueça de alterar um dos construtores, a classe ganharia um novo bug, provavelmente um NullPointException.

Esta abordagem é muito interessante quando você tem até três ou quatro construtores, mais do isso é recomendado o uso do padrão Builder, como apresenta a Listagem 4.

Listagem 4. Uso do Padrão Builder.

public class Pessoa { private Pessoa(Builder builder) { this.nome = builder.nome; this.idade = builder.idade; this.estadoCivil = builder.estadoCivil; this.peso = builder.peso; this.altura = builder.altura; } private final String nome; private final char estadoCivil; private final int idade; private final int peso; private final float altura; public static class Builder { public Builder(String nome, int idade) { this.nome = nome; this.idade = idade; } public Builder estadoCivil(char estadoCivil) { this.estadoCivil = estadoCivil; return this; } public Builder peso(int peso) { this.peso = peso; return this; } public Builder altura(float altura) { this.altura = altura; return this; } public Pessoa build() { return new Pessoa(this); } // Campos Obrigatórios private final String nome; private final int idade; // Campos Opcionais private char estadoCivil = 'I'; private int peso = 0; private float altura = 0f; } }

Note que agora a classe Pessoa se tornou imutável com todos os campos definidos com a palavra-chave final, e os valores dos parâmetros default estão todos em um único local. A classe Builder é uma subclasse de Pessoa, os métodos setters do Builder retornam a própria instância (this), de forma que as chamadas podem ser encadeadas. Veja que ficou mais simples e intuitivo criar uma instância da classe Pessoa, conforme ilustra a Listagem 5.

Listagem 5. O Padrão Builder em ação.

Pessoa wagner = new Pessoa.Builder("Wagner", 30) .estadoCivil('C') .peso(90) .altura(1.86f) .build();

Note também que agora o construtor da classe Pessoa está privado, para evitar a construção direta do objeto, obrigando o desenvolvedor a chamar o método build() para criar uma instância de Pessoa.

No exemplo da Listagem 5 informamos o estado civil, o peso e a altura da pessoa que queremos criar, e por último é chamado o método build(), que por sua vez cria o objeto da classe Pessoa passando ele mesmo (this) como parâmetro em sua implementação apresentada na Listagem 4 .

Princípio da Responsabilidade Única (The Single Responsibility Principle)

Este princípio diz que cada componente deve ser responsável por somente uma funcionalidade, portanto, deve existir apenas um motivo para que ele mude. Quando há mais de um motivo para se alterar uma classe é por que ela tem mais responsabilidades do que deveria. Neste caso, deve-se considerar a decomposição da mesma em duas ou mais classes. Assim, quando algo sobre esta responsabilidade mudar, você saberá exatamente onde procurar a classe para fazer as alterações no código.

O princípio da responsabilidade única é muito similar com o DRY, e geralmente os dois são aplicados em conjunto. Enquanto o princípio DRY nos ensina sobre como colocar as funcionalidades do código em um único lugar para evitar a duplicação, o SRP nos diz para ter certeza de que a classe faça apenas uma função, de maneira bem definida. Durante a fase de definição do design do sistema, a aplicação do SRP é fundamental para que o software alcance ou tenha uma alta coesão.

Para demonstrar o uso de SRP, veja a classe Restaurante da Figura 3.

Figura 3. Violação do SRP.

Esta classe claramente viola o princípio da Responsabilidade Única porque tem mais responsabilidades do que realmente deveria. Ao fazer uma análise em seus métodos, deduziremos que não é responsabilidade de um restaurante servir pratos, limpar as mesas e listar os pratos, mas sim abrir, fechar e informar a lotação máxima do estabelecimento.

Para organizar este modelo, o ideal é refatorar a classe Restaurante movendo os métodos que não fazem sentido para as classes responsáveis por estas atividades. Feito isso, nosso modelo ficaria similar ao apresentado na Figura 4.

Figura 4. Classe que segue o SRP.

Princípio da Substituição de Liskov (LSP)

Esse princípio foi proposto por Barbara Liskov em 1987, e diz que “subtipos de uma classe devem ser substituíveis por seus tipos base”. O fundamento deste princípio está sobre interfaces e contratos, e sobre qual a estratégia adotada quando temos que decidir se devemos estender uma classe ou utilizar outro meio, como composição, para resolver este problema.

Quando Liskov falou em substituição ela quis dizer que em uma hierarquia de classes, toda subclasse (que implemente uma interface) pode ser substituída por outra subclasse que implemente seu tipo base. Para entender melhor, tome como um exemplo o método f() de uma classe qualquer, que recebe como argumento uma referência para a classe base B. Agora imagine que ao passar para o método f() uma referência da classe D (que é derivada de B) recebemos um erro, ou um comportamento inesperado. Neste caso, a classe D viola o LSP, pois essa classe é frágil em relação ao método f(), isto é, teremos que customizá-la para que ela possa ser utilizada em nossa aplicação.

Para citar outro exemplo, veja o framework Collections do Java (Figura 5). Nesta API toda subclasse da interface Collection pode ser substituída tanto por uma subclasse de List quanto de Set, ou uma de suas derivadas, sem gerar exceção. Na Listagem 6 apresentamos um exemplo.

Figura 5. Framework Collections do Java.

Listagem 6. Exemplo do Princípio de Substituição de Liskov.

Collection col = new ArrayList(); col.add("ElementoArrayList"); col = new HashSet(); col.add("ElementoHashSet"); col = new TreeSet();

Se os mecanismos por trás do OCP são a abstração e o polimorfismo, em Java o mecanismo principal que nos dá este suporte é a herança. Por sua vez, o princípio de Liskov nos diz como utilizar herança da maneira correta.

Para entendermos melhor este conceito, veja outro exemplo na Listagem 7, mas agora de uma classe que viola o LSP.

Listagem 7. Mal uso de herança.

public class Processo { TipoEnvio tipo; public Processo(TipoEnvio tipo){ this.tipo = tipo; } public void executar(){ // Tratamento especial quando utilizamos meio do tipo Email. if (tipo instanceof Email){ ((Email) tipo).loadParams(); } tipo.enviar(); } }

Na Listagem 7, observe que no método executar() fazemos um tratamento especial quando o tipo de envio a ser utilizado for Email, o que denota um mal uso de herança.

Para demonstrar um último exemplo, vamos voltar à Figura 2 (sobre OCP), onde definimos a interface CalculoPagamento para criar as classes CalculoContratado e CalculoConsultor. Neste exemplo, se precisarmos formular um novo cálculo para vendedores comissionados, teríamos um novo problema, pois precisaríamos de outra informação para nossa fórmula: o valor da comissão. Para atender este problema utilizando herança, nosso modelo ficaria similar ao da Figura 6.

Figura 6. Violação do princípio de Liskov.

Ao analisar a classe CalculoComissionado notaremos que foi necessário sobrecarregar o método calcular() para aceitar um novo parâmetro, que é justamente o valor da comissão. Entretanto, o método calcular() que herdamos de CalculoPagamento acabou perdendo o sentido, pois não temos como calcular o pagamento sem o valor da comissão, violando assim o princípio da substituição de Liskov. Desta maneira não poderíamos trocar a classe CalculoConsultor por CalculoComissionado, por exemplo. Outro problema referente ao LSP é que uma violação como esta torna o código confuso, tanto para debug quanto para o desenvolvedor que utilizará a classe, podendo gerar dúvidas em relação a qual método utilizar.

Uma boa maneira de resolver o problema do LSP sem utilizar herança é delegar as funcionalidades em comum para a classe especializada ou utilizar composição. Na Figura 7, apresentamos uma solução para o problema exposto na Figura 6, onde delegamos o cálculo da comissão para uma classe especializada (Comissao).

Figura 7. Solução aderente ao princípio de Liskov.

Dentro do universo Java temos alguns exemplos de classes que violam o LSP. Entre os mais famosos podemos citar a especificação EJB 2.1, que além de violar o princípio de Liskov, viola outros princípios, como o SRP, pois a especificação obrigava o desenvolvedor a implementar diversas outras classes e definir os métodos de callback para um único EJB. No Java SE podemos citar a classe java.util.Stack, que estende a classe java.util.Vector (um Stack não é um Vector); ou a classe java.util.Properties, que estende a classe java.util.Hashtable (uma lista de propriedades não é um Hashtable). Portanto, sempre que possível utilize composição ao invés de herança. Dessa forma evitamos expor detalhes de implementação sem necessidade.

Princípio do Conhecimento Mínimo (The Principle of Least Knowledge)

Conhecido também como a Lei de Demeter, esse princípio visa reduzir as interações entre os objetos fazendo com que as mesmas aconteçam apenas entre os objetos mais próximos, que possuem um vínculo direto, como associação ou agregação.

Na prática, quando estamos montando o design da nossa aplicação, devemos tomar cuidado com o número de classes com que os objetos interagem e como deve ser a interação entre eles.

Este princípio nos previne de criar um modelo com muitas classes acopladas, evitando que mudanças em nosso sistema afetem outras áreas da aplicação. Um sistema construído com base em muitas dependências se torna frágil e destinado a horas e horas de manutenção, além de trazer uma complexidade desnecessária.

Este princípio nos fornece algumas regras para validarmos se nossa classe possui um conhecimento mínimo referente a outros objetos. Para exemplificar, considere um objeto, e a partir de qualquer um de seus métodos, o princípio nos diz que devemos invocar outros métodos que pertençam somente:

Para entendermos melhor este conceito, veja o exemplo na Listagem 8 que viola o Princípio do Conhecimento Mínimo.

Listagem 8. Violação do Princípio do Conhecimento Mínimo.

public class Notificacao{ public Notificacao(Cliente cliente){ this.cliente = cliente; } private Cliente cliente; public void enviarEmail(){ // Violação do Princípio do Conhecimento Mínimo String email = cliente.getDadosComunicacao().getEmail(); enviarEmail(email); } }

Nesta listagem apresentamos um exemplo simples. A classe Notificacao possui o método enviarEmail(), que possui o claro objetivo de enviar e-mails, mas para realizar esta ação, primeiro o objeto cliente faz uma chamada ao método getDadosComunicacao(), que por sua vez, com o objeto retornado, faz uma chamada ao método getEmail() logo em seguida. Com isso, na mesma linha, o objeto cliente utiliza mais de um objeto para retornar o e-mail do cliente, aumentando assim o número de objetos conhecidos diretamente por nós. Quanto maior o ciclo de chamadas a métodos encadeados, pior.

Na Listagem 9 apresentamos o mesmo exemplo, mas agora aplicando o princípio.

Listagem 9. Classe que aplica o Princípio do Conhecimento Mínimo.

public class Notificacao{ ... public void enviarEmail(){ enviarEmail(cliente.getEmail()); } }

Um dos benefícios deste princípio é que ele reduz as dependências entre objetos e facilita o entendimento e a manutenção do software. Por outro lado, o número de classes do sistema aumenta consideravelmente, pois precisamos de mais classes para efetuar chamadas únicas a métodos de outros componentes.

Conclusões

Neste artigo aprendemos o quanto os princípios, padrões e as práticas são importantes na construção de um Design Ágil. Entretanto, mais importante do que isso são as pessoas neste processo, pois são elas que fazem estas práticas funcionarem. Para fazer um projeto de software ter sucesso, precisamos de times de desenvolvedores que sejam autogerenciáveis e colaborativos, que saibam exatamente o objetivo de cada interação do projeto e as atividades a serem feitas.

Se agilidade é sobre construir software em pequenos incrementos, podemos garantir a qualidade do que está sendo produzido através da aplicação de padrões de projeto e de princípios de design que foram abordados neste artigo, como os Princípios de Responsabilidade Única (SRP), Aberto/Fechado (OCP), DRY, Substituição de Liskov e Conhecimento Mínimo (PLK).

Na próxima e última parte do artigo, veremos mais alguns princípios de programação OO, boas práticas de desenvolvimento e outros padrões de projeto relacionados.

Referências

Artigos relacionados