Pacotes que são maximamente ESTÁVEIS devem ser maximamente ABSTRATOS. PACOTES instáveis DEVEM SER CONCRETOS. A abstração de um pacote deve ser PROPORCIONAL a sua estabilidade.
PÓS-GRADUAÇÃO EM ENGENHARIA DE SOFTWARES COM ÊNFASE EM MÉTODOS ÁGEIS
1. UNA – PÓS GRADUAÇÃO EM
ENGENHARIA DE SOFTWARES COM
ÊNFASE EM MÉTODOS ÁGEIS.
Cláudio Xavier
e
Samuel Lopes
SAP - Stablility and Abstract Principle
Princípio da Abstração e Estabilidade.
Belo Horizonte
17-06-2011
2. Introdução
Este princípio é mais apropriado para aplicações que excedem 50.000 linhas de código e exige uma equipe
de engenheiros para escrever. Este artigo descreve um conjunto de princípios e métricas que podem ser
usados para medir a qualidade de um design (projeto) orientado a objeto em termos de interdependência
entre os pacotes.
Projetos que são altamente interdependentes tendem a ser rígidos, difícil de manter e sem reusabilidade.
No entanto, a interdependência é necessária se os pacotes do projeto estão colaborativos. Logo, algumas
formas de dependência são desejáveis, e outras indesejáveis.
Os princípios e os parâmetros analisados no presente artigo tem a ver com estabilidade.
Resumindo estabilidade é o núcleo de todo projeto de software, devemos trabalhar para tornar um projeto
estável na presença de mudanças. Esse objetivo tem a ver com o princípio Aberto (para espanção) e
Fechado (para modificação) (OCP).
Nesse artigo veremos o conceito de impácto da estabilidade das relações entre os pacotes em larga escala
de um aplicativo.
Exemplo de mudança no cliente com sóftware interdependente.
Usuários e gerentes são incapazes de prever quando vai haver uma mudança. Uma simples mudança em
uma parte do pedido pode provocar falhas em outras partes que parecem ser completamente
independentes. Corrigindo esses problemas podem surgir ainda mais problemas, e o processo de
manutenção começa a se assemelhar a um cachorro correndo atrás do rabo.
É difícil reutilizar um projeto que é altamente interdependente. Por isso os desenvolvedores se assustam
com a quantidade de trabalho para separar uma parte indesejável do projeto, da parte desejável se um
projeto possui essa característica.
Muitas vezes para fazer a separação de uma parte do sistema que não será mais utilizada, o custo é maior
do que refazer o projeto do zero. Por isso é comum essa ação em algumas empresas.
Para ilustrar essa situação, vamos utilizar um programa simples que é carregado com a tarefa de copiar
caracteres digitados em um teclado e enviar para uma impressora, e que a plataforma de implementação
não dá suporte a independência dos dispositivos.
Há três módulos. O módulo "Copy" chama os outros dois. Imagine um loop dentro do módulo "Copy". O
corpo do loop que chama o módulo "Read Keybord” (leitura do teclado) para buscar um caracter do teclado,
que envia um caracter para o
módulo "Write Printer” (Escrever impressora) que imprime o caráter.
3. Os dois módulos de baixo nível são bem reutilizáveis. Eles podem ser usados em muitos outros programas
para ter acesso ao teclado e a impressora. Este é o mesmo tipo de reutilização que ganhamos com
bibliotecas de rotinas.
Veja um exemplo de código parecido com o módulo “Copy”:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using BR.POO.LeitorCaracteres;
using BR.POO.ImpressoraCaracteres;
using BR.POO.ImpressorTXT;
namespace BR.POO.Copiador
{
class Copiador
{
//Copia caracteres e imprime para impressora.
public static void CapturaCaracterParaImpressora()
{
char[] caracter = LeitorCaracteres.LeitorCaracteres.obterCaracteres();
for (int i = 0; i < caracter.Length; i++)
{
ImpressoraCaracteres.ImpressoraCaracteres.ImprimeCaracteres(caracter[i]);
}
}
}
}
Note que o módulo "Copy" é dependente do módulo "Write Printer", e portanto, não pode ser reutilizado em
um novo contexto, apesar da funcionalidade desse módulo ser muito interessante, ele não é reutilizável em
qualquer contexto que não envolva um teclado ou uma impressora.
Por exemplo, considere esse contexo: um programa que copia os caracteres digitados em um teclado para
um arquivo em disco.
Certamente poderíamos modificar o módulo "Copiar" para dar-lhe a nova funcionalidade desejada.
Poderíamos acrescentar um “if” para que possamos escolher entre o módulo "Write Printer" e o "Write Disk”,
dependendo somete de algum tipo de comando. No entanto, isso acrescenta novas interdependências, para
o sistema, e conforme o passar do tempo, cada vez mais dispositivos
podem participar do programa, então o módulo "Copy" estará repleto de declarações “if” e “else”
e será dependente de vários módulos de nível inferior. Ele se tornará rígido e frágil.
Veja o código do módulo que lê o teclado:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace BR.POO.LeitorCaracteres
{
public class LeitorCaracteres
{
public static char[] obterCaracteres()
{
char[] caracteres = { 'S', 'A', 'P'};
return caracteres;
}
}
}
4. Veja o código do módulo que imprime o que foi escrito:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace BR.POO.ImpressoraCaracteres
{
public class ImpressoraCaracteres
{
public static void ImprimeCaracteres(char caracter)
{
string caracteres = caracter.ToString();
System.Console.WriteLine(caracteres);
}
}
}
Veja um exemplo do BR.POO.Copiador modificado para imprimir para arquivo:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using BR.POO.LeitorCaracteres;
using BR.POO.ImpressoraCaracteres;
using BR.POO.ImpressorTXT;
namespace BR.POO.Copiador
{
class Copiador
{
//Copia caracteres e imprime para impressora.
public static void CapturaCaracterParaImpressora()
{
char[] caracter = LeitorCaracteres.LeitorCaracteres.obterCaracteres();
for (int i = 0; i < caracter.Length; i++)
{
ImpressoraCaracteres.ImpressoraCaracteres.ImprimeCaracteres(caracter[i]);
}
//Copia caracteres e imprime para arquivo.
ImpressorTXT.ImpressorTXT.ImprimeParaArquivoTXT(caracter);
}
}
}
5. Veja o código do módulo que imprime para arquivo:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace BR.POO.ImpressorTXT
{
public class ImpressorTXT
{
public static void ImprimeParaArquivoTXT(char[] caracteres)
{
string texto = string.Empty;
for (int i = 0; i < caracteres.Length; i++)
{
texto = texto + caracteres[i].ToString();
}
texto = texto + " - Exemplo de aplicação com alta interdependêcia";
System.IO.File.WriteAllText(@"C:ArquivoSAP.txt", texto);
}
}
}
Invertendo dependências com OOD
Uma forma de caracterizar o problema acima é de notar que o módulo que contém
um alto nível de acoplamento, ou seja, o módulo "Copy", é dependente de seus detalhes.
Se pudéssemos controlar os outros módulos a partir de qualquer dispositivo de entrada para qualquer
dispositivo de saída, poderíamos reutilizar livrimente o “Copy”. O OOD nos dá mecanismos para a
realização dessa inversão de dependência.
Considere o diagrama de classe simples:
Acima nós temos uma classe "Copy" que contém uma classe "Reader" e "Writer" abstract. Pode-se
facilmente imaginar um loop dentro da classe "Copy" que recebe caracteres de "Reader" e envia-los para
"Writer".
6. No entanto, essa classe "Copy" de não depende em tudo da "Keyboard Reader", nem da "Printer Writer".
Assim, as dependências foram invertidas. Agora, a classe "Copy" depende
somente das abstracts, e o “Read” e o “Writer”.
Agora podemos reutilizar a classe "Copy", independentemente do "Keyboard Reader" e do
"Printe Writer". Podemos inventar novos tipos de "Reader" e "Writer" que podem dar suporte à classe
"Copy". Além disso, não importa quantos tipos de "Reader e
"Writer" são criados, "Copy" não dependerá de nenhum deles.
Não haverá interdependências para deixar o programa frágil ou rígido. Esta é a essência do DIP.
O que torna a versão do programa OO de Copy robusto, sustentável e reutilizável?
É a sua falta de interdependências. Mas ele tem algumas dependências e as dependências
não interferem na qualidade desejável. Por que não? Porque é extremamente improvável que mude o
objetivo das dependências, com as classes “Reader” e “Writer” pois elas são do tipo não-volátil.
O que fazer quando existem forças que provocam a mudança?
Certamente poderíamos imaginar algumas mudanças se estendecemos um pouco o nosso pensamento.
Mas no curso dos acontecimentos normal, essas classes têm baixa volatilidade.
Desde "Copy" dependa de módulos que são do tipo não-volátil, é muito pouco provável que a “Copy” sofra
alterações. "Copy" também é um exemplo do princípio "Open/Closed". "Copy" está aberta a ser expanção,
uma vez que podem criar novas versões de "Reader"
e "Writer". No entanto, "Copy" está fechada para a modificação, já que não tem que modificá-lo para
alcançar essas extensões. Assim, podemos dizer que uma dependência boa é uma dependência de algo
com baixa volatilidade. Quanto menos volátil o objetivo da dependência, melhor a dependência. Da mesma
forma uma "Má Dependência" é uma dependência de algo que é volátil. Quanto mais volátil o objetivo da
dependência, pior é a dependência.
Estabilidade e Independência
A definição clássica da estabilidade palavra é: "Não é facilmente abalado."
Esta é a definição que iremos utilizar neste artigo. Ou seja, a estabilidade não é uma medida da
probabilidade que um módulo vai mudar, e sim é uma medida da dificuldade de um módulo em mudar.
Como se alcançar a estabilidade? Por que, por exemplo "Reader" e "Writer", são tão estáveis?
Considere novamente as forças que poderiam fazê-los mudar. Eles não dependem de nada
em tudo, então a mudança de uma dependencia não podem estender-se até eles e levá-los a mudar.
Essa característica é chamda de "Independência".
Classes Independente são classes que não dependem de qualquer outra coisa.
Outra razão que "Reader" e "Writer" são estáveis é que eles são dependencias de outras classes. Entre
"Copy", "KeyboardReader" e "KeyboardWriter".
O fato é que, podem existir alterações de "Reader" e "Writer", mas, quanto mais dependencias essas
classes tiverem, mais difícil será alteralas.
Se alterarmos "Reader" ou "writer" que teria que mudar todas as outras classes
que dependem delas. Assim, essa mudança daria muito trabalho o que nos impede de mudar
essas classes, e aumentando a sua estabilidade.
7. Classes responsável tendem a ser estáveis porque qualquer mudança tem grande um impacto.
As classes mais estáveis, são classes que são independentes e responsáveis. Essas classes não têm
nenhuma razão para mudar, e muitas razões para não mudar.
O Princípio dependências Estável (SDP)
As dependências entre pacotes em um projeto devem ser no sentido da estabilidade dos PACOTES.
Os PACOTES devem depender apenas de pacotes que são MAIS ESTÁVEL que ele.
Projetos não podem ser completamente estáticos. Alguma volatilidade é necessário ser mantida no projeto.
Nós conseguimos isso em conformidade com o Princípio Encerramento Comum (PCC).
Usando este princípio, criamos pacotes que estão sensíveis a certos tipos de alterações.
Estes pacotes são destinados a serem voláteis.
Nós esperamos que eles mudem. Qualquer pacote que nós esperamos ser voláteis não devem depender de
um pacote que é difícil mudar! Caso contrário, o pacote voláiíl também será difícil de mudar.
Conformando-se à SDP, podemos garantir que os módulos que são projetados para ser instáveis
(Isto é fácil de mudar), não são considerados pelos módulos que são mais estáveis (isto é mais difícil
mudar) do que eles.
Métricas de Estabilidade
Como podemos medir a estabilidade de um pacote?
• Uma maneira é contar o número de dependências que entram e saem desse pacote. Estas
contagens nos permitirá calcular a posição estabilidade do pacote.
• Ca: Acoplamentos Aferentes: O número de classes de fora deste pacote, que dependem
em classes dentro deste pacote.
• Ce: Acoplamentos eferente: O número de classes dentro desse pacote que depende de
classes de fora deste pacote.
• I: Instabilidade: (Ce/(Ca + Ce)): Esta métrica tem no intervalo [0,1].
I = 0 (indica ser um pacote maximamente estável).
I = 1 (indica um pacote máximamente instável).
As métricas de Ca e Ce são calculados pela contagem do número de classes fora do
pacote em questão que têm dependências com as classes dentro do pacote em questão.
O PSD diz que a métrica de um pacote que deve ser maior do que as métricas I do
pacotes que ele depende. ou seja, eu métricas devem diminuir na direção de dependência.
Nem todos os pacotes devem ser estáveis. Se todos os pacotes em um sistema foram maximamente
estável, o sistema seria imutável.
8. Esta não é uma situação desejável. Na verdade, queremos projetar a nossa estrutura de pacotes, de modo
que alguns pacotes são instáveis, e alguns são estáveis. A figura a seguir mostra o ideal
configuração de um sistema com três pacotes.
O Princípio da Abstração Estável (SAP)
Pacotes que são maximamente ESTÁVEIS devem ser maximamente ABSTRATOS.
PACOTES instáveis DEVEM SER CONCRETOS.
A abstração de um pacote deve ser PROPORCIONAL a sua estabilidade.
Este princípio estabelece uma relação entre a estabilidade e a abstração. Ela diz que um
pacote estável deve também ser abstrato de modo que sua estabilidade não impeça que ele seja
modificado. Por outro lado, ele diz que um pacote instável deve ser de concreto, uma vez que a sua
instabilidade permita que o código de concreto dentro dele seja facilmente alterado.
Veja um exemplo do BR.POO.Copiador aplicando o princício SAP. Note que o copiador agora pode
receber qualquer tipo de leitor de caracteres e impressoras, sem sofrer nenhuma modificacão:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using BR.POO.Ler;
using BR.POO.Imprimir;
namespace BR.POO.Copiador.SAP
{
class Copiador
{
/*Exemplo de aplicação estável e abstrata.
(Aberta a espanção e fechada para modificação*/
public static void LerImprimir()
{
char[] leituraEntrada = Ler.Ler.Leitura();
Imprimir.Imprimir.Imprime(leituraEntrada);
}
}
}
9. Veja o código do módulo ler:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using BR.POO.LeitorCaracteres;
namespace BR.POO.Ler
{
public class Ler
{
public static char[] Leitura()
{
char[] leitura = LeitorCaracteres.LeitorCaracteres.obterCaracteres();
return leitura;
}
}
}
Veja o código do módulo Imprimir:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using BR.POO.ImpressorTXT;
using BR.POO.ImpressoraCaracteres;
namespace BR.POO.Imprimir
{
public class Imprimir
{
public static void Imprime(char[] leitura)
{
ImpressorTXT.ImpressorTXT.ImprimeParaArquivoTXT(leitura);
for (int i = 0; i < leitura.Length; i++)
{
ImpressoraCaracteres.ImpressoraCaracteres.ImprimeCaracteres(leitura[i]);
}
}
}
}
Código do módulo que lê o teclado:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace BR.POO.LeitorCaracteres
{
public class LeitorCaracteres
{
public static char[] obterCaracteres()
{
char[] caracteres = { 'S', 'A', 'P'};
return caracteres;
}
}
}
10. Código do módulo que imprime o que foi escrito:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace BR.POO.ImpressoraCaracteres
{
public class ImpressoraCaracteres
{
public static void ImprimeCaracteres(char caracter)
{
string caracteres = caracter.ToString();
System.Console.WriteLine(caracteres);
}
}
}
Código do módulo que imprime para arquivo:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace BR.POO.ImpressorTXT
{
public class ImpressorTXT
{
public static void ImprimeParaArquivoTXT(char[] caracteres)
{
string texto = string.Empty;
for (int i = 0; i < caracteres.Length; i++)
{
texto = texto + caracteres[i].ToString();
}
texto = texto + " - Exemplo de aplicação com alta interdependêcia";
System.IO.File.WriteAllText(@"C:ArquivoSAP.txt", texto);
}
}
}
11. Como mensurar?
(NC): O número de classes do pacote
(Na): O número de classes abstratas no
pacote
(Abstração): A = Na / Nc
Um A tem o intervalo [0,1]
A = 0 (implica que o pacote não tem classes abstratas)
A = 1 (implica que o pacote possui somente classes abstratas)
Distância da Sequência Principal
(Distância D): D = | A + I - 1 | / √ 2
O pacote deve estar ligado ou próximo da seqüência principal
Intervalos D’ a partir de [0, 0,707 ~]
(Distância normalizada D ‘): D’ = | I + A - 1 |
Intervalos D 'a partir de [0, 1]
D = 0 indica que o pacote está diretamente ligado
a seqüência principal
D = 1 indica que o pacote está tão longe
longe possível a partir da seqüência principal
Gráfico de Métrica de Variância de Todas as Métricas D