quarta-feira, 5 de fevereiro de 2014

Dependência entre pacotes

Muito se pode dizer sobre a Arquitetura de um software, apenas estudando os relacionamentos de dependência entre seus Pacotes. Continuando nosso "papo" sobre Arquitetura, vamos estudar um pouco os tipos de dependências entre Pacotes e seu reflexo na qualidade de um software.




Pacotes?

Já falamos sobre pacotes em vários artigos aqui. Se você tem dúvida sobre o conceito, eu recomendo a leitura de "Camadas x Pacotes", e também pode consultar outras fontes, como "http://pt.wikipedia.org/wiki/Pacote", ou mesmo "http://pt.wikipedia.org/wiki/Diagrama_de_pacotes".

Dependência?

Um relacionamento de dependência pode existir entre dois elementos (Classes ou Interfaces) ou entre Pacotes, o que acontece quando um elemento de um pacote faz referência a um elemento de outro pacote. Esta referência pode ser:

  • Instanciar uma Classe de outro Pacote;
  • Invocar um método estático de uma Classe de outro Pacote;
  • Definir uma variável ou um parâmetro de método com um tipo definido em outro Pacote;
A dependência pode ser mais forte ou mais fraca. Dependências de compilação são mais fortes, e, como o nome diz, exigem a presença do elemento alvo (a classe ou interface da qual dependem), no caminho de construção (build path) para que possam ser compiladas. Este seria um exemplo: 

...
    MinhaClasse xpto = new MinhaClasse;
...

Se a Classe "MinhaClasse" não estiver no "build path", não conseguiremos compilar esse código. Agora, temos a dependência fraca, também chamada de "Runtime dependency", que não exige a presença do elemento alvo no "build path" para compilação, porém, pode causar erros, caso ele não esteja presente no "build path" no momento da execução do código. Veja um exemplo: 

package com.obomprogramador.teste;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class RuntimeClient {

   public void test() {
      try {
        Class clazz = Class.forName("com.obomprogramador.teste.RuntimeDep");
        Object obj1 = clazz.newInstance();
        Method metodo = obj1.getClass().getMethod("calcular");
        metodo.invoke(obj1);
  } catch (ClassNotFoundException e) {
   e.printStackTrace();
  } catch (InstantiationException e) {
   e.printStackTrace();
  } catch (IllegalAccessException e) {
   e.printStackTrace();
  } catch (NoSuchMethodException e) {
   e.printStackTrace();
  } catch (SecurityException e) {
   e.printStackTrace();
  } catch (IllegalArgumentException e) {
   e.printStackTrace();
  } catch (InvocationTargetException e) {
   e.printStackTrace();
  }
 }
}

Neste exemplo, podemos compilar nossa classe "RuntimeClient" sem problemas, mesmo que a classe "com.obomprogramador.teste.RuntimeDep" não esteja no "build path". Porém, ao tentarmos executar, vamos tomar uma "Exception" do tipo "ClassNotFoundException", caso a classe não esteja no "build path".

Manutenibilidade, testabilidade e flexibilidade

Como eu já disse aqui ("O nirvana do bom código fonte"), existem três capacidades que são afetadas diretamente pela maneira como os Pacotes estão relacionados:

  • Manutenibilidade: A facilidade em realizar manutenções corretivas ou evolutivas em determinado código fonte;
  • Testabilidade: A facilidade em escrever testes abrangentes (aumentar a Cobertura de testes) em um determinado código fonte;
  • Flexibilidade: A facilidade em adaptar um software às mudanças nos requisitos ou às novas funcionalidades;

O que prejudica estas três capacidades? Certamente, a Complexidade excessiva do código fonte pode comprometer as três. Porém, existem mais problemas, por exemplo, o alto Acoplamento entre seus elementos, geralmente causado por baixa Coesão ou por Complexidade Acidental, pode causar o efeito de "Brittleness", que é a propagação de alterações.

Propagação de alterações é quando temos que mexer em uma pequena funcionalidade, digamos, de apenas uma única classe, porém, somos obrigados a alterar vários elementos em função disto.

A necessidade de estudar bem as dependências entre pacotes é exatamente para melhorar estes três indicadores, fundamentais para reduzir a Dívida Técnica de um software.

Acoplamentos

Acoplamento é o termo que designa uma dependência entre pacotes. Temos dois tipos:

  • Acoplamentos Aferentes (Ca ou "Afferent Couplings"): É a quantidade de outros pacotes que dependem das classes de determinado pacote. Indica a responsabilidade do pacote perante o Software, como um todo;
  • Acoplamentos Eferentes (Ce ou "Efferent Couplings"): É a quantidade de outros pacotes, que as classes de determinado pacote dependem. Indica a independência de determinado pacote, com relação ao resto do Software;


É muito importante analisarmos com calma a questão dos acoplamentos. O que significa? Significa que um pacote com Ca muito alto (eu sei, é meio "fuzzy logic") tem uma responsabilidade muito grande, talvez até mesmo múltiplas responsabilidades, e pode ser uma grande fonte de riscos.

Um pacote com Ce muito alto indica que é muito dependente de outros pacotes, logo, pode ser mais sujeito à propagação de alterações, o que aumenta o risco de manutenção do Software.

Instabilidade

É a vulnerabilidade de um pacote com relação à propagação de alterações. É uma métrica proposta por Robert C. Martin, e é calculada segundo a fórmula: I = Ce / (Ce + Ca). Um pacote com valor de instabilidade próximo a Zero significa que pouco instável, ou seja, mais estável, já, um pacote com valor próximo a Um significa que é muito instável, ou seja, muito sujeito à alterações.

Abstrações

Abstração é a representação de um comportamento através de um elemento abstrato, seja uma Classe Abstrata ou uma Interface. Implementação é a criação de uma Classe Concreta (instanciável) a partir de uma Abstração.

Eis um exemplo de abstração:

public interface Veiculo {
   boolean ligar();
}

E eis um exemplo de implementação:

public class Carro implements Veiculo {
   public boolean ligar() {
      ...
   }
}

O uso de Abstrações, especialmente nas dependências entre pacotes, aumenta a manutenibilidade e flexibilidade do software, nos permitindo Injetar classes concretas sem necessidade de alteração de código fonte.

O ideal é termos "Camadas de abstração" em nosso software, de tal forma que isolem ao máximo possível a propagação de alterações. Se desejarmos substituir uma Implementação, podemos fazê-lo apenas substituindo a classe Concreta (a implementação) a ser utilizada, sem necessidade de alteração de código fonte.

Existem alguns princípios de projeto Orientado a Objetos que definem isso:

Um pacote que não contenha Abstrações é considerado mais Concreto. 

Princípios aplicados ao projeto de pacotes

A Arquitetura de um software visa aumentar suas "qualidades" (ou "capacidades"), ou seja, o grau em que atendem aos "Requisitos não funcionais" de um projeto.

E existem alguns princípios que se aplicam ao projeto de pacotes, de modo a garantir que a Arquitetura seja adequada. 

Além dos três princípios que falamos anteriormente, Reuse Release Equivalence Principle (REP), Common Closure Principle (CCP) e Common Reuse Principle (CRP) (veja "Camadas e Pacotes"), temos alguns outros a estudar: 

Acyclic Dependencies Principle - ADP (Princípio das Dependências Acíclicas): A dependência entre pacotes não pode conter ciclos. 


Dependência circular


Dependência cíclica

Stable Dependencies Principle - SDP (Princípio das Dependências Estáveis): A dependência entre pacotes deve caminhar sempre da direção da estabilidade. Um pacote somente deve depender de pacotes mais estáveis do que ele. 



A dependência que o "Pacote 1" tem do "Pacote 2" é inadequada, pois o "Pacote 2" é mais instável que o próprio "Pacote 1", e isto pode provocar propagação de alterações. 

Stable Abstractions Principle - SAP (Princípio das Abstrações Estáveis): Pacotes que são totalmente estáveis, devem ser totalmente abstratos, e pacotes que são totalmente instáveis, devem ser totalmente concretos. 



Os pacotes 5 e 6 são totalmente estáveis, logo, devem ser totalmente compostos por Abstrações.

Estes três princípios melhoram a Manutenibilidade, Testabilidade e Flexibilidade do software, aumentando ao máximo sua qualidade geral. Quando dependemos de Abstrações e estas são estáveis, podemos evitar a propagação de alterações no software.

Medindo a adequação da Arquitetura

Uma métrica muito importante, porém muito desprezada pelos Arquitetos de software é a "Distance from Main Sequence" - DMS (Distância da Sequência Principal), que mede o quanto um Pacote se afasta dos três princípios que citamos.

Existe uma função linear que representa o equilíbrio entre Abstração e Instabilidade: A + I = 1. Ferramentas como o JDepend, nos permitem medir nossos pacotes e saber o quanto estão próximos ou afastados desta linha ideal. Um pacote com valor 1 significa que está muito afastado deste ideal, logo, deveria ser analisado para possível refatoração.