Na primeira parte desse artigo, eu comecei investigando o cruzamento dos padrões de design tradicionais Gang of Four GoF (consultar “Recursos”) e abordagens mais funcionais. Eu continuo esta jornada neste capítulo, mostrando a solução para um problema comum em três diferentes paradigmas: padrões, metaprogramação e composição funcional.
Se o paradigma principal que sua linguagem suporta for objetos, é fácil começar a pensar sobre soluções para cada problema nesses termos. No entanto, as linguagens mais modernas são as de multiparadigmas, e isso significa que elas suportam objetos, metaobjetos, funcional e outros paradigmas. Aprender como usar diferentes paradigmas para os problemas adequados é parte da evolução voltada para ser um melhor desenvolvedor.
Neste capítulo, vou lidar com um problema tradicional solucionado pelo padrão de design de Adaptador: convertendo uma interface para trabalhar com um modelo incompatível. Primeiramente, a abordagem tradicional, composta em Java.
Adaptador em Java
O padrão do Adaptador converte uma interface designada para uma classe em uma interface compatível. Isso é utilizado quando duas classes podem, teoricamente, trabalhar juntas, mas isso não ocorre devido a detalhes de implementação. Para esse exemplo, foram criadas algumas classes simples que modelam o problema de encaixe de um pino quadrado em um buraco redondo. Algumas vezes, um pino quadrado se encaixará em um buraco redondo, como ilustrado na Figura 1, dependendo dos tamanhos relativos do pino e do buraco:
Figura 1. Um pino quadrado em um buraco redondo.
Para determinar se o quadrado se encaixará em um círculo, utilizei a fórmula mostrada na Figura 2:
Figura 2. Fórmula para determinar se um quadrado se encaixará em um círculo.
A fórmula da Figura 2 pega a largura de um dos lados do quadrado dividida por 2, enquadra-a, e multiplica o resultado por 2. Se este valor for menor que o raio do círculo, o pino se encaixará.
Eu poderia solucionar o problema do pino quadrado/ buraco redondo, de maneira superficial, com uma classe simples do utilitário que lida com conversões. Mas isso exemplifica um problema ainda maior. Por exemplo, e se eu fosse ajustar um botão para caber em um tipo de Painel que não tivesse sido projetado para ele? O problema de pinos quadrados e buracos redondos é uma simplificação conveniente do problema geral abordado pelo padrão de design de Adaptador: o ajuste de duas interfaces incompatíveis. Para permitir que pinos quadrados trabalhem com buracos redondos, preciso de algumas classes e interfaces para implementar o padrão de Adaptador, como mostra a Listagem 1:
public class SquarePeg { private int width;
public SquarePeg(int width) { this.width = width; }
public int getWidth() { return width; } }
public interface Circularity { public double getRadius(); }
public class RoundPeg implements Circularity { private double radius;
public double getRadius() { return radius; }
public RoundPeg(int radius) { this.radius = radius; } }
public class RoundHole { private double radius;
public RoundHole(double radius) { this.radius = radius; }
public boolean pegFits(Circularity peg) { return peg.getRadius() <= radius; }
}
Listagem 1. Pinos quadrados e buracos redondos em Java.
Para reduzir a quantidade de códigos Java, adicionei uma interface nomeada comoCircularity para indicar que o implementador possui um raio. Isso me permite escrever o código RoundHole em termos de objetos redondos, não apenas RoundPeg s. Esta é uma concessão comum no padrão do Adaptador para facilitar a resolução de tipo.
Para ajustar pinos quadrados em buracos redondos, preciso de um adaptador que adicione Circularity a SquarePeg s ao expor um método getRadius(), como mostra a Listagem 2:
public class SquarePegAdaptor implementa Circularity { private SquarePeg peg;
public SquarePegAdaptor(SquarePeg peg) { this.peg = peg; }
public double getRadius() { return Math.sqrt(Math.pow((peg.getWidth()/2), 2) * 2); } }
Listagem 2. Adaptador de pino quadrado.
Para testar que meu adaptador realmente permite encaixar adequadamente pinos quadrados em buracos redondos, implementei o teste exibido na Listagem 3:
@Test
public void square_pegs_in_round_holes() {
RoundHole hole = new RoundHole(4.0);
Circularity peg;
for (int i = 3; i <= 10; i++) {
peg = new SquarePegAdaptor(new SquarePeg(i));
if (i < 6)
assertTrue(hole.pegFits(peg));
else
assertFalse(hole.pegFits(peg));
}
}
Listagem 3. Testando adaptação.
Na Listagem 3, para cada largura proposta, envolvo o SquarePegAdaptor em torno da criação do SquarePeg, ativando o método hole’s pegFits() para retornar uma avaliação inteligente como a de encaixe do pino.
Este código é direto, pois trata-se de um padrão simples, embora seja prolixo, para implementar em Java. Está claro que se trata de um paradigma de abordagem de design padrão GoF. No entanto, a abordagem padrão não é o único caminho.
Adaptador dinâmico em Groovy
O Groovy (consulte “Recursos”) suporta diversos paradigmas de programação que o Java não suporta, portanto, ele será utilizado para o restante dos exemplos. Primeiramente, implementarei a solução “padrão” de padrão de Adaptador da Listagem 2 conforme transferida para Groovy, como mostra a Listagem 4:
class SquarePeg { def width }
class RoundPeg { def radius }
class RoundHole { def radius
def pegFits(peg) { peg.radius <= radius } }
class SquarePegAdapter { def peg
def getRadius() { Math.sqrt(((peg.width/2) ** 2)*2) } }
Listagem 4. Pinos, buracos e adaptadores em Groovy.
A diferença mais notável entre a versão Java da Listagem 2 e a versão Groovy da Listagem 4 é a prolixidade. O Groovy foi projetado para eliminar algumas repetições de Java através de tipos dinâmicos e conveniências como permitir que a última linha do método sirva, automaticamente, de valor de retorno para o método, como mostra o getRadius().
O teste para a versão Groovy do adaptador aparece na Listagem 5:
@Test void pegs_and_holes() {
def hole = new RoundHole(radius:4.0)
(4..7).each { w ->
def peg = new SquarePegAdapter(
peg:new SquarePeg(width:w))
if (w < 6 )
assertTrue hole.pegFits(peg)
else
assertFalse hole.pegFits(peg)
}
}
Listagem 5. Testando adaptador tradicional em Groovy.
Na Listagem 5, aproveitei outra conveniência do Groovy, chamar o construtor de nome/valor que o Groovy gerou automaticamente quando construí o RoundHole, o SquarePegAdaptor e o SquarePeg.
Apesar da sintaxe, esta versão é exatamente como a versão Java e segue o paradigma de design padrão GoF. É comum que desenvolvedores Groovy provenientes de uma base Java tragam sua experiência anterior para uma nova sintaxe. No entanto, o Groovy possui meios mais elegantes para solucionar este problema usando metaprogramação.
Usando metaprogramação para adaptação
Um dos importantes recursos do Groovy é o seu suporte eficiente para metaprogramação. Usarei isso para construir o adaptador diretamente na classe através do ExpandoMetaClass.
ExpandoMetaClass
Um recurso comum de linguagens dinâmicas são as classes abertas: a habilidade de reabrir classes existentes (suas ou de classes de sistema como String ou Object) para adicionar, eliminar ou alterar métodos. As classes abertas, geralmente, são utilizadas em linguagens de domínio específico (DLSs) e para o desenvolvimento de interfaces fluentes. O Groovy possui dois mecanismos para classes abertas: categorias e ExpandoMetaClass. Meu exemplo mostra apenas a sintaxe de expando.
O ExpandoMetaClass permite que você adicione novos métodos às classes ou instâncias de objeto individual. Nesse caso de adaptação, eu preciso adicionar raio a meu SquarePeg antes que eu possa verificar se isso se encaixará em um buraco redondo, como mostra a Listagem 6:
static {
SquarePeg.metaClass.getRadius = { ->
Math.sqrt(((delegate.width/2) ** 2)*2)
}
}
@Test void expando_adapter() {
def hole = new RoundHole(radius:4.0)
(4..7).each { w ->
def peg = new SquarePeg(width:w)
if (w < 6)
assertTrue hole.pegFits(peg)
else
assertFalse hole.pegFits(peg)
}
}
Listagem 6. Usando ExpandoMetaClass para adicionar raio a pinos quadrados.
Todas as classes em Groovy possuem uma propriedade metaClass predefinida, expondo este ExpandoMetaClass da classe. Na Listagem 6, utilizo essa propriedade para adicionar um método getRadius(), usando a conhecida fórmula para a classe SquarePeg. A sincronização é importante quando você usa ExpandoMetaClass; devo garantir que o método foi adicionado antes de tentar invocá-lo no teste de unidade. Dessa forma, adiciono o novo método no inicializador estático da classe de teste, que adiciona o método ao SquarePeg conforme a classe de teste é carregada. Após o método getRadius() ser adicionado ao SquarePeg, posso transferi-lo para o método hole.pegFits, e o tipo dinâmico do Groovy cuida do restante.
Usar o ExpandoMetaClass é certamente mais eficiente que a versão mais prolixa do padrão. E isso é praticamente imperceptível – o que também é uma de suas desvantagens. A adição de métodos de atacado às classes existentes deve ser feita com cuidado porque você troca conveniência por uma conduta imperceptível, o que pode ser difícil de depurar. Isso é aceitável em alguns casos, como em DSLs e para alterações disseminadas à infraestrutura existente a favor de estruturas.
Este exemplo mostra a utilização do paradigma de metaprogramação – modificando a classe existente – para solucionar o problema de adaptador. No entanto, essa não é a única maneira de usar o dinamismo do Groovy para solucionar este problema.
Adaptadores dinâmicos
O Groovy foi otimizado para se integrar bem com o Java, incluindo locais onde o Java é relativamente rígido. Por exemplo, a geração dinâmica de classes é complicada em Java, mas o Groovy lida com isso facilmente. Isso sugere que posso gerar uma classe de adaptador dinamicamente, como mostra a Listagem 7:
def roundPegOf(squarePeg) {
[getRadius:{Math.sqrt(
((squarePeg.width/2) ** 2)*2)}] as RoundThing
}
@Test void functional_adaptor() {
def hole = new RoundHole(radius:4.0)
(4..7).each { w ->
def peg = roundPegOf(new SquarePeg(width:w))
if (w < 6)
assertTrue hole.pegFits(peg)
else
assertFalse hole.pegFits(peg)
}
}
Listagem 7. Usando adaptadores dinâmicos.
A sintaxe literal de hash do Groovy utiliza chaves quadradas, o que aparece no método roundPegOf() da Listagem 7. Para gerar uma classe que implementa uma interface, o Groovy permite que você crie um hash com nomes de métodos como as chaves e os blocos de códigos de implementação para os valores. O operador as utiliza o hash para gerar uma classe que implemente a interface, usando os nomes das chaves de hashs para gerar métodos de instância. Dessa forma, naListagem 7, o método roundPegOf() cria um hash de entrada única getRadius conforme o nome do método (não é necessário usar chaves de hash do Groovy entre aspas duplas quando elas estão em sequências) e meu código de conversão conhecido conforme a implementação. O operador as converte isso em uma classe que implementa a interface RoundThing , atuando como o adaptador em torno da criação do SquarePeg no teste functional_adaptor().
A habilidade de gerar uma classe de maneira dinâmica elimina grande parte da prolixidade e formalidade da abordagem de padrões tradicionais. E também é mais explícita que a abordagem de metaprogramação: não estou adicionando novos métodos à classe; estou gerando um wrapper no momento exato para propósitos de adaptação. Essa abordagem utiliza o paradigma de design padrão (adicionar uma classe de adaptador), mas com o mínimo de confusão e sintaxe.
Adaptadores funcionais
“Quando tudo o que você tem é um martelo, todos os problemas parecem um prego”. Se o seu único paradigma é a orientação do objeto, você pode perder a capacidade de visualizar possibilidades alternativas. Um dos perigos de investir muito tempo em linguagens sem funções de alto nível é a aplicação de padrões em excesso para solucionar problemas. Muitos padrões (Observer, Visitor e Command para citar alguns exemplos) estão no centro dos mecanismos para aplicação de código móvel, implementados em linguagens que carecem de funções de alto nível. Posso descartar grande parte da captura de objetos e apenas escrever uma função para administrar a conversão. E assim, constata-se que esta abordagem tem algumas vantagens.
Funções
Se você possui funções de alto nível (funções que podem ser exibidas em qualquer lugar e que qualquer outra linguagem pode exibir, incluindo as classes externas), você pode escrever uma função de conversão que administre a adaptação para você, como mostra o código Groovy na Listagem 8:
def pegFits(peg, hole) {
Math.sqrt(((peg.width/2) ** 2)*2) <= hole.radius
}
@Test void functional_all_the_way() {
def hole = new RoundHole(radius:4.0)
(4..7).each { w ->
def peg = new SquarePeg(width:w)
if (w < 6)
assertTrue pegFits(peg, hole)
else
assertFalse pegFits(peg, hole)
}
}
Listagem 8. Usando uma função simples de conversão.
Na Listagem 8, crio uma função que aceita um pino e um buraco, e a utilizo para verificar o encaixe do pino. A abordagem funciona, porém, elimina a decisão sobre ajuste do buraco, onde a orientação do objeto o considerou ajustável. Em alguns casos, isso pode fazer sentido para externalizar essa decisão ao invés de adaptar a classe. Isso representa o paradigma funcional: funções puras que aceitam parâmetros e retornam resultados.
Composição
Antes de deixar a abordagem funcional, mostrarei meu adaptador favorito, que une o design de padrão e as abordagens funcionais. Para ilustrar a vantagem do uso dos geradores dinâmicos leves, oferecidos como funções de alto nível, considere o exemplo na Listagem 9:
def pegFits(peg, hole) {
Math.sqrt(((peg.width/2) ** 2)*2) <= hole.radius
}
@Test void functional_all_the_way() {
def hole = new RoundHole(radius:4.0)
(4..7).each { w ->
def peg = new SquarePeg(width:w)
if (w < 6)
assertTrue pegFits(peg, hole)
else
assertFalse pegFits(peg, hole)
}
}
Listagem 9. Compondo funções através de adaptadores dinâmicos leves.
Na Listagem 9, crio algumas funções que retornam adaptadores dinâmicos que me permitem encadear todos os adaptadores de maneira conveniente e legível. A composição de funções permite controlar e encapsular o que acontece com seus parâmetros sem se preocupar com quem poderia usá-los como parâmetro. Esta é uma abordagem bastante funcional que utiliza a habilidade do Groovy para criar classes de wrapper dinâmicas como a implementação.
Compare a abordagem de adaptador dinâmico leve com a versão complexa de composição de adaptador das bibliotecas de E/S Java, como mostra a Listagem 10:
ZipInputStream zis =
new ZipInputStream(
new BufferedInputStream(
new FileInputStream(argv[0])));
Listagem 10. Composição complexa de adaptador.
O exemplo na Listagem 10 mostra um problema comum que o adaptador enfrenta: a habilidade de selecionar e combinar condutas compostas. Devido à carência de funções de alto nível, o Java é forçado a fazer a composição através de construtores. Usar funções para capturar outras funções e modificar seu retorno é comum na programação, menos em Java, porque a linguagem gera conflitos no formato de sintaxe em excesso.
Conclusão
Se você sempre se prende ao mesmo paradigma, torna-se difícil visualizar os benefícios das abordagens alternativas, porque isso não se ajusta à sua visualização de mundo. Linguagens modernas com paradigmas mistos que oferecem uma variedade de opções de design, e entendimento sobre como cada paradigma funciona (e interage com outros paradigmas) ajudam você a optar por soluções melhores. Neste capítulo, mostrei o problema comum de adaptabilidade e solucionei-o através do padrão de design de adaptador tradicional em Java e Groovy. Em seguida, solucionei o problema utilizando metaprogramação Groovy e o ExpandoMetaClass, e por fim, mostrei as classes de adaptador dinâmico. Você também pode constatar que possuir uma sintaxe leve para as classes de adaptador permite a composição de funções convenientes, o que é complexo em Java.
No próximo capítulo, continuo a exploração do cruzamento de padrões de design e programação funcional.
Recursos
Aprender
- The Productive Programmer (Neal Ford, O’Reilly Media, 2008): O livro mais recente de Neal Ford aborda as ferramentas e práticas que ajudam você a melhorar sua eficiência em codificação.
- Design Patterns: Elements of Reusable Object-Oriented Software (Erich Gamma et al., Addison-Wesley, 1994): O clássico trabalho do Gang of Four sobre padrões de design.
- Design Patterns in Dynamic Languages: A apresentação de Peter Norvig mostra um caso em que as linguagens de grande eficácia (como as linguagens funcionais) têm menos necessidade de padrões de design.
- Padrão de adaptador: O adaptador é um popular padrão de design Gang of Four.
- Groovy: Groovy é uma linguagem dinâmica do JVM com metaprogramação eficiente e construção funcional.
- “Practically Groovy: Metaprogramming with closures, ExpandoMetaClass, and categories“: Leia mais sobre a habilidade Groovy para adicionar novos métodos às classes de maneira dinâmica em tempo de execução.
- Navegue nalivraria de tecnologia e busque por livros sobre estes e outros assuntos técnicos.
- Área de tecnologia developerWorks: Encontre centenas de artigos sobre cada aspecto da programação Java.
Obter produtos e tecnologias
- Avalie produtos IBM da melhor maneira para você: Faça download de produtos, avalie um produto online, use um produto no ambiente da nuvem ou passe algumas horas na SOA Sandbox para saber como implementar arquitetura orientada a serviço (SOA) de maneira eficiente.
Discutir
- Participe da comunidade developerWorks. Conecte-se com outros usuários do developerWorks ao mesmo tempo que explora blogs, fóruns, grupos e wikis orientados a desenvolvedor.
***
Sobre o autor: Neal Ford é arquiteto de software e Meme Wrangler na ThoughtWorks, consultoria de TI global. Ele também projeta e desenvolve aplicativos, materiais educativos, artigos de revistas, software educacional e apresentações em vídeo/ DVD, além de ser autor e editor de livros que abordam uma variedade de tecnologias, incluindo o mais recenteThe Productive Programmer. Sua ênfase é na elaboração e no desenvolvimento de aplicativos corporativos de larga escala. Ele também é palestrante admirado em conferências de desenvolvedores no mundo todo. Confira seu website.







