O acoplamento fraco mede o quanto uma classe, depende de, ou está relacionada a, outra classe ou subsistema.A capacidade de uma classe em herdar o comportamento de outra(s) é uma das principais características do paradigma OO. A principal vantagem é poder criar novas classes quase de graça, aproveitando o código de outra. Esse artigo discute esses dois conceitos e mostra porque a herança, em geral, ajuda a comprometer o acoplamento fraco.

Acoplamento fraco

Uma classe com acoplamento forte depende muito (em geral sem necessidade) de outras. Isso pode conduzir aos seguintes problemas [Larman]:

  • Classes difíceis de aproveitar tendo em vista que sempre que esta for utilizada todas as outras das quais ela depende devem estar presentes;
  • Alterações nas classes relacionadas podem forçar mudanças locais e;
  • São difíceis de compreender isoladamente.

Formas comuns de acoplamento ocorrem através de: variáveis de instância, variáveis locais a métodos ou de seus argumentos, chamada de serviços em outra classe, uma classe deriva direta ou indiretamente de outra ou uma classe implementa uma determinada interface. Resumindo, sempre que uma classe referencia um outro tipo em qualquer uma das circunstâncias acima está ocorrendo acoplamento. Considere o código:


public class X
 {
     private ClasseConcretaY var1;
     
     void M1(ClasseConcretaW var2 ) { … }
 }

Existem dois pontos principais de acoplamento, na variável de instância var1, que é do tipo ClasseConcretaY, e no argumento var2, que é do tipo ClasseConcretaW. Nestas duas partes do código a classe X referencia outras duas classes concretas. Isso significa que, sempre que esta classe for utilizada, as outras duas deverão estar disponíveis no espaço de nomes do programa. No caso de Java, o(s) pacote(s) onde estas se encontram deverá(ão) estar no classpath.

Mas, referenciar outras classes sempre causa problemas de acoplamento? A resposta é, depende! Referenciar classes estáveis e disseminadas raramente é um problema. Por exemplo, utilizar o pacote java.util num programa em Java dificilmente causará problemas futuros de acoplamento, uma vez que qualquer ambiente de execução Java contém essa biblioteca. O problema está em classes instáveis, pouco conhecidas, ou seja, nas classes que são criadas para atender os problemas específicos dos projetos.

Como diminuir o acoplamento?

Uma regra geral para diminuir o acoplamento é “programar para uma interface e não para uma implementação” [Gamma]. No exemplo acima isso significa substituir as declarações das classes concretas por declarações de interfaces. Fazendo isso desacopla-se o código de uma implementação específica, tornando-o dependente apenas de uma interface. Essa não é a solução definitiva, um bom projeto com boas atribuições de responsabilidades é crucial, porém ajuda muito. É mais fácil compreender isoladamente uma classe que referencia apenas interfaces e mais [Gamma]:

  • Os clientes (usuários da classe) permanecem sem conhecimento dos tipos específicos dos objetos que eles usam, contanto que os objetos tenham aderência à interface que os clientes esperam;
  • Os clientes permanecem sem conhecimento das classes que implementam estes objetos; eles somente têm conhecimento das classes abstratas que definem a interface.

Herança

A herança é a principal característica de distinção entre um sistema de programação orientado a objeto e outros sistemas de programação. As classes são inseridas em uma hierarquia de especializações de tal forma que uma classe mais especializada (subclasse) herda todas as propriedades da classe mais geral (superclasse) a qual é subordinada na hierarquia.

O principal benefício da herança é o reaproveitamento de código. A herança permite ao programador criar uma nova classe programando somente as diferenças existentes na subclasse em relação à superclasse. Isto se adéqua bem a forma como compreendemos o mundo real, no qual conseguimos identificar naturalmente estas relações.

A reutilização por meio de subclasses é dito “reutilização de caixa branca”, pois usualmente expõe o interior das classes ancestrais para as subclasses. A herança é definida estaticamente em tempo de compilação e é simples de utilizar, uma vez que é suportada diretamente pela linguagem de programação.

Acoplamento fraco x Herança

A decisão de derivação a partir de uma superclasse precisa ser cuidadosamente considerada, uma vez que ela é uma forma muito forte de acoplamento [Larman]. As classes ancestrais freqüentemente definem pelo menos parte da representação física das suas subclasses. A implementação de uma subclasse, desta forma, torna-se tão amarrada à implementação da sua classe mãe que qualquer mudança na implementação desta forçará uma mudança naquela. Vejamos uma situação onde isso é verdadeiro. Considere o esquema de herança abaixo:


 abstract public class X
 {
     private final int MAX = 100;
     
     public int CalculaMaximo(int i) 
     {
        return  i * MAX;
     }
  
     public int UsaMaximo(int i)
     {
         int maximo = CalculaMaximo(i);   
  
         //faz alguma coisa de útil com maximo …
      }
 }
       
 public class Y extends X
 {
    
     public void UsaMetodoDaClasseX()
     {
        ….
        int aux = UsaMaximo(10);
        …
      }    
 }

Até aqui tudo bem! Agora considere a seguinte modificação na classe X:


abstract public class X
 {
     private final int MAX = 100;
     
     public int CalculaMaximo(int  i) 
     {
        return  (int) i * (MAX /100); // Aproxima o resultado com cast
     }
  
     public int UsaMaximo(int i) { … }
 }

Essa alteração na maneira como o máximo está sendo calculado pode gerar efeitos colaterais na classe Y. A aproximação para inteiro pode funcionar para alguns métodos que usam a CalculaMaximo() mas provocar resultados errôneos na UsaMetodoDaClasseX(). Assim, para que a classe Y continue funcionando, esta precisaria ser adaptada à nova realidade. Problemas podem ocorrer também quando estruturas de dados visíveis nas classes derivadas são alteradas. Um array bidimensional transformado para vetor ou mesmo um tipo int modificado para float certamente acarretará problemas.

Este exemplo simples ajuda a mostrar como uma alteração na implementação de um método numa classe base pode provocar anomalias nas suas classes derivadas. Observe que não ocorreu uma alteração de interface, o que necessariamente (e notoriamente) implica em alterações nas classes clientes. É por isso que a herança, em particular, revela um alto acoplamento. Além das hierarquias de classes criadas estarem suscetíveis às mudanças de interface, estão suscetíveis também às alterações nas implementações dos métodos.

Quando uma subclasse redefine algumas, mas não todas as operações, ela também pode afetar as operações que herda, assumindo-se que elas chamam as operações redefinidas [Gamma]. Isso gera um efeito semelhante ao apresentado acima, porém no sentido oposto, onde alterações nas classes mais especializadas podem gerar problemas nas classes base. Se a classe Y tivesse sobrescrito o método CalculaMaximo(), o problema com a aproximação poderia ocorrer no método UsaMaximo() da classe X.

Como evitar a herança?

Uma outra forma de reaproveitar funcionalidade é através da composição de objetos. Novas funcionalidades são obtidas compondo objetos para obter funcionalidades mais complexas. Uma das vantagens desta abordagem é a flexibilidade em poder selecionar em tempo de execução qual objeto será usado na composição (contanto que este respeite a interface definida). No exemplo anterior, se a classe Y tivesse uma referencia para um outro objeto com a funcionalidade definida em X, quando este fosse alterado restaria a ela ainda a possibilidade de utilizar uma versão antiga e seu funcionamento não estaria necessariamente comprometido.

Conclusão

Evitar herança e privilegiar a composição é em geral uma boa prática em projetos orientados a objetos. Favorecer a composição de objetos em relação à herança ajuda a manter cada classe encapsulada e focalizada em uma única tarefa. Suas classes e hierarquias de classes se manterão pequenas e mais tratáveis [Gamma]. Além disso, vimos como e porquê a utilização da herança resulta num alto acoplamento e os problemas que isso pode acarretar.

Bibliografia:
  • [Gamma] Gamma, E; Helm, R; Johnson, R; Vlissides, J. Padrões de Projeto – Soluções Reutilizáveis de Software Orientado a Objetos, Bookman, 2000.
  • [Larman] Larman, C. Utilizando UML e Padrões, Bookman, 2004.

Confira também