Aula 3: Desacoplamento 2ª. Parte - mit

Propaganda
Aula 3: Desacoplamento 2ª. Parte
Na última aula, nós falamos a respeito da importância das dependências entre as partes do programa
em um projeto de software. Uma boa linguagem de programação permite a você expressar as
dependências entre as partes, além de controlá-las – prevenindo o surgimento de dependências não
planejadas. Nesta aula, nós vamos demonstrar os aspectos do Java que podem ser utilizados para
expressar e controlar dependências. Vamos estudar também uma variedade de soluções para um
problema simples de programação, ilustrando com ênfase o papel das interfaces.
3.1 Revisão: Diagrama de Dependências Modular
Vamos começar com uma breve revisão do diagrama de dependência modular (MDD) da última
aula. Um MDD mostra dois tipos de partes de programas: partes de implementação (classes em
Java) exibidas como retângulos com uma única faixa no topo, e as partes de especificação exibidas
como retângulos com faixas tanto no topo quanto na parte de baixo. A organização das partes em
agrupamentos (tal como packages1, ou pacotes, em Java) pode ser expressa no MDD através de
contornos englobando as partes do programa. Assim como definido pelo estilo de diagrama
denominado Venn-diagramstyle.
Uma flecha plana com a ponta aberta conecta uma parte de implementação A com uma parte de
especificação S, e expressa que o significado de A depende do significado de S. Isto, pois a
especificação S não pode, por si só, ter um significado dependente de outras partes, o que garante
que o significado de uma parte pode ser determinado a partir da própria parte e da especificação da
qual ela depende, não sendo necessário quaisquer outras informações. Uma flecha pontilhada de A
para S significa uma dependência fraca; ela expressa que A depende apenas da existência de uma
parte que satisfaça à especificação S, mas que, na verdade, não possui dependência de nenhum
detalhe de S. Uma flecha com a ponta fechada de uma parte de implementação A para uma parte de
especificação S expressa que A satisfaz S; seu significado está em conformidade com o significado
de S.
Graças ao fato de que as especificações são tão essenciais, nós iremos sempre assumir que elas
estão presentes. Na maioria das vezes, nós não iremos desenhar partes de especificação
explicitamente, portanto, uma flecha de dependência entre duas partes A e B deve ser interpretada
como uma dependência de A para a especificação de B, e como uma flecha de conformidade de B
para sua especificação. Nós iremos demonstrar as interfaces do Java como partes de especificação
explicitamente.
1
O termo package será utilizado como sinônimo de pacote com a intenção de se criar familiaridade com os
termos da linguagem Java.
30
3.2 Java Namespace
Como qualquer grande trabalho escrito, um programa se beneficia de uma organização estrutural
hierárquica. Quando se tenta compreender uma grande estrutura, é sempre útil observá-la de baixo
para cima, iniciando-se com os níveis mais grosseiros da estrutura e procedendo-se para níveis mais
e mais detalhados. O sistema de nomes do Java suporta esta estrutura hierárquica. E promove
também um outro importante benefício. Componentes diferentes podem usar os mesmos nomes
para seus subcomponentes, com distintos significados locais. No contexto do sistema como um
todo, os subcomponentes terão nomes que são determinados pelos componentes dos quais eles
pertencem, evitando confusões. Isto é vital, pois permite aos desenvolvedores trabalharem
independentemente sem se preocupar com conflitos de nomes.
Agora apresentaremos como o sistema de nomes do Java funciona. Os componentes principais são
as classes e as interfaces, que possuem designações de nomes de métodos e de nomes de campos.
Variáveis locais (dentro de métodos) e argumentos de métodos também são designados. Cada nome
em um programa Java possui um escopo, ou abrangência: uma porção do texto do programa dentro
da qual aquele nome é válido e atado àquele componente. Argumentos de métodos, por exemplo,
têm o escopo do método; campos têm o escopo da classe, e às vezes um escopo ainda maior. O
mesmo nome pode ser usado para referenciar coisas diferentes quando não há ambigüidade. Por
exemplo, é possível utilizar-se o mesmo nome para um campo, um método e uma classe; consulte a
especificação (spec) da linguagem Java para obter exemplos.
Um programa Java é organizado em packages, ou pacotes. Cada classe ou interface possui seu
próprio arquivo (desconsiderando-se inner classes, ou classes internas, que não serão discutidas).
Os packages refletem a estrutura de diretórios. Da mesma forma que diretórios, packages podem ser
aninhados em estruturas de profundidades arbitrárias. Para organizar seu código em packages, você
deve fazer duas coisas: primeiro, você indica no topo de cada arquivo a quais packages esta classe
ou interface pertence e, segundo, você organiza os arquivos fisicamente em uma estrutura de
diretórios de forma que eles estejam de acordo com a estrutura dos packages. Por exemplo, a classe
dnj.browser.Protocol seria um arquivo chamado Protocol.java no diretório dnj/browser.
Pode-se demonstrar esta estrutura no nosso diagrama de dependências. As classes e interfaces
formam as partes entre as quais as dependências são esquematizadas. Packages são mostrados como
contornos englobando estas classes e interfaces. Às vezes é conveniente esconder as dependências
exatas entre as partes de diferentes packages e apenas mostrar um arco de dependência no nível dos
packages. Uma dependência de um package significa que alguma classe ou interface (podem ser
várias) daquele package possui uma dependência; uma dependência sobre um package significa
31
uma dependência de uma classe ou interface (podem ser várias também) que pertence àquele
package.
3.3 Controle de Acesso
Os controles de acesso do Java permitem que se controlem as dependências. No texto de uma
classe, é possível indicar quais outras classes podem ter dependências sobre ela, desta forma, é
possível, até certo grau, controlar a natureza das dependências.
Uma classe declarada como public pode ser referenciada por quaisquer outras classes; do contrário,
ela só pode ser referenciada por classes dentro do mesmo package. Portanto, através deste
modificador podemos evitar dependências de qualquer outra classe que pertença a outro package.
Membros de uma classe – isto é, seus campos e métodos – podem ser marcados como public,
private ou protected. Um membro public pode ser acessado de qualquer lugar. Um membro private
só pode ser acessado de dentro da classe onde é declarado. Um membro protected pode ser
acessado de dentro do package, ou de fora do package por uma subclasse2 da classe onde o membro
é declarado – criando, portanto, um efeito há muito conhecido de que um membro protected é mais
acessível, ao invés de menos acessível, como a idéia de proteção presume.
Recorde-se que uma dependência de A sobre B significa uma dependência de A sobre a
especificação de B. O uso de métodos modificadores entre os membros de B nos permitem controlar
a natureza das dependências ao alterar-se quais membros pertencem à especificação de B. O
controle do acesso aos campos de B é uma ajuda para se alcançar independência de representação,
mas nem sempre uma garantia (como veremos adiante no curso).
2
O termo subclasse, utilizado no decorrer do texto, refere-se à prática de derivação de classes da
programação orientada a objetos.
32
3.4 Linguagens Seguras
Uma propriedade chave de um programa é a de que uma parte só deveria depender de outra se o
fizermos explicitamente referenciando a outra parte através de seu nome. Isto parece óbvio, mas, de
fato, esta é uma característica válida apenas para as chamadas “linguagens de programação
seguras”. Em linguagens não seguras, o texto em uma parte pode afetar o comportamento de outra
sem que nomes sejam compartilhados. Isto nos leva aos chamados “bugs traiçoeiros” que são
dificílimos de se detectar, e que podem ocasionar efeitos desastrosos e imprevisíveis.
Veja como eles surgem. Considere um programa escrito em C no qual um módulo (em C é apenas
um arquivo) faz a atualização de um array. Uma tentativa de se definir o valor de um elemento de
um array além de seus limites às vezes irá falhar, pois causa uma violação de memória, atingindo
uma área de memória além daquela definido para o processo. Mas, infelizmente, na maioria das
vezes isso irá funcionar, e o resultado será a escrita de um pedaço arbitrário de memória –
arbitrário, pois o programador não sabe como o compilador alocou a memória do programa, e não
pode predizer quais outras estruturas de dados foram afetadas. Como resultado, uma atualização do
array a pode afetar o valor de uma estrutura de dados com o nome d, por exemplo, que é declarada
em um módulo diferente e que nem mesmo possui um tipo em comum com a.
Linguagens seguras controlam estes efeitos através de diversas técnicas combinadas. Checagem
dinâmica de limites de arrays previnem o tipo de atualização que acabamos de mencionar; em Java
uma exceção seria lançada. O gerenciamento automático de memória garante que a memória não
seja liberada e em seguida, erroneamente, utilizada. Ambas as técnicas baseiam-se na idéia
fundamental de “tipagem forte”, que garante que um acesso declarado como sendo um valor de um
tipo t no programa texto será sempre um acesso a uma valor do tipo t em tempo de execução. Não
há risco de que código projetado para ser utilizado sobre arrays seja aplicado a uma string ou a um
inteiro.
Linguagens seguras surgiram em 1960. Algumas famosas linguagens seguras incluem Algol-60,
Pascal, Modula, LISP, CLU, Ada, ML, e agora Java. É interessante notar que por muitos anos a
indústria reclamou que os custos da segurança destas linguagens eram muito altos, e que era
inviável migrar de linguagens não seguras (como C++) para linguagens seguras (como Java). Java
se beneficiou de muita publicidade a respeito de applets, e agora que a linguagem é largamente
utilizada, muitas bibliotecas estão disponíveis e existem muitos programadores que conhecem Java,
muitas companhias deram o braço a torcer e estão reconhecendo os benefícios de uma linguagem
segura. Algumas linguagens seguras garantem a correção de tipagem em tempo de compilação –
através de ‘tipagem estática’. Outras, como Scheme e LISP, realizam sua checagem de tipo em
tempo de execução, sendo que seus sistemas de tipagem apenas distinguem os tipos primitivos.
Veremos rapidamente como um sistema de tipagem mais expressivo também pode ajudar no
33
controle das dependências. Se a confiabilidade é importante, é sábio utilizar uma linguagem segura.
Na aula 1 eu contei uma estória sobre o uso de linguagens não seguras em um sistema de raios-x.
3.5 Interfaces
Em linguagens com tipagem estática, podem-se controlar dependências através da escolha de tipos.
Pode-se dizer que uma classe que menciona apenas objetos do tipo T não pode ter uma dependência
de uma classe que possui objetos de um tipo T diferente. Em outras palavras, pode-se deduzir, a
partir dos tipos de uma classe, de quais outras classes esta classe depende.
No entanto, em linguagens com subtipos (tipos derivados), uma coisa interessante é possível.
Suponha que a classe A mencione apenas a classe B. Isto não significa que apenas métodos criados
a partir da classe B possam ser chamados. Em Java, os objetos instanciados a partir de uma
subclasse C de B são tidos como tendo também o tipo B, assim, mesmo que A não possa criar
objetos da classe C diretamente, ela pode acessá-los por intermédio de outra classe. O tipo C é dito
ser um subtipo do tipo B, pois um objeto C pode ser utilizado quando um objeto B é aguardado. Isto
é chamado ‘substituibilidade’, ou seja, a possibilidade de substituir.
Na verdade, a prática de se criar subclasses funde dois princípios distintos. Um é a subtipagem: que
define que objetos de uma classe C são tidos como tendo tipo compatível com B, por exemplo. O
outro é a herança: que define que o código da classe C pode reutilizar o código de B. Mais adiante
neste curso iremos discutir algumas das conseqüências infortúnias de se fundir estes dois princípios,
e veremos como a substituibilidade nem sempre funciona, como é de se esperar. Por enquanto,
iremos focar no mecanismo de subtipagem apenas, pois é o que importa na nossa discussão. O Java
provê uma noção de interfaces que proporciona mais flexibilidade na prática da subtipagem do que
o uso de subclasses. Uma interface Java é, na nossa terminologia, uma parte de especificação pura.
Ela não contém código executável, é utilizada apenas para se alcançar desacoplamento.
Veja como funciona. Ao invés de se ter uma classe A dependente de uma classe B, introduz-se uma
classe I. Agora, A menciona I ao invés de B, e B deve satisfazer a especificação de I. É claro que o
compilador Java não trabalha com especificações comportamentais: ele apenas checa se os tipos dos
métodos de B são compatíveis com os tipos declarados em I. Em tempo de execução, toda vez que
A estiver aguardando um objeto do tipo I, um objeto do tipo B também é aceito. Por exemplo, na
biblioteca Java, há uma classe chamada java.util.LinkedList que implementa listas encadeadas. Se
você estiver escrevendo código que exige apenas que um objeto seja uma lista, e não
necessariamente que seja uma lista encadeada, deve-se utilizar o tipo java.util.List no seu código, o
que é uma interface implementada por java.util.LinkedList. Existem outras classes como ArrayList e
Vector que implementam esta interface. Desde que seu código refira-se apenas à interface, ele irá
funcionar com qualquer uma destas classes de implementação.
34
Várias classes podem implementar a mesma interface, e uma classe pode implementar várias
interfaces. Pelo contrário, uma classe pode herdar no máximo uma classe. Por causa disso, algumas
pessoas utilizam o termo ‘herança de múltipla especificação’ para descrever a funcionalidade das
interfaces do Java, ao contrário da verdadeira herança múltipla na qual pode-se reutilizar código de
múltiplas superclasses.
Inicialmente, as interfaces trazem dois benefícios. Primeiro, elas permitem que se expressem partes
de pura especificação no código, de forma que seja possível garantir que o uso da classe B por uma
classe A envolve apenas uma dependência de A para a especificação S, e não em outros detalhes de
B. Segundo, as interfaces permitem criar várias partes de implementação que estejam em
conformidade com uma única especificação, sendo que a seleção de qual parte de implementação
será usada ocorrerá em tempo de execução.
3.6 Exemplo: Instrumentação de um Programa
No restante da aula, iremos estudar alguns mecanismos de desacoplamento no contexto de um
exemplo que é pequeno, mas que é representativo de uma importante classe de problemas.
Suponha que se queiram reportar os passos incrementais de um programa no decorrer de sua
execução, exibindo seu progresso linha por linha. Por exemplo, em um compilador com suas
diversas etapas, pode-se desejar exibir uma mensagem quando cada etapa começa e termina. Em um
cliente de e-mail, podemos exibir cada um dos passos envolvidos na tarefa de se fazer o download
dos e-mails a partir de um servidor. Este tipo de mecanismo de acompanhamento da execução é útil
quando os passos individuais podem levar um longo tempo de execução ou quando são propensos à
falha (de forma que o usuário possa cancelar o comando que originou a execução). Barras de
progresso são muitas vezes utilizadas neste contexto, mas elas introduzem complicações adicionais
com as quais não iremos nos preocupar.
Como um exemplo concreto, considere um cliente de e-mail que possua um package principal
(core) contendo uma classe Session com código projetado para estabelecer uma sessão de
comunicação com um servidor e para fazer o download de mensagens, uma classe Folder para os
objetos que modelam pastas e seu conteúdo, e uma classe Compactor que contém o código para
compactar a representação das pastas no disco. Assuma que há chamadas de Session para Folder e
de Folder para Compactor, assuma ainda que as intensas atividades de recursos computacionais que
queremos instrumentar ocorrem apenas em Session e Compactor, e não em Folder.
O módulo diagrama de dependência mostra que Session depende de Folder, que possui uma
dependência mútua de Compactor.
35
Iremos analisar uma variedade de meios para implementar nosso mecanismo de instrumentação, e
iremos estudar as vantagens e desvantagens de cada. Começando pelo mais simples, mais ingênuo
projeto possível, podemos distribuir sentenças como
System.out.println (“Iniciando download”);
ao longo do programa.
3.6.1 Abstração por parametrização
O problema com este esquema é óbvio. Quando rodamos o programa em modo batch, podemos
redirecionar a saída de dados padrão do programa para um arquivo. Percebemos, então, que isto
seria útil para gerar mensagens acrescidas da informação do momento quando elas ocorreram, de
maneira que poderíamos mais tarde, quando lermos o arquivo de mensagens, saber quanto tempo
durou cada um dos vários passos executados. Desejamos que nossas sentenças sejam da forma
System.out.println (“Iniciando download em: ” + new Date ());
O que deveria ser fácil, mas não é. Temos que procurar todas as sentenças presentes em nosso
código (e distinguí-las de outras chamadas a System.out.println destinadas a outros propósitos), e
alterar cada uma delas separadamente.
É claro, o que deveríamos ter feito é definir um procedimento para encapsular esta funcionalidade.
Em Java, seria feito através de um método estático:
public class StandardOutReporter {
public static void report (String msg) {
System.out.println (msg);
}
}
Agora, a alteração pode ser realizada em um único local no código. Apenas modificamos o
36
procedimento para
public class StandardOutReporter {
public static void report (String msg) {
System.out.println (msg + “ at: “ + new Date ());
}
}
Matthias Felleisen chama isso de princípio do ‘ponto de controle individual’. O mecanismo neste
caso é um com o qual você deve estar familiar: o curso 6001 o chama de abstração por
parametrização, pois cada chamada ao procedimento, tal como
StandardOutReporter.report (“Iniciando download”);
é uma instância da descrição genérica, com o parametrizador msg atado a um valor particular.
Podemos ilustrar o ponto de controle individual em um diagrama de dependência modular.
Introduzimos uma única classe da qual as classes que utilizam o mecanismo de instrumentação
dependem: StandardOutReporter. Perceba que não há dependência de Folder para
StandardOutReporter, pois o código de Folder não faz chamadas para esta outra classe.
3.6.2 Desacoplamento com Interfaces
No entanto, este esquema está bem longe da perfeição. Automatizar a funcionalidade em uma única
classe foi uma boa idéia, mas o código ainda possui uma dependência na noção de escrita para a
saída de dados padrão. Se quiséssemos criar uma nova versão do nosso sistema com uma interface
gráfica de usuário (GUI), precisaríamos substituir esta classe com uma outra contendo o código
apropriado para a GUI. Isto significaria alterar todas as referências no package core para uma classe
diferente, ou alterar o código da própria classe, e ter que manipular duas versões incompatíveis da
37
classe com o mesmo nome. Nenhuma das duas é uma boa opção.
De fato, o problema é pior do que isso. Em um programa que utiliza GUI, escreve-se para a GUI
através de uma chamada a um método de um objeto que representa parte da interface gráfica: um
painel de texto, ou um campo de mensagem. Em Swing, o toolkit de interfaces de usuário do Java,
as subclasses de JTextComponent possuem um método setText. Dado um componente
JTextComponent representado pela variável outputArea, por exemplo, a sentença de exibição
poderia ser:
outputArea.setText (msg)
Como vamos passar a referência para o componente na posição de chamada (na nova classe
StandardOutReporter)? E como vamos fazê-lo sem introduzir código específico do Swing na classe
responsável por reportar os progressos da execução do programa?
As interfaces Java provêm a solução. Criamos uma interface com um único método para reportar os
progressos de execução que será chamado para exibição dos resultados.
public interface Reporter {
void report (String msg);
}
Agora, acrescentamos a cada método do nosso sistema um argumento deste tipo. A classe Session,
por exemplo, pode ter um método download:
void download (Reporter r, …) {
r.report (“Iniciando download” );
…
}
Agora definimos uma classe que irá implementar o comportamento do método report. Iremos
utilizar a saída de dados padrão como nosso exemplo, por razões de simplicidade:
public class StandardOutReporter implements Reporter {
public void report (String msg) {
System.out.println (msg + “ at: “ + new Date ());
}
}
38
Esta classe não é a mesma classe que a anterior, e que possuía o mesmo nome. O método não é mais
estático, de maneira que podemos instanciar um objeto da classe e chamar o método a partir dele.
Além do mais, indicamos que esta classe é uma implementação da interface Reporter. É claro, para
a saída de dados padrão esta solução não parece ser muito boa, e a criação do objeto implica em um
custo computacional. Mas, para o caso da GUI, iremos fazer algo mais elaborado e criar um objeto
que esteja atado ao mecanismo JTextComponent em particular.
public class JTextComponentReporter implements Reporter {
JTextComponent comp;
public JTextComponentReporter (JTextComponent c) {comp = c;}
public void report (String msg) {
comp.setText (msg + “ at: “ + new Date ());
}
}
No topo do programa, iremos criar um objeto e passá-lo:
s.download (new StandardOutReporter (), …);
Onde s representa uma instância da parte (ou classe) Session.
Agora, alcançamos algo interessante. A chamada ao método report executa, em tempo de execução,
código que invoca System.out. Mas métodos como o download da classe Session dependem apenas
da interface Reporter, que não faz referência a nenhum mecanismo de saída de dados específico.
Conseguimos, com sucesso, desacoplar o mecanismo de saída de dados e o programa, quebrando a
dependência do núcleo do programa de seus dispositivos de saída de dados (seu I/O).
Observe o diagrama de dependência modular. Recorde-se que uma flecha com a cabeça fechada de
A para B é lida como ‘A satisfaz B’. B pode ser uma classe ou uma interface; o relacionamento em
Java pode ser de implementação ou de extensão. Aqui, a classe StandardOutReporter satisfaz a
interface Reporter.
A propriedade chave do esquema é que não há mais uma dependência de nenhuma classe do
package núcleo (core) sobre uma classe do package gui. Todas as dependências apontam (pelo
menos logicamente!) de gui para core. Para alterar a saída de dados do programa para um
mecanismo GUI, ao invés da saída de dados padrão, teríamos apenas que substituir a classe
StandardOutReporter pela classe JTextComponentReporter, e modificar o código da classe
principal do package GUI para chamar seu construtor nas classes que contém código de I/O
concreto. Este idioma, esta maneira de se fazer as coisas, talvez seja o uso mais popular das
39
interfaces, sendo muito valioso seu aprendizado e domínio.
Lembre-se que as flechas pontilhadas são dependências fracas. Uma dependência fraca de A para B
significa que A referencia os nomes de B, mas não referencia o nome de nenhum de seus membros.
Em outras palavras, A sabe que a classe ou interface B existe, e refere-se a variáveis daquele tipo,
mas não faz chamadas a nenhum método de B, e nem acessa qualquer campo de B.
A dependência fraca de Main sobre Reporter simplesmente indica que a classe Main pode incluir
código que manipula um objeto do tipo Reporter genérico; não se trata de um problema. A
dependência fraca de Folder sobre Reporter, no entanto, é um problema: o objeto do tipo Reporter
deve ser passado através de métodos de Folder para métodos de Compactor. Todo método da
cadeia de chamadas que alcança um método instrumentado deve receber um Reporter como
argumento. O que é um incomodo, e que pode tornar doloroso um processo posterior de
aperfeiçoamento deste esquema.
3.6.3 Interfaces vs. Classes Abstratas
Você deve estar se perguntando se poderíamos ter utilizado uma classe abstrata ao invés de uma
interface. Uma classe abstrata é uma classe que não está completamente implementada; ela não
pode ser instanciada, ela deve ser estendida através de uma subclasse que a completa. Classes
abstratas são úteis quando se deseja produzir código comum a várias classes. Suponha que
quiséssemos exibir uma mensagem dizendo quanto tempo cada passo da execução levou. Nós
40
podemos implementar uma classe Reporter cujos objetos mantém o estado no qual estavam quando
da última chamada ao método report, calculando, então, a diferença de tempo entre a chamada
corrente e a última chamada. Ao tornar esta classe abstrata, poderíamos reutilizar o código em cada
uma das subclasses concretas StandardOutReporter, JTextComponentReporter, etc.
Por que não fazer com que o argumento do método download da classe Session tenha esta classe
abstrata como seu tipo, ao invés de uma interface? Há duas razões. A primeira é que desejamos que
a dependência sobre o código de Reporter seja a mais fraca possível. A interface não possui código
nenhum; ela expressa a especificação mínima do que é necessário. A segunda razão é que não há
herança múltipla em Java: uma classe pode estender no máximo uma única outra classe. Portanto,
quando você estiver projetando o núcleo do programa, você não quer utilizar sua única
oportunidade de utilizar o recurso de subclasses prematuramente. Uma classe pode implementar
qualquer número de interfaces, ou seja, ao escolher uma interface, você deixa por conta do
projetista das classes Reporter a questão de como elas serão implementadas.
3.6.4 Campos Estáticos
A desvantagem clara do esquema discutido na seção 3.6.3 é que o objeto Reporter deve ser inserido
por todo o núcleo do programa. Se toda a saída de dados for exibida em um único componente de
texto, parece ser não muito bom ter que passar uma referência a este objeto pra lá e pra cá dentro do
código. Em termos de dependência, todo método possui, no mínimo, uma dependência fraca sobre a
interface Reporter
Variáveis globais, ou campos estáticos em Java, provêm uma solução para este problema. Para
eliminar muitas destas dependências, nós podemos manter o objeto do tipo Reporter como um
campo estático de uma classe:
public class StaticReporter {
static Reporter r;
static void setReporter (Reporter r) {
this.r = r;
}
static void report (String msg) {
r.report (msg);
}
}
Agora, tudo que precisamos fazer é definir o objeto Reporter estático no início:
41
StaticReporter.setReporter (new StandardOutReporter ());
E podemos fazer chamadas para ele sem a necessidade de uma referência para um objeto:
void download (…) {
StaticReporter.report (“Iniciando download” );
…
}
No diagrama de dependência modular, o efeito desta alteração é que agora apenas as classes que
realmente utilizam o objeto do tipo Reporter são dependentes dela:
Perceba agora que a dependência fraca de Folder não existe mais. Nós já vimos este conceito antes,
é claro, no nosso segundo esquema no qual a classe StandardOutReporter possuía um método
estático. Este esquema combina o aspecto estático com o desacoplamento que as interfaces
providenciam.
As referências globais são cômodas, pois lhe permitem alterar o comportamento de métodos em
níveis bem baixos da hierarquia de chamadas sem a necessidade de alteração do código que os
envoca. Mas as variáveis globais são perigosas. Elas podem tornar o código extremamente difícil de
ser compreendido. Para se determinar o efeito de uma chamada a StaticReporter.report, por
exemplo, é necessário saber como o campo estático r está definido. Pode haver uma chamada ao
método setReporter em qualquer lugar no código, e para se definir qual efeito ela tem, é necessário
traçar o fluxo da execução para se determinar quando o método é executado em relação ao código
de interesse.
42
Outro problema com relação às variáveis globais é que elas apenas funcionam bem quando
realmente há um objeto que possui significado persistente. A saída padrão de dados é um destes
casos. Mas componentes de texto em uma GUI não são. Nós podemos, muito bem, desejar que
diferentes partes de nosso programa reportem seu progresso a diferentes painéis da nossa GUI. No
esquema em que os objetos do tipo Reporter são passados de uma parte para outra do código, nós
podemos criar diferentes objetos e passá-los a diferentes partes do código. Na versão estática,
teremos que criar métodos diferentes, o que começa a degenerar a elegância do código.
Concorrências também ocasionam dúvidas na idéia de se usar um único objeto. Suponha que
façamos o upgrade de nosso cliente de e-mail para que seja capaz de fazer o download das
mensagens a partir de diversos servidores de forma concorrente, isto é, ao mesmo tempo. Nós não
gostaríamos que as mensagens de progresso de todas as sessões de download fossem intercaladas
em uma única saída de dados.
Uma boa prática é ser cuidadoso com variáveis globais. Pergunte a você mesmo se é possível
utilizar um mesmo objeto. Normalmente, você irá encontrar amplas razões para ter mais do que um
objeto no decorrer de seu programa. Este esquema é denominado Singleton na literatura, pois a
classe possui um único objeto.
43
Download