Back-End

23 ago, 2012

Pensamento funcional: acoplamento e composição – Parte 01

Publicidade

A programação orientada a objetos torna o código mais compreensível por meio da contenção de partes móveis. A programação funcional torna o código mais compreensível por meio da minimização de partes móveis.
— Michael Feathers, autor de Working with Legacy Code, no Twitter

Trabalhar em uma abstração particular todos os dias faz com que ela se infiltre gradualmente em seu cérebro, influenciando a forma como você soluciona problemas. Um dos objetivos dessa série é ilustrar uma maneira funcional de olhar para problemas típicos. Nesse artigo e no próximo, abordarei a reutilização de código por meio de refatoração e do impacto da abstração do operador.

Um dos objetivos da orientação a objetos é facilitar o encapsulamento e o trabalho com estado. Portanto, suas abstrações tendem a usar o estado para solucionar problemas comuns, implicando o uso de várias classes e interações – o que a citação de Michael Feathers acima chama de “peças móveis”. A programação funcional tenta minimizar peças móveis compondo as peças juntas, em vez de acoplar as estruturas. Esse é um conceito sutil difícil de ver por desenvolvedores cuja experiência principal é com linguagens orientadas a objeto.

Reutilização de código via estrutura

O estilo de programação orientado a objeto imperativo (e especialmente ele) usa estrutura e sistemas de mensagens como blocos de construção. Para reutilizar código orientado a objeto, você extrai o código de destino em outra classe e, a seguir, usa herança para acessá-lo.

Duplicação inadvertida de código

Para ilustrar a reutilização de código e suas implicações, voltarei a uma versão do classificador de números que artigos anteriores usam para ilustrar a estrutura e o estilo de código. O classificador determina se um inteiro positivo é abundante, perfeitoou deficiente. Se a soma dos fatores do número for maior do que duas vezes o número, ele é abundante; se a soma for igual a duas vezes o número, ele é perfeito; caso contrário (se a soma for menor do que duas vezes o número), ele é deficiente.

Também é possível escrever código que usa os fatores de um inteiro positivo para determinar se ele é um número primo (definido como um inteiro maior do que 1 cujos fatores são 1 e o número em si). Como ambos os problemas baseiam-se nos fatores de número, eles são bons candidatos para refatoramento e, portanto, para ilustrar estilos de reutilização de código.

A Listagem 1 mostra o classificador de número escrito em um estilo imperativo:

import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

import static java.lang.Math.sqrt;

public class ClassifierAlpha {
private int number;

public ClassifierAlpha(int number) {
this.number = number;
}

public boolean isFactor(int potential_factor) {
return number % potential_factor == 0;
}

public Set<Integer> factors() {
HashSet<Integer> factors = new HashSet<Integer>();
for (int i = 1; i <= sqrt(number); i++)
if (isFactor(i)) {
factors.add(i);
factors.add(number / i);

}
return factors;
}

static public int sum(Set<Integer> factors) {
Iterator it = factors.iterator();
int sum = 0;
while (it.hasNext())
sum += (Integer) it.next();
return sum;
}

public boolean isPerfect() {
return sum(factors()) – number == number;
}

public boolean isAbundant() {
return sum(factors()) – number > number;
}

public boolean isDeficient() {
return sum(factors()) – number < number;
}

}

Discuto a derivação desse código no primeiro artigo, portanto não o repetirei agora. Sua finalidade aqui é ilustrar a reutilização de código. Isso me leva ao código na Listagem 2, que testa números primos:

import java.util.HashSet;
import java.util.Set;

import static java.lang.Math.sqrt;

public class PrimeAlpha {
private int number;

public PrimeAlpha(int number) {
this.number = number;
}

public boolean isPrime() {
Set<Integer> primeSet = new HashSet<Integer>() {{
add(1); add(number);}};
return number > 1 &&
factors().equals(primeSet);
}

public boolean isFactor(int potential_factor) {
return number % potential_factor == 0;
}

public Set<Integer> factors() {
HashSet<Integer> factors = new HashSet<Integer>();
for (int i = 1; i <= sqrt(number); i++)
if (isFactor(i)) {
factors.add(i);
factors.add(number / i);
}
return factors;
}
}

Alguns itens de destaque aparecem na Listagem 2. O primeiro é a inicialização do código ligeiramente estranha no método isPrime() . Esse é um exemplo de um inicializador de instância. Para saber mais sobre inicialização de instância – uma técnica do Java que é incidental à programação funcional – , consulte “Arquitetura evolucionária e projeto emergente: Aproveitando código reutilizável, Parte 2“.

Os outros itens de interesse na Listagem 2 são o método isFactor() e o método factors() . Note que eles são idênticos às suas contrapartidas na classe ClassifierAlpha (na Listagem 1). Esse é o resultado natural de implementar duas soluções de forma independente e descobrir que você tem virtualmente a mesma funcionalidade.

Refatoração para eliminar duplicação

A solução para esse tipo de duplicação é refatorar o código em uma única classe Factors , que aparece na Listagem 3:

import java.util.Set;
import static java.lang.Math.sqrt;
import java.util.HashSet;

public class FactorsBeta {
protected int number;

public FactorsBeta(int number) {
this.number = number;
}

public boolean isFactor(int potential_factor) {
return number % potential_factor == 0;
}

public Set<Integer> getFactors() {
HashSet<Integer> factors = new HashSet<Integer>();
for (int i = 1; i <= sqrt(number); i++)
if (isFactor(i)) {
factors.add(i);
factors.add(number / i);
}
return factors;
}
}

O código da Listagem 3 é o resultado de usar a refatoração de Extract Superclass . Note que, como ambos os métodos extraídos usam a variável de membro number , ela é levada para a superclasse. Enquanto realizada esse refatoramento, o IDE perguntou se eu gostaria de manipular o acesso (par de acessador, escopo protegido e assim por diante). Escolhi escopo protegido, que adiciona number na classe e cria um construtor para definir seu valor.

Depois de isolar e remover o código duplicado, o classificador de números e o testador de números primos ficam muito mais simples. A Listagem 4 mostra o classificador de números refatorado:

import java.util.Iterator;
import java.util.Set;

public class ClassifierBeta extends FactorsBeta {

public ClassifierBeta(int number) {
super(number);
}

public int sum() {
Iterator it = getFactors().iterator();
int sum = 0;
while (it.hasNext())
sum += (Integer) it.next();
return sum;
}

public boolean isPerfect() {
return sum() – number == number;
}

public boolean isAbundant() {
return sum() – number > number;
}

public boolean isDeficient() {
return sum() – number < number;
}

}

A Listagem 5 mostra o testador de número primo refatorado:

import java.util.HashSet;
import java.util.Set;

public class PrimeBeta extends FactorsBeta {
public PrimeBeta(int number) {
super(number);
}

public boolean isPrime() {
Set<Integer> primeSet = new HashSet<Integer>() {{
add(1); add(number);}};
return getFactors().equals(primeSet);
}
}

Independentemente da opção de acesso que você escolher para o membro number ao refatorar, é preciso lidar com uma rede de classes ao pensar sobre esse problema. Frequentemente, isso é bom, pois permite que você isole partes do problema, mas também tem consequências descendentes ao fazer alterações na classe pai.

Esse é um exemplo de reutilização de código via acoplamento: vincular dois elementos (nesse caso, classes) por meio do estado compartilhado do campo number e do método getFactors() da superclasse. Em outras palavras, isso funciona usando as regras de acoplamento integradas na linguagem. A orientação a objeto define estilos de interação acoplada (como você acessa variáveis de membro por meio de herança, por exemplo), portando há regras predefinidas sobre como as coisas são acopladas — o que é bom, pois é possível argumentar sobre o comportamento de forma consistente. Não me entenda errado — não estou sugerindo que usar herança seja uma má ideia. Em vez disso, estou sugerindo que ela é usada em excesso em linguagens orientadas a objetos, em vez de abstrações alternativas que têm características melhores.

Reutilização de código via composição

No segundo artigo dessa série, apresentei uma versão funcional do classificador de números em Java, mostrado na Listagem 6:

public class FClassifier {

static public boolean isFactor(int number, int potential_factor) {
return number % potential_factor == 0;
}

static public Set<Integer> factors(int number) {
HashSet<Integer> factors = new HashSet<Integer>();
for (int i = 1; i <= sqrt(number); i++)
if (isFactor(number, i)) {
factors.add(i);
factors.add(number / i);
}
return factors;
}

public static int sumOfFactors(int number) {
Iterator<Integer> it = factors(number).iterator();
int sum = 0;
while (it.hasNext())
sum += it.next();
return sum;
}

public static boolean isPerfect(int number) {
return sumOfFactors(number) – number == number;
}

public static boolean isAbundant(int number) {
return sumOfFactors(number) – number > number;
}

public static boolean isDeficient(int number) {
return sumOfFactors(number) – number < number;
}
}

Também tenho uma versão funcional (usando funções puras e nenhum estado compartilhado) do testador de número primo, cujo método isPrime() aparece na Listagem 7. O resto de seu código é idêntico aos métodos de mesmo nome na Listagem 6.

public static boolean isPrime(int number) {
Set<Integer> factors = factors(number);
return number > 1 &&
factors.size() == 2 &&
factors.contains(1) &&
factors.contains(number);
}

Como fiz com as versões imperativas, extraí o código duplicado em sua própria classe Factors , alterando o nome do método factors para of para facilitar a leitura, como mostra a Listagem 8:

import java.util.HashSet;
import java.util.Set;
import static java.lang.Math.sqrt;

public class Factors {
static public boolean isFactor(int number, int potential_factor) {
return number % potential_factor == 0;
}

static public Set<Integer> of(int number) {
HashSet<Integer> factors = new HashSet<Integer>();
for (int i = 1; i <= sqrt(number); i++)
if (isFactor(number, i)) {
factors.add(i);
factors.add(number / i);
}
return factors;
}
}

Como o estado na versão funcional é passado como parâmetro, nenhum estado compartilhado vem com a extração. Depois de extrair essa classe, posso refatorar o classificador e o testador de número primo funcionais para usá-la. A Listagem 9 mostra o classificador de números refatorado:

public class FClassifier {

public static int sumOfFactors(int number) {
Iterator<Integer> it = Factors.of(number).iterator();
int sum = 0;
while (it.hasNext())
sum += it.next();
return sum;
}

public static boolean isPerfect(int number) {
return sumOfFactors(number) – number == number;
}

public static boolean isAbundant(int number) {
return sumOfFactors(number) – number > number;
}

public static boolean isDeficient(int number) {
return sumOfFactors(number) – number < number;
}
}

A Listagem 10 mostra o testador de número primo refatorado:

import java.util.Set;

public class FPrime {

public static boolean isPrime(int number) {
Set<Integer> factors = Factors.of(number);
return number > 1 &&
factors.size() == 2 &&
factors.contains(1) &&
factors.contains(number);
}
}

Note que não usei quaisquer bibliotecas ou linguagens especiais para tornar a segunda versão mais funcional. Mas eu o fiz usando composição em vez de acoplamento para a reutilização de código. Tanto a Listagem 9 quanto a A Listagem 10 usa a classe Factors , mas seu uso é inteiramente contido dentro dos métodos individuais.

A distinção entre acoplamento e composição é sutil, mas importante. Em um exemplo simples como esse, é possível ver o esqueleto da estrutura do código aparecendo. No entanto, ao refatorar uma base de código grande, o acoplamento é exibido em todos os lugares, pois é um dos mecanismos de reutilização em linguagens orientadas a objeto. A dificuldade de entender estruturas exuberantemente acopladas prejudicou a reutilização em linguagens orientadas a objetos, limitando a reutilização efetiva a domínios técnicos bem definidos, como o mapeamento relacional de objetos e bibliotecas de widgets. O mesmo nível de reutilização nos ilude ao escrever código Java menos estruturado de forma óbvia (como o código que é escrito em aplicativos de negócios).

Você poderia ter tornado a versão imperativa melhor observando o que o IDE oferece durante a refatoração, recusando educadamente e, em vez disso, usando composição.

Conclusão

Pensar como um programador mais funcional significa pensar de forma diferente sobre todos os aspectos da codificação. A reutilização de código é uma meta óbvia do desenvolvimento e as abstrações imperativas tendem a solucionar esse problema de forma diferente da forma como os programadores funcionais o solucionam. Esse artigo comparou os dois estilos de reutilização de código: acoplamento via herança e composição via parâmetros. O próximo artigo continuará a explorar essa importante divisão.

***

O IBM Forms fornece uma solução de formulários eletrônicos com área de cobertura zero para ajudá-lo a automatizar e retirar processos de negócio com base em formulários do desktop e movê-los para a Web, onde se integram com suas operações gerais de negócios. Ele fornece uma experiência do usuário atraente e interativa, custo total de propriedade menor e automação de processos aprimorada.

Recursos

Aprender

Obter produtos e tecnologias

Discutir

Participe da comunidade do developerWorks. Entre em contato com outros usuários do developerWorks, enquanto explora os blogs, fóruns, grupos e wikis orientados ao desenvolvedor.

***

Sobre o autor: Neal Ford é um arquiteto de software e Meme Wrangler, na ThoughtWorks, uma consultoria global de TI. Projeta e desenvolve aplicativos, materiais de instrução, artigos para revistas, treinamentos e apresentações em vídeo/DVD, e é autor ou editor de livros que abordam uma variedade de tecnologias, inclusive The Productive Programmer Seu enfoque é o projeto e construção de aplicativos corporativos de grande porte. Também é orador internacionalmente aclamado nas conferências de desenvolvedores ao redor do mundo. Conheça seu Web site.

***

Artigo original disponível em: http://www.ibm.com/developerworks/br/java/library/j-ft5/index.html