Introdução A Padrões de Projeto - Design Patterns
Introdução A Padrões de Projeto - Design Patterns
Introdução A Padrões de Projeto - Design Patterns
Padrões de projeto de software e visão geral dos padrões GRASP, SOLID e GoF.
PROPÓSITO
Compreender o conceito de padrões de projeto e ser capaz de identificar oportunidades para a
aplicação dos principais padrões em projetos de sistemas são habilidades importantes para a
sua formação, pois elas possibilitam a criação de programas bem estruturados por meio do uso
de soluções reconhecidamente bem-sucedidas para problemas recorrentes no
desenvolvimento de software.
PREPARAÇÃO
Este conteúdo não requer nenhum pré-requisito operacional obrigatório. Porém, antes de
iniciar, é recomendado ter instalado em seu computador um programa que lhe permita elaborar
modelos sob a forma de diagramas da UML (Linguagem Unificada de Modelagem). Nossa
sugestão inicial é o Astah Free Student License, usado nos exemplos deste estudo, e, para
isso, será necessário lançar mão do seu e-mail institucional para ativar a licença. Preencha os
dados do formulário no site do software, envie e aguarde a liberação de sua licença em seu e-
mail institucional. Ao receber a licença, siga as instruções do e-mail e instale o produto em seu
computador. Os arquivos-fonte dos diagramas Astah UML mostrados neste conteúdo podem
ser baixados aqui.
Sugestões de links adicionais de programas livres para modelagem de sistemas em UML (UML
Tools) podem ser encontradas em buscas na internet.
OBJETIVOS
MÓDULO 1
MÓDULO 2
Reconhecer o propósito dos padrões GRASP e as situações nas quais eles podem ser
aplicados
MÓDULO 3
Reconhecer o propósito dos principais padrões GoF e as situações nas quais eles podem ser
aplicados
INTRODUÇÃO
Desenvolvedores pouco experientes frequentemente têm dificuldade em estruturar um software
de forma que ele satisfaça os requisitos de qualidade demandados.
Por que é importante estruturar um software adequadamente? Não basta programar as suas
instruções de forma que ele realize corretamente as funções demandadas pelo cliente?
Neste estudo, você aprenderá como os conceitos de padrões de projeto, padrões GRASP, GoF
e princípios SOLID podem ser empregados para a produção de sistemas bem estruturados.
MÓDULO 1
Você já ouviu falar em padrões de projeto (design patterns)? Sabe por que eles foram
criados?
DESIGN PATTERNS
Design patterns ou padrões de projeto são soluções gerais para problemas recorrentes
durante o desenvolvimento de um software.
Neste módulo, você vai aprender o que são padrões de projeto, como eles são definidos e
entender as vantagens e desvantagens da sua utilização.
Imagine que você foi alocado em um novo trabalho, ficando responsável por evoluir um sistema
desenvolvido por outras pessoas ao longo de anos.
Você não se sentiria mais confortável se o software tivesse sido projetado com estruturas de
solução que você já conhecesse?
Entretanto, quando temos que estruturar um sistema, a pergunta passa a ser bem mais
complexa:
Ao desenvolvermos um sistema, nos deparamos inúmeras vezes com essa pergunta. É comum
um desenvolvedor iniciante se sentir perdido diante da diversidade de respostas possíveis,
sendo que algumas podem facilitar, enquanto outras podem dificultar bastante as futuras
evoluções do sistema.
Imagem: Shutterstock.com
Quanto maior for o arsenal de situações conhecidas por um jogador, maiores as chances que
ele terá de fazer uma boa jogada em pouco tempo.
É isso mesmo: os jogadores de xadrez também têm um prazo para cumprir. Existem
campeonatos que definem um tempo máximo de duas horas para que cada jogador realize
seus primeiros quarenta lances.
Um jogador iniciante não aprende apenas jogando e vivenciando essas situações. Ele pode
estudar inúmeras situações catalogadas pelas quais outros jogadores já passaram.
COMENTÁRIO
Existem catálogos de situações e estratégias para o início, para o meio e para a finalização
de um jogo de xadrez.
As estratégias para o início de jogo são conhecidas como aberturas. Cada abertura tem um
nome e corresponde a um conjunto de jogadas que geram consequências positivas e,
eventualmente, negativas para cada lado (peças brancas ou pretas). Uma dessas aberturas é o
nome de uma das séries originais mais vistas na história da Netflix: O gambito da rainha.
RESPOSTA
Muitos problemas em estruturação de software são recorrentes, então que tal registrá-los com
as respectivas soluções para que, de forma análoga aos livros de abertura do xadrez,
possamos compartilhar esse conhecimento com desenvolvedores que estejam encarando um
problema análogo pela primeira vez?
Foi o que pensaram quatro engenheiros de software (Erich Gamma, Richard Helm, Ralph
Johnson e John Vlissides) em 1994, quando publicaram o livro Design patterns: elements of
reusable object-oriented software , descrevendo 23 padrões que eles utilizaram no
desenvolvimento de diversos sistemas ao longo de suas carreiras. Os autores ficaram mais
conhecidos como a “gangue dos quatro” (tradução de gang of four), e os padrões publicados
são conhecidos como os “padrões GoF”. Desde então, diversas publicações surgiram
relatando novos padrões, críticas a padrões existentes, padrões específicos para determinadas
linguagens e paradigmas de programação, padrões arquiteturais e até mesmo antipadrões –
construções que tipicamente trazem consequências negativas para a estrutura de um sistema.
O QUE É UM PADRÃO DE PROJETO
Em uma estrutura de solução orientada a objetos, a solução é descrita por classes, interfaces e
mecanismos de colaboração entre objetos.
Porém, o conceito de padrões de projeto é mais abrangente, uma vez que existem padrões
voltados para outros paradigmas de programação, como, por exemplo, programação
funcional e programação reativa.
NOME
O nome possibilita a comunicação entre os desenvolvedores em um nível mais abstrato. Em
uma discussão de projeto, em vez de descrevermos todos os elementos participantes da
solução para um problema, a forma de comunicação entre eles e as consequências do seu
uso, basta mencionarmos o nome do padrão apropriado.
Um desenvolvedor experiente pode recomendar algo como: “use o Observer para resolver
esse problema!”, e isso será suficiente, pois conhecendo o padrão Observer, você já saberá
como estruturar a solução. Portanto, os nomes dos padrões foram um vocabulário
compartilhado pelos desenvolvedores, para agilizar a discussão de soluções de projeto.
PROBLEMA
Um padrão deve descrever as situações nas quais a sua utilização é apropriada, explicando o
problema e o contexto. Os problemas podem ser específicos, como, por exemplo: “como
podemos representar diferentes algoritmos com o mesmo propósito na forma de classes?” ou
“como podemos instanciar uma classe específica, dentre várias similares, sem criar uma
dependência rígida com a implementação escolhida?”.
Este elemento também pode descrever e discutir soluções inadequadas ou pouco flexíveis
para o contexto apresentado, de forma que, se você estiver pensando em uma solução similar
às descritas, o seu problema passará a ser: “ok, estou vendo que a estrutura que estava
pensando em aplicar não é adequada. Como devo, então, estruturar a solução?”.
SOLUÇÃO
O padrão deve descrever os elementos recomendados para a implementação, suas
responsabilidades, seus relacionamentos e colaborações. A solução é uma descrição abstrata
que pode ser replicada em diferentes situações e sistemas. O desenvolvedor deve traduzir
essa descrição abstrata em uma implementação específica no problema particular que ele
estiver resolvendo. Note que a solução é uma ideia que precisa ser construída e implementada
pelo desenvolvedor. Um catálogo de padrões de projeto é bem diferente de uma biblioteca de
código, pois um padrão de projeto não tem o propósito de promover reutilização de código,
mas sim de conhecimento, know-how.
CONSEQUÊNCIAS
Um padrão precisa discutir os custos e benefícios da solução proposta. Uma solução pode dar
maior flexibilidade ao projeto, ao mesmo tempo em que pode trazer maior complexidade ou até
mesmo resultar em problemas de performance. Antes de utilizarmos um padrão, devemos
sempre avaliar os custos e benefícios em relação ao problema particular que estamos
querendo resolver.
Por que você deve conhecer padrões de projeto? Podemos enumerar alguns motivos:
Padrões reforçam práticas de reutilização de código com estruturas que acomodam mudanças
por meio do uso de mecanismos como delegação, composição e outras técnicas que não
sejam baseadas em estruturas de herança.
Padrões fornecem uma linguagem comum para os desenvolvedores, agilizando a troca de
ideias e experiências.
Desde que o conceito foi apresentado na década de 1990, surgiu uma grande quantidade de
padrões. Portanto, a curva de aprendizado pode ser grande.
Decidir se um padrão pode ser empregado em um problema específico nem sempre é uma
tarefa fácil e requer alguma experiência.
É comum um iniciante achar que os padrões devem estar por toda a implementação e acabar
fazendo uso inadequado deles.
Não existe consenso sobre a qualidade de todos os padrões. Singleton, por exemplo, é um
padrão GoF que gera grande controvérsia, sendo considerado um antipadrão por muitos.
SAIBA MAIS
Como achá-los? Experimente fazer uma busca na internet com termos gerais, tais como
“pattern books”, “patterns catalog”, ou mais específicos, como “java design patterns”, “cloud
architecture patterns”, “node js patterns”.
VERIFICANDO O APRENDIZADO
1. ASSINALE A AFIRMATIVA VERDADEIRA SOBRE PADRÕES DE
PROJETO:
A) Um padrão de projeto corresponde a uma biblioteca de códigos pronta para ser utilizada de
forma a resolver um problema recorrente em projetos de software.
E) Padrões de projeto podem ser utilizados somente com linguagens orientadas a objetos.
A) Todo padrão deve ter um nome, pois eles formam um vocabulário que agiliza a discussão de
uma solução entre os desenvolvedores de software.
D) Como todo padrão corresponde a uma solução bem-sucedida para o problema, ele deve
apresentar as vantagens obtidas com o seu uso.
E) Um padrão não deve descrever desvantagens do seu uso, pois isso caracterizaria que a
solução não é adequada para o problema.
GABARITO
A alternativa D é falsa, pois um padrão precisa ter sido utilizado com sucesso em pelo
menos três contextos diferentes de aplicação.
2. Assinale a afirmativa falsa sobre a forma com que os padrões devem ser descritos:
MÓDULO 2
Reconhecer o propósito dos padrões GRASP e as situações nas quais eles podem
ser aplicados
PADRÕES GRASP
GRASP é o acrônimo para o termo em inglês General Responsibility Assignment Software
Patterns, termo definido por Craig Larman no livro intitulado Applying UML and patterns, que
define padrões gerais para a atribuição de responsabilidades em software.
Os padrões GRASP podem ser vistos como princípios gerais de projeto de software orientado
a objetos aplicáveis a diversos problemas específicos.
Especialista
Criador
Baixo acoplamento
Alta coesão
Controlador
Polimorfismo
ESPECIALISTA
Problema:
Solução:
COMENTÁRIO
Suponha que você esteja desenvolvendo um site de vendas de produtos pela internet.
A figura a seguir apresenta um modelo simplificado de classes desse domínio. Digamos que o
site deva apresentar o pedido do cliente e o valor total do pedido.
Como você organizaria as responsabilidades entre as classes, para fornecer essa informação?
O valor total do pedido pode ser definido como a soma do valor de cada um de seus itens.
A classe Item do Pedido conhece a quantidade e o produto associado. Vamos definir, então,
uma operação obterValor na classe Item do Pedido.
E o preço do produto?
Basta o objeto item do pedido pedir essa informação para o produto, por meio da operação de
acesso getPrecoUnitario.
Consequências:
Quando o padrão Especialista não é seguido, é comum encontrarmos uma solução deficiente
(antipadrão) conhecida como “God Class” – que consiste em definir apenas operações de
acesso (conhecidas como getters e setters) nas classes de domínio – e concentrar toda a
lógica de determinada funcionalidade do sistema em uma única classe, usualmente definida na
forma de uma classe de controle ou de serviço. Essa classe implementa procedimentos
utilizando as operações de acesso das diversas classes de domínio, que, nesse estilo de
solução, são conhecidas como classes “idiotas”.
Existem, entretanto, situações em que a utilização desse padrão pode comprometer conceitos
como coesão e acoplamento.
EXEMPLO
Qual classe deveria ser responsável por implementar o armazenamento dos dados de um
Pedido no banco de dados?
Pelo princípio do Especialista, deveria ser a própria classe Pedido, uma vez que ela detém
todas as informações que serão armazenadas. Porém, essa solução acoplaria a classe de
negócio com conceitos relativos à tecnologia de armazenamento (e.g. SQL, NoSQL, arquivos
etc.), ferindo o princípio fundamental da coesão, pois a classe Pedido ficaria sujeita a dois tipos
de mudança: mudanças no negócio e mudanças na tecnologia de armazenamento, o que é
absolutamente inadequado.
CRIADOR
Problema
Solução
RESPOSTA
Uma abordagem comum, mas inadequada, é instanciar o item em uma classe de serviço e
apenas acumulá-lo no Pedido. Entretanto, quando se trata de uma relação entre um agregado
e suas partes, a responsabilidade pela criação das partes deve ser alocada ao agregado,
responsável por todo o ciclo de vida das suas partes (criação e destruição).
Consequências
O padrão Criador é especialmente indicado para a criação de instâncias que formam parte de
um agregado, por promover uma solução de menor acoplamento.
Por outro lado, o padrão Criador não é apropriado em algumas situações especiais, como, por
exemplo, a criação condicional de uma instância dentro de uma família de classes similares.
BAIXO ACOPLAMENTO
Problema
Se uma classe A depende de uma classe B, dizemos que A depende de uma implementação
concreta presente em B.
Por outro lado, se uma classe A depende de uma interface I, dizemos que A depende de uma
abstração, pois A poderia trabalhar com diferentes implementações concretas de I, sem
depender de nenhuma específica.
De forma geral, sistemas mais flexíveis são construídos quando fazemos implementações
(classes) dependerem de abstrações (interfaces), especialmente quando a interface abstrair
diferentes possibilidades de implementação, seja por envolver diferentes soluções tecnológicas
(ex.: soluções de armazenamento e recuperação de dados), seja por envolver diferentes
questões de negócio (ex.: diferentes regras de negócio, diferentes fornecedores de uma
mesma solução de pagamento etc.).
Solução
Essa solução gerou um acoplamento entre Pedido e Produto que não está presente na solução
dada pelo padrão Especialista.
Consequências
ATENÇÃO
Cuidado para não gerar soluções excessivamente complexas em que não haja motivação real
para criar soluções mais flexíveis. Em geral, manter as classes de domínio isoladas e não
dependentes de tecnologia (ex.: persistência, GUI, integração entre sistemas) é uma política
geral de acoplamento que deve ser seguida, recomendada por diversas proposições de
arquitetura.
GUI
ALTA COESÃO
Problema
COMENTÁRIO
Módulos ou classes com baixa coesão realizam muitas operações pouco correlacionadas,
gerando sistemas de difícil entendimento, reuso, manutenção e muito sensíveis às mudanças.
Solução
A solução consiste em definir módulos de alta coesão. Mas como se mede a coesão?
Coesão está ligada ao critério utilizado para reunir um conjunto de elementos em um mesmo
módulo.
Coesão está ligada ao critério utilizado para reunir um conjunto de elementos em um mesmo
módulo.
1
COESÃO DE UM PACOTE
COESÃO DE UM SUBSISTEMA
A coesão de um módulo, seja ele classe, pacote ou subsistema, pode ser classificada de
acordo com o critério utilizado para reunir o conjunto dos elementos que o compõem.
Devemos estruturar os módulos de forma que eles apresentem coesão funcional, isto é, os
elementos são agrupados porque, juntos, cumprem um único propósito bem definido.
As classes do pacote java.io da linguagem Java, por exemplo, estão reunidas por serem
responsáveis pela entrada e saída de um programa. Nesse pacote, encontramos classes com
responsabilidades bem específicas, como:
FILEOUTPUTSTREAM
FILEINPUTSTREAM
FILEREADER
FILEWRITER
Consequências
COMENTÁRIO
Sistemas construídos com módulos apresentando alta coesão tendem a ser mais flexíveis,
mais fáceis de serem entendidos e evoluídos, além de proporcionarem mais possibilidades de
reutilização e de um projeto com baixo acoplamento.
Entretanto, em sistemas distribuídos é preciso balancear a elaboração de módulos com
responsabilidades específicas com o princípio fundamental de sistemas distribuídos, que
consiste em minimizar as chamadas entre processos.
CONTROLADOR
Problema
Um sistema interage com elementos externos, também conhecidos como atores. Muitos
elementos externos geram eventos que devem ser capturados pelo sistema, processados e
produzir alguma resposta, interna ou externa.
Por exemplo, quando o cliente solicita o fechamento de um pedido na nossa loja online, esse
evento precisa ser capturado e processado pelo sistema. Este padrão procura resolver o
seguinte problema:
Solução
Opção 1
Opção 2
Uma classe correspondente a um caso de uso onde o evento ocorra. Normalmente essa classe
tem o nome formado pelo nome do caso de uso com um sufixo Processador, Controlador,
Sessao ou algo similar.
Essa classe deve reunir o tratamento de todos os eventos que o sistema receba no contexto
deste caso de uso. Esta solução evita a concentração das responsabilidades de tratamento de
eventos de diferentes funcionalidades em um único Controlador Fachada, evitando a criação
de um módulo com baixa coesão.
ATENÇÃO
Note que esta classe não cumpre responsabilidades de interface com o usuário. Em um
sistema de home banking, por exemplo, o usuário informa todos os dados de uma transação de
transferência em um componente de interface com o usuário e, ao pressionar o botão transferir,
esse componente delega a requisição para o controlador realizar o processamento lógico da
transferência. Assim, o mesmo controlador pode atender a solicitações realizadas por
diferentes interfaces com o usuário (web, dispositivo móvel, totem 24 horas).
Consequências
Em uma orquestra, um maestro comanda o momento em que cada músico deve entrar em
ação, mas ele mesmo não toca nenhum instrumento.
Um problema que pode ocorrer com este padrão é alocar ao Controlador responsabilidades
além da orquestração, como se o maestro, além de comandar os músicos, ficasse responsável
também por tocar piano, flauta e outros instrumentos. Essa concentração de responsabilidades
no Controlador gera um módulo grande, complexo e que ninguém se sente confortável em
evoluir.
POLIMORFISMO
Problema
Suponha que você esteja implementando a parte de pagamento em cartão de uma loja virtual.
Para realizar um pagamento interagindo diretamente com uma administradora de cartão, temos
que passar por um processo longo e complexo de homologação junto a ela. Imagine realizar
esse processo com cada administradora!
BROKER
API
Agora imagine que fornecemos uma solução de software de loja virtual para diferentes lojas,
que podem demandar diferentes brokers de pagamento em função das suas exigências de
segurança, preço e volume de transações. Isso significa que o nosso software tem que ser
capaz de funcionar com diferentes brokers, cada um com a sua API.
Sem polimorfismo, esse problema poderia ser resolvido com uma solução baseada em if-then-
else ou switch-case, sendo cada alternativa de brokers mapeada para um comando case no
switch ou em uma condição no if-then-else (figura a seguir).
Pense como ficaria esse código se houvesse vinte brokers diferentes.
No nosso exemplo, as alternativas são todos os diferentes tipos de brokers com as suas
respectivas APIs.
Solução
A solução via polimorfismo consiste em criar uma interface genérica para a qual podem existir
diversas implementações específicas (veja na figura seguinte).
A estrutura condicional é substituída por uma única chamada utilizando essa interface genérica.
O chamador, portanto, não precisa saber quem está do outro lado da interface concretamente
provendo a implementação. Essa capacidade de um elemento (a interface genérica) poder
assumir diferentes formas concretas (broker1, broker2 ou broker3, no nosso exemplo) é
conhecida como polimorfismo.
Imagem: Alexandre Luis Correa, adaptada por Heloise Godinho
Solução com polimorfismo.
Consequências
DICA
Esse princípio geral é utilizado para a definição de diversos padrões GoF, tais como: Adapter,
Command, Composite, Proxy, State e Strategy.
Entretanto, é preciso ter cuidado para não implementar estruturas genéricas em situações em
que não haja possibilidade de variação. Uma solução genérica é mais flexível, mas é preciso
estar atento para não investir esforço na produção de soluções genéricas para problemas que
sejam específicos por natureza, isto é, que não apresentem variantes de implementação.
COESÃO E ACOPLAMENTO, OS PILARES
DA ESTRUTURAÇÃO DE SOFTWARE
Os conceitos de coesão e acoplamento são apresentados de maneira detalhada no vídeo a
seguir.
VERIFICANDO O APRENDIZADO
1. ASSINALE A AFIRMATIVA QUE APRESENTA A RECOMENDAÇÃO
EXPRESSA PELO PADRÃO GRASP BAIXO ACOPLAMENTO:
D) Alocar as responsabilidades aos módulos de forma que cada módulo reúna elementos que
cumpram um único propósito.
D) Alocar as responsabilidades aos módulos de forma que cada módulo reúna elementos que
cumpram um único propósito.
GABARITO
MÓDULO 3
Descrever as características dos princípios SOLID
PRINCÍPIOS SOLID
Você já ouviu falar em SOLID?
SOLID é um acrônimo para cinco princípios de projeto orientado a objetos, definidos por Robert
C. Martin, que devem ser seguidos para nos ajudar a produzir software com uma estrutura
sólida, isto é, uma estrutura que permita sua evolução e manutenção com menor esforço.
O princípio SRP (do inglês Single Responsibility Principle) estabelece que o módulo deve
ser responsável por um – e apenas um – ator, representando o papel desempenhado por
alguém envolvido com o software. Nesse contexto, um módulo corresponde a um arquivo-
fonte.
EXEMPLO
Suponha que a nossa loja virtual tenha requisitos de busca de produtos por nome e por
departamento, cadastro de um novo produto e exportação de dados dos produtos em formato
CSV. Se pensarmos conforme o padrão Especialista, por exemplo, todas essas funcionalidades
trabalham com os dados dos produtos e, portanto, poderiam ser alocadas ao módulo
ProdutoRepository, conforme ilustrado na figura seguinte.
Imagem: Alexandre Luis Correa
Violação do princípio SRP.
ATENÇÃO
Essa alocação de responsabilidades viola o princípio SRP, pois define um módulo que atende a
diferentes atores.
Pesquisar produtos por nome e por departamento são demandas do cliente da loja.
De acordo com o princípio SRP, devemos separar essas responsabilidades em três módulos, o
que nos daria a flexibilidade de, por exemplo, separar a busca de produtos e a inclusão de
produtos em serviços fisicamente independentes, o que possibilitaria atender diferentes
demandas de escalabilidade de forma mais racional.
EXEMPLO
A busca de produtos pode ter que atender a milhares de clientes simultâneos, enquanto esse
número, no caso da inclusão de produtos, não deve passar de algumas dezenas de usuários.
PRINCÍPIO ABERTO FECHADO (OCP)
O princípio OCP (do inglês Open Closed Principle) estabelece que um módulo deve estar
aberto para extensões, mas fechado para modificações, isto é, seu comportamento deve ser
extensível sem que seja necessário alterá-lo para acomodar as novas extensões.
Você deve estar pensando como é possível fazer com que um módulo estenda seu
comportamento sem que seja necessário mudar uma linha do seu código. A chave para esse
enigma chama-se abstração. Em uma linguagem orientada a objetos, é possível criar uma
interface abstrata que apresente diferentes implementações concretas e, portanto, um novo
comportamento, em conformidade com essa abstração, pode ser adicionado simplesmente
criando-se um módulo.
O exemplo a seguir apresenta um código que não segue o princípio Open Closed. A classe
CalculadoraGeometrica contém operações para calcular a área de triângulos e quadrados.
Imagine que o programa tenha que calcular a área de um círculo, por exemplo.
Não, pois teremos que adicionar uma nova operação obterArea à CalculadoraGeometrica.
Essa é uma forma comum de violação, em que uma classe concentra diversas operações da
mesma natureza que operam sobre tipos diferentes.
Neste caso, a classe CalculadoraGeometrica possui uma operação obterArea, que recebe um
tipo genérico FiguraGeometrica, mas continua concentrando a lógica de cálculo para todos os
tipos de figuras. Para incluir um círculo, precisaríamos adicionar a operação obterAreaCirculo e
modificar a implementação da operação obterArea, inserindo uma nova condição. Imagine
como ficaria o código dessa operação se ela tivesse que calcular a área de cinquenta tipos
diferentes de figuras geométricas!
O que devemos fazer para que o módulo CalculadoraGeometrica possa estar aberto para
trabalhar com novos tipos de figuras sem precisarmos alterar o seu código?
Para isso, definimos que toda figura geométrica é capaz de calcular sua área, sendo que a
fórmula de cálculo de cada figura específica deve ser implementada nas realizações concretas
de FiguraGeometrica.
Dessa forma, para adicionar uma nova figura geométrica, basta adicionar um novo módulo com
uma implementação específica para essa operação.
O princípio LSP (do inglês Liskov Substitution Principle) foi definido em 1988 por Barbara
Liskov e estabelece que um tipo deve poder ser substituído por qualquer um de seus subtipos
sem alterar o correto funcionamento do sistema.
O exemplo seguinte apresenta um caso clássico de violação do princípio de Liskov. Na
geometria, um quadrado pode ser visto como um caso particular de um retângulo. Se levarmos
essa propriedade para a implementação em software, podemos definir uma classe Quadrado
como uma extensão da classe Retangulo.
ATENÇÃO
Note que, como a largura do quadrado é igual ao comprimento, a implementação dos métodos
de acesso (setLargura e setComprimento) do quadrado deve se preocupar em preservar essa
propriedade.
Porém, o próximo exemplo mostra como essa solução viola o princípio da substituição de
Liskov. O método verificarArea da classe ClienteRetangulo recebe um objeto r do tipo
Retangulo.
De acordo com esse princípio, o código desse método deveria funcionar com qualquer objeto
de um tipo derivado de Retangulo. Entretanto, no caso de um objeto do tipo Quadrado, a
execução da chamada setComprimento fará com que os dois atributos do quadrado passem a
ter o valor 10. Em seguida, a execução da chamada setLargura fará com que os dois atributos
do quadrado passem a ter o valor 8 e, portanto, o valor retornado pelo método área definido na
classe Retangulo será 64 e não 80, violando o comportamento esperado para um retângulo.
Esse caso ilustra que, embora um quadrado possa ser classificado como um caso particular de
um retângulo, o princípio de Liskov estabelece que um subtipo não pode estabelecer restrições
adicionais que violem o comportamento genérico do supertipo. Nesse caso, o fato de o
quadrado exigir que comprimento e largura sejam iguais o torna mais restrito que o retângulo.
Portanto, definir a classe Quadrado como um subtipo de Retangulo é uma solução inapropriada
para este problema, do ponto de vista da orientação a objetos, por violar o princípio de Liskov.
O princípio ISP (do inglês Interface Segregation Principle) estabelece que clientes de uma
classe não devem ser forçados a depender de operações que eles não utilizem.
O código do exemplo a seguir apresenta uma interface que viola esse princípio por reunir
operações que dificilmente serão utilizadas em conjunto. As operações login e registrar estão
ligadas ao registro e à autorização de acesso de um usuário, enquanto a operação logErro
registra mensagens de erro ocorridas durante algum processamento, e a operação enviarEmail
permite enviar um e-mail para o usuário logado.
Segundo este princípio, devemos manter a coesão funcional das interfaces, evitando colocar
operações de diferentes naturezas em uma única interface. Aplicando ao exemplo, poderíamos
segregar a interface original em três interfaces (exemplo a seguir):
O princípio DIP (do inglês Dependency Inversion Principle) estabelece que as entidades
concretas devem depender de abstrações e não de outras entidades concretas. Isso é
especialmente importante quando estabelecemos dependências entre entidades de camadas
diferentes do sistema.
Segundo este princípio, um módulo de alto nível não deve depender de um módulo de baixo
nível.
São aqueles ligados estritamente ao domínio da aplicação (ex.: Loja, Produto, ServiçoVenda
etc.).
Módulos de baixo nível
São ligados a alguma tecnologia específica (ex.: acesso a banco de dados, interface com o
usuário etc.) e, portanto, mais voláteis.
Como aspectos tecnológicos são voláteis, devemos isolar a lógica de negócio, permitindo que
a implementação concreta desses aspectos possa variar sem afetar as classes que
implementam a lógica de negócio.
A figura a seguir apresenta o diagrama UML correspondente à nova estrutura da solução após
a aplicação desse princípio. A classe ServicoConsultaProduto depende apenas da interface
ProdutoRepository, enquanto a classe ProdutoRepositoryOracle é uma das possíveis
implementações concretas dessa interface.
VERIFICANDO O APRENDIZADO
1. ASSINALE A AFIRMATIVA QUE EXPRESSA A INTENÇÃO DO PRINCÍPIO
OPEN CLOSED:
A) Módulos clientes de um tipo genérico devem ser capazes de utilizar qualquer especialização
dele, sem precisar conhecer ou se adaptar a qualquer especialização específica.
C) Módulos clientes não devem ser forçados a depender de módulos que trazem elementos
desnecessários ou irrelevantes para as suas necessidades de uso.
D) Um módulo de alto nível não deve depender de uma implementação concreta de nível
inferior. Ambos devem depender de abstrações.
E) Se um módulo possuir elementos que possam ser modificados por razões diferentes para
atender a necessidades de diferentes clientes, ele deve ser decomposto em um ou mais
módulos, de forma que cada um atenda a apenas um cliente.
A) Módulos clientes de um tipo genérico devem ser capazes de utilizar qualquer especialização
dele, sem precisar conhecer ou se adaptar a qualquer especialização específica.
C) Módulos clientes não devem ser forçados a depender de módulos que trazem elementos
desnecessários ou irrelevantes para as suas necessidades de uso.
D) Um módulo de alto nível não deve depender de uma implementação concreta de nível
inferior. Ambos devem depender de abstrações.
E) Se um módulo possuir elementos que possam ser modificados por razões diferentes para
atender a necessidades de diferentes clientes, ele deve ser decomposto em um ou mais
módulos, de forma que cada um atenda a apenas um cliente.
GABARITO
Reconhecer o propósito dos principais padrões GoF e as situações nas quais eles
podem ser aplicados
Este módulo apresenta uma seleção de alguns dos padrões GoF mais utilizados no
desenvolvimento de sistemas.
FACTORY METHOD
Problema
Este padrão define uma interface genérica de criação de um objeto, deixando a decisão da
classe específica a ser instanciada para as implementações concretas dessa interface. Este
padrão é muito utilizado no desenvolvimento de frameworks.
Os frameworks definem pontos de extensão (hot spots) nos quais devem ser feitas
adaptações do código para um sistema específico. Tipicamente, os frameworks definem
classes e interfaces abstratas que devem ser implementadas nas aplicações que os utilizem.
Um exemplo de framework muito usado em Java é o Spring.
FRAMEWORKS
Em aplicações desenvolvidas em Java, por exemplo, podemos criar um objeto de uma classe
simplesmente utilizando o operador new.
COMENTÁRIO
Embora seja uma solução aceitável para pequenos programas, à medida que o sistema cresce,
a quantidade de códigos para criar objetos também cresce, espalhando-se por todo o sistema.
Como a criação de um objeto determina dependência entre a classe onde a instanciação está
sendo feita e a classe instanciada, é preciso muito cuidado para não criar dependências que
dificultem futuras evoluções do sistema.
Solução
A figura a seguir apresenta a estrutura de solução proposta pelo padrão Factory Method.
Os participantes são:
Produto: define o tipo abstrato dos objetos criados pelo padrão. Todos os objetos
concretamente instanciados serão derivados deste tipo.
Criador: declara um Factory Method que define uma interface abstrata que retorna um
objeto genérico do tipo Produto.
Na parte inferior, estão as classes concretas de uma aplicação que utiliza o framework para
gerar um relatório de vendas.
Consequências
ADAPTER
Problema
EXEMPLO
Quando um turista americano chega em um hotel no Brasil, ele provavelmente não conseguirá
colocar o seu carregador de celular na tomada que está na parede do quarto.
Imagem: Shutterstock.com
Como ele não pode mudar a tomada que está na parede, pois ela é propriedade do hotel, a
solução é utilizar um adaptador que converta os pinos do carregador americano em uma saída
de três pinos compatível com as tomadas brasileiras.
Portanto, o software de vendas deve ser capaz de ser plugado a diferentes API de pagamento
que oferecem basicamente o mesmo serviço: intermediar as diversas formas de pagamento
existentes no mercado.
Gostaríamos de poder fazer um software de vendas aderente ao princípio Open Closed, isto é,
um software que pudesse trabalhar com novos fornecedores que apareçam no mercado, sem
termos que mexer em módulos já existentes.
Solução
ATENÇÃO
Note que cada fornecedor possuirá um adaptador específico, da mesma forma que existem
adaptadores específicos para cada padrão de tomada.
A figura a seguir apresenta como este padrão poderia ser aplicado no problema do software
para a loja de departamentos. Suponha que existam dois brokers de pagamento (ABC e
XPTO), sendo as suas respectivas API representadas pelas classes BrokerPagamentoABC e
BrokerPagamentoXPTO. Note que as operações dessas classes, embora similares, têm nomes
diferentes e poderiam também ter parâmetros de chamada e retorno diferentes. Essas classes
são fornecidas pelos fabricantes e não podem ser alteradas.
A aplicação deste padrão consiste em definir uma interface genérica (BrokerPagamento) que
será utilizada em todos os módulos do sistema que precisem interagir com o brokers de
pagamento (representados genericamente pela classe Cliente do diagrama). Para cada API
específica, criamos um adaptador que implementa a interface genérica BrokerPagamento,
traduzindo a chamada da operação genérica pagtoEmCartaoCredito para o protocolo
específico da API. Dessa forma, a operação do AdaptadorBrokerABC chamará a operação
pagarCartaoCredito do BrokerPagamentoABC, enquanto a operação do AdaptadorBrokerXPTO
chamará a operação efetuarPagtoCredito de BrokerPagamentoXPTO.
Consequências
FACADE
Problema
Solução
A figura a seguir mostra como essas dependências entre um elemento de interface com o
usuário e os elementos de negócio e armazenamento podem ser simplificadas pela introdução
da classe ServicoVenda. Ela passa a servir de fachada para a execução de uma operação
complexa do sistema, oferecendo uma interface única e simplificada para a classe GUI Ponto
de Venda. Em vez de chamar várias operações de diferentes objetos, basta ela chamar a
operação registrarItem definida na classe fachada. Dessa forma, os elementos da camada de
interface com o usuário ficam isolados da complexidade de implementação e da estrutura
interna dos objetos pertencentes à lógica de negócio envolvida na operação.
Imagem: Alexandre Luis Correa
Padrão Facade – solução.
Consequências
O padrão Facade é bastante utilizado na estruturação dos serviços lógicos que um sistema
oferece, isolando a camada de interface com o usuário da estrutura da lógica de negócio do
sistema. Portanto, este padrão nada mais é do que a aplicação do princípio da abstração, em
que isolamos um cliente de detalhes irrelevantes de implementação, promovendo uma
estrutura com menor acoplamento entre as camadas.
STRATEGY
Problema
O problema resolvido pelo padrão Strategy ocorre quando temos diferentes algoritmos para
realizar determinada tarefa.
EXEMPLO
Uma loja de departamentos pode ter diferentes políticas de desconto aplicáveis em função da
época do ano (ex.: Natal, Páscoa, Dia das Mães, Dia das Crianças).
Como organizar esses algoritmos de forma que possamos respeitar o princípio Open
Closed, fazendo com que o sistema de vendas possa aplicar novas políticas de desconto
sem que seja necessário modificar o que já está implementado?
Solução
O padrão Strategy define uma família de algoritmos, onde cada um é encapsulado em sua
própria classe. Os diferentes algoritmos compõem uma família que pode ser utilizada de forma
intercambiável, e novos algoritmos podem ser adicionados à família sem afetar o código
existente.
A figura a seguir apresenta a estrutura do padrão. O tipo Estrategia define uma interface
comum a todos os algoritmos, podendo ser implementado como uma classe abstrata ou como
uma interface. Essa interface genérica é utilizada pelo participante Contexto quando este
necessitar que o algoritmo seja executado. Estrategia A, B e C são implementações específicas
do algoritmo genérico que o participante Contexto pode disparar. A estratégia específica deve
ser injetada no módulo Contexto, que pode alimentar a estratégia com seus dados.
Para obter o valor a pagar da Venda, basta obter o seu valor total, somando o valor dos seus
itens, e subtrair o valor retornado pela operação aplicar da instância de PoliticaDesconto,
associada ao objeto venda.
Note que novas políticas podem ser agregadas à solução, bastando adicionar uma nova
implementação da interface PoliticaDesconto. Nenhuma alteração na classe Venda é
necessária, pois ela faz uso apenas da interface genérica, não dependendo de nenhuma
política de descontos específica.
Consequências
TEMPLATE METHOD
Problema
Preparar mistura (via moagem do café) Preparar mistura (via infusão do chá)
Solução
A figura seguinte apresenta uma solução para o problema da máquina de café e da máquina de
chá utilizando este padrão.
B) Fornecer uma interface simples para uma operação complexa do sistema, evitando que o
módulo cliente dessa operação tenha que lidar com diferentes tipos de objetos e chamadas de
operações.
E) Garantir que exista uma e apenas uma instância de uma classe, fornecendo um ponto de
acesso global a essa instância.
B) Fornecer uma interface simples para uma operação complexa do sistema, evitando que o
módulo cliente dessa operação tenha que lidar com diferentes tipos de objetos e chamadas de
operações.
E) Garantir que exista uma e apenas uma instância de uma classe, fornecendo um ponto de
acesso global a essa instância.
GABARITO
A alternativa B corresponde ao padrão Facade, uma vez que esse padrão visa propiciar
uma interface de alto nível simples para as operações oferecidas pelo módulo
representado pela fachada.
CONCLUSÃO
CONSIDERAÇÕES FINAIS
Neste estudo, você viu que os desenvolvedores de software devem produzir sistemas com uma
estrutura adequada que permita a sua evolução com custos e prazos aceitáveis. Padrões de
projeto formam um corpo de conhecimento muito útil, pois permitem a reutilização de boas
soluções de projeto e a produção de sistemas bem estruturados.
Finalmente, você conheceu os principais padrões de projeto GoF e viu como eles promovem o
reuso de soluções para problemas recorrentes em projetos de software.
O assunto é vasto e de grande importância no mercado, sendo o seu conhecimento um
diferencial do profissional de desenvolvimento de sistemas. Um aspecto interessante nessa
área é que, dentro do espírito de reuso de padrões de projeto, a comunidade de
desenvolvimento costuma compartilhar suas experiências de forma colaborativa.
AVALIAÇÃO DO TEMA:
REFERÊNCIAS
BUSCHMANN, F. et al. Pattern-oriented software architecture: a system of patterns. 1. ed.
River Street, NJ: John Wiley & Sons, 2000.
MARTIN, R.C. Clean architecture: a craftsman´s guide to software structure and design. 1. ed.
Upper Saddle River, NJ: Prentice Hall, 2017.
EXPLORE+
Indicamos a leitura dos livros:
Patterns of enterprise application architecture, de Martin Fowler. O livro apresenta
padrões para módulos de interface com o usuário, persistência de dados, lógica de
negócio e integração de sistemas.
Java EE 8 design patterns and best practices, de Rhuan Rocha e João Purificação. O
livro aborda padrões para interface com o usuário, lógica do negócio, integração de
sistemas, orientação a aspectos, programação reativa e microsserviços.
Além de livros-texto, é possível encontrar muitos artigos técnicos por meio de buscas na
internet, usando os acrônimos dos tipos de padrões e princípios abordados no conteúdo (GoF,
GRASP, SOLID) ou os nomes específicos dos padrões e princípios que pretenda utilizar,
aproveitando soluções adotadas com sucesso por outras pessoas.
CONTEUDISTA
Alexandre Luís Correa
CURRÍCULO LATTES