Classes e Interfaces
Este artigo contém orientações sobre o uso
de classes e interfaces, tornando-as mais usáveis, robustas e
flexíveis.
Item 12: Minimize a acessibilidade de
classes e membros
O conceito de encapsulamento significa que um módulo deve
ocultar seus dados internos e implementação, separando a
API, que será usada pelos outros módulos, da
implementação. Isso permite um maior desacoplamento entre
módulos, o que possibilita que sejam desenvolvidos, trabalhados
e usados
individualmente. Outras vantagens são o aumento de sua
reusabilidade
e a redução dos riscos no
desenvolvimento de sistemas maiores.
No Java, a acessibilidade de uma entidade é determinada pelo
local em que ela está declarada e os modificadores de acesso que
são utilizados.
A regra geral é que você deve deixar cada classe ou membro
o mais inacessível possível.
No caso de classes não-internas, as opções
são públicas ou com permissão dentro do pacote
(default). Este segundo tipo de acesso é melhor pois torna a
classe parte da implementação do pacote ao invés
de parte da API, sendo possível modificá-la,
trocá-la ou eliminá-la em uma próxima
versão sem quebrar o código dos clientes da classe.
Deixando-a pública, você fica obrigado a mantê-la e
suportá-la para sempre. Se a classe for usada somente por uma
única outra classe, você pode considerar a possibilidade
de torná-la uma inner class, reduzindo ainda mais sua
acessibilidade. Porém, apenas reduzir o acesso para o pacote
já costuma ser suficiente, pois fica claro que a classe
não faz parte da API exposta para o cliente.
Para membros (campos, métodos, classes aninhadas ou interfaces
aninhadas) existem 4 níveis de acesso, nesta ordem: private
(somente dentro da classe), default (qualquer classe dentro do mesmo
pacote), protected (subclasses e classes dentro do mesmo pacote) e
public (qualquer lugar).
Depois de definida a API pública, todos os outros membros
deveriam ser declarados private. O surgimento de muitos
campos utilizados por outras classes que são parte da
implementação do módulo (portanto usando o acesso
default) é uma indicação para considerar a
decomposição da classe para obter partes mais
desacopladas.
No caso de membros de classes públicas, a
alteração de acessibilidade de default para protected
é enorme, pois torna o membro parte da API pública,
representando um comprometimento com os usuários da classe.
A única restrição que existe sobre reduzir o
acesso é no caso de herança, em que não é
permitido que o método sobrescrito da subclasse tenha uma
acessibilidade menor que o da superclasse ou interface.
Campos nunca devem ser públicos, pois isso tira sua liberdade
para limitar seus valores, tomar alguma ação
quando ele é alterado ou mudar a sua representação
interna. Classes que possuem campos públicos alteráveis
nunca são thread-safe. O único uso válido de
campos públicos é o caso de constantes, declaradas como
public static final e que referenciam um tipo primitivo ou objeto
imutável. Note que uma array é sempre mutável,
portanto é errado deixá-la em um campo deste tipo, pois
seus valores podem ser alterados. Uma solução para esse
caso é o uso de uma List:
private static final Type[]
PRIVATE_VALUES;
public static final List VALUES =
Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
Outra opção é ter um método que retorne uma
cópia da array.
Item 13: Prefira a
imutabilidade
Uma classe
imutável é aquela cujas instâncias
não podem ser modificadas. A informação é
fornecida quando cada instância é criada e permanece fixa
pelo tempo de vida do objeto. Este tipo de classe é mais
fácil de desenhar e implementar, além de ser menos
sujeita a erros e mais segura.
Para ter uma
classe com essa característica devem ser seguidos
os seguintes passos:
1. Não
provenha nenhum método que modifique o objeto;
2. Garanta que
nenhum método pode ser sobrescrito (marcando a
classe como final, os métodos como final ou deixando apenas um
construtor privado);
3. Marque todos
os campos como final;
4.
Marque
todos os campos como private (não
necessário mas recomendável);
5. Garanta
acesso exclusivo a todos os componentes mutáveis que
são referenciados dentro da classe. Crie cópias de
segurança para quando o objeto é exposto ao cliente.
Objetos
imutáveis são simples de usar, pois não é
necessário nenhum esforço por parte do programador para
conferir se seu estado não foi alterado desde sua
criação. Também são thread-safe, não
sendo necessária sincronização e favorecendo o
reuso de instâncias. Pode-se inclusive criar métodos
static factory para promover este reuso.
Eles também são muito práticos para serem usados
como parte de outros objetos, já que o número de
variações do objeto fica controlado. Um caso especial
deste princípio é utilizá-los como chaves em mapas
e em sets.
A única desvantagem de classes imutáveis é a
necessidade de um objeto diferente para cada valor distinto, o que pode
causar um overhead de performance dependendo do custo da
criação do objeto. Nos casos em que não é
possível um mecanismo de otimização para trabalhar
com os objetos internamente, pode ser criada uma classe auxiliar
mutável usada de forma intermediária, como o caso da
StringBuffer
para
o String.
Algumas classes, como a BigInteger e BigDecimal, não
foram
protegidas de herança. Portanto, ao utilizar objetos deste tipo,
é necessário checar a classe para garantir que seja
imutável ou criar um objeto do tipo correto, da seguinte forma:
public void foo(BigInteger b) {
if (b.getClass() != BigInteger.class)
b = new BigInteger(b.toByteArray());
...
}
Note que as regras que garantem que um objeto seja imutável
não precisam ser seguidas à risca. Um objeto pode mudar
alguns estados internos e mesmo assim ser considerado imutável
do ponto de vista externo.
De forma geral, as classes devem ser imutáveis a não ser
que exista um bom motivo para que não sejam. Neste último
caso, ainda deve ser limitada a mutabilidade da classe tanto quanto
possível, reduzindo o número de estados possíveis
e inicializando completamente os objetos nos construtores.
Item 14: Prefira composição
ao invés de herança
A herança entre classes é um meio poderoso de atingir
reusabilidade do código, mas leva a um software frágil se
for utilizada inapropriadamente. É seguro utilizá-la
dentro de um único pacote, em que o mesmo programador é
responsável pela superclasse e subclasse, ou quando a classe foi
desenhada e documentada especificamente para ser estendida. Já
no caso se classes concretas comuns de outros pacotes há maiores
riscos.
Herança quebra o encapsulamento, uma vez que a subclasse depende
dos detalhes de implementação da sua superclasse para
funcionar corretamente. Se a implementação da superclasse
muda de uma versão para outra, a subclasse pode quebrar sem nem
ter sido alterada. Conseqüentemente, a subclasse precisa
acompanhar a
evolução de sua superclasse. Alguns exemplos de causas
comuns de erro são a alteração da
implementação de um método utilizado pela
subclasse ou a inclusão de um novo método que permita
alterar o objeto sem passar pela verificação ou
tratamento do dado que é realizado em outro método da
subclasse.
Também ocorrerão problemas se a superclasse criar um novo
método com a mesma assinatura de um que já foi
implementado da subclasse, podendo até impedir sua
compilação.
Uma forma de evitar estes problemas é, ao invés de
estender a classe, incluir na nova classe um campo privado
referenciando um objeto da classe existente. Desta forma, a classe
existente torna-se um componente da classe nova. Cada
método da classe nova pode chamar os métodos da classe
antiga através desta instância e retornar o resultado.
Exemplo: criar uma classe com a funcionalidade de um HashSet e que
também conte os elementos que estão sendo adicionados:
public
class InstrumentedSet implements Set {
private final
Set s;
private int
addCount = 0;
public
InstrumentedSet(Set s) {
this.s = s;
}
public boolean
add(Object o) {
addCount++;
return s.add(o);
}
public boolean
addAll(Collection c) {
addCount += c.size();
return s.addAll(c);
}
public int
getAddCount() {
return addCount;
}
// Forwarding
methods
public void
clear()
{
s.clear();
}
public boolean
contains(Object o) {return
s.contains(o); }
public boolean
containsAll(Collection c) { return s.containsAll(c); }
public boolean
isEmpty()
{ return s.isEmpty();
}
public
Iterator iterator()
{ return
s.iterator(); }
public boolean
remove(Object o) { return
s.remove(o); }
public boolean
removeAll(Collection c) { return s.removeAll(c); }
public boolean
retainAll(Collection c) { return
s.retainAll(c); }
public int
size()
{ return
s.size(); }
public
Object[] toArray()
{ return s.toArray();
}
public
Object[] toArray(Object[] a) { return
s.toArray(a); }
public boolean
equals(Object o) { return
s.equals(o); }
public int
hashCode()
{ return
s.hashCode(); }
public String
toString()
{ return s.toString();
}
}
Este tipo de classe é chamado de wrapper, pois envolve outra
instância de um Set adicionando funcionalidades. Também
é conhecida pelo design pattern Decorator.
Herança só é apropriada em casos em que a
subclasse realmente é um subtipo da superclasse, obedecendo a
relação B "é um" A. Além disso, deve ser
observada se a API da classe que estará sendo estendida é
apropriada, pois usando herança você estará
propagando as falhas para a nova classe.
Item 15: Especifique e documente para
herança, caso contrário, proíba-a
O item anterior avisou sobre os perigos de herdar uma classe que
não foi criada com esse propósito. Então quando a
classe for feita para ser herdada ela deve documentar precisamente os
efeitos de sobrescrever qualquer método. Devem ser indicados
quais métodos "sobrescrevíveis" (public ou protected e
não marcado como final) são chamados, em qual
seqüência e como os resultados de cada chamada afetam o
processamento posterior. Exemplo: "This
implementation iterates over the collection looking for the specified
element. If it finds the element, it removes the element from the
collection using the iterator's remove method."
(retirado da API da AbstractCollection). Nesta
documentação do método remove fica claro
que
sobrescrever o iterator irá afetar seu comportamento. Pode ser
observado
aqui que herança quebra o encapsulamento, uma vez que é
necessário expor detalhes da implementação.
Também podem ser criados métodos e campos "protected"
para expor parte da implementação que pode ser
sobrescrita nas subclasses, como por exemplo o método removeRange da AbstractList. Neste caso
há uma implementação padrão que é
chamada pelo método clear, podendo ser
alterada para melhorar a performance do clear em casos que
ela não é apropriada, poupando o programador de ter que
reescrever todo o mecanismo.
Outra regra que deve ser seguida é que construtores não
devem chamar métodos sobrescrevíveis, diretamente ou
indiretamente. Isso porque o construtor da superclasse é
executado antes do da subclasse, e assim o método da subclasse
seria chamado antes de seu construtor ter sido executado, com o objeto
em um estado incompleto.
As interfaces Cloneable e Serializable também
devem ser evitadas, pois representam uma tarefa extra para o
programador que herdar a classe. No caso delas serem implementadas, a
mesma regra dos construtores deve ser aplicada nos métodos clone e readObject, uma vez que
passam por processos similares. No caso do readResolve e writeReplace, é
necessário que os métodos sejam colocados com acesso
protected para ficarem visíveis para as subclasses.
Portanto, no caso de classes que não estão preparadas
para herança, o melhor é impedí-la. Isso pode ser
atingido marcando a classe como final ou tornando todos os construtores
privados (e substituindo-os por métodos static factory).
Se não for conveniente proibir a herança, o mais
prático é garantir que a classe nunca chame um de seus
métodos sobrescrevíveis, pois assim sobrescrever um
método não afetará o comportamento de
nenhum
outro. Para isso, pode-se mover o conteúdo desses
métodos para um método auxiliar privado, deixando o
método original apenas com uma chamada para o método
auxiliar. Dentro dos auxiliares, as chamadas a métodos que
seriam
sobrescrevíveis são substituídas pela chamada
direta ao respectivo auxiliar.
Item 16: Prefira interfaces a classes
abstratas
Interfaces e classes abstratas definem um tipo que pode ter
múltiplas implementações.
Implementar uma interface nova em uma classe já existente
é muito fácil, pois basta declará-la e incluir
seus métodos. Já no caso de classes abstratas, seria
necessário colocá-la em algum ponto da hierarquia que
fizesse com que as classes necessárias herdassem dela,
forçando todos os descendentes a estendê-la.
Por isso interfaces são ideais para definir tipos
secundários, ou seja, um comportamento adicional ao seu tipo
primário, como no caso da interface Comparable. Classes
abstratas não são adequadas para essa tarefa pois Java
não permite herança múltipla, e pelo mesmo motivo
citado anteriormente não existe um local razoável para
colocar este tipo dentro da hierarquia de classes. Desta forma, as
interfaces são mais apropriadas para construir e agregar tipos
que não fazem sentido em uma estrutura hierárquica.
Interfaces também são úteis para permitir a
inclusão de funcionalidades através de wrappers, como
demonstrado no exemplo do item 14. Usando apenas classes abstratas, o
programador seria obrigado a usar herança, perdendo
flexibilidade.
É possível combinar as qualidades de interfaces e classes
provendo um esqueleto de implementação da interface em
uma classe abstrata, pois assim o tipo ainda está definido na
interface e o programador que vai utilizá-la pode opcionalmente
utilizar a classe abstrata para auxiliar na
implementação. No caso do programador não poder
estender a classe abstrata, ela ainda pode ser útil sendo
utilizada internamente em uma instância de inner class
anônima, uma técnica conhecida como herança múltipla simulada.
Por convenção, o nome da classe abstrata deve ser AbstractInterface, onde Interface é o nome da
interface que ela implementa. Como essas implementações
foram criadas para serem utilizadas por herança, todas as
orientações descritas no item 15 devem ser consideradas
aqui.
A única vantagem do uso de classes abstratas ao invés de
interfaces é a possibilidade de incluir um novo método
sem quebrar as classes que estão utilizando a classe abstrata,
desde que o método seja concreto. No caso de interfaces, as
classes antigas não estariam implementando o novo método
e portanto não compilariam mais. Portanto, uma vez que a
interface é liberada e está sendo usada amplamente,
é quase impossível alterá-la, e por isso é
necessário estudar cuidadosamente logo de
início como ela deve
ser.
Assim, a regra geral é que uma interface é a melhor
forma de definir um tipo que permite múltiplas
implementações. Uma exceção a essa regra
é o caso em que a facilidade de evolução é
considerada mais importante do que a flexibilidade.
Item 17: Utilize interfaces apenas para
definir tipos
Quando uma classe implementa uma interface, a interface serve como um tipo que pode ser utilizado para
referir-se à instância da classe. É inapropriado
definir uma interface de qualquer outra maneira.
Um exemplo de mau uso é a chamada interface de constantes, que
não contém nenhum método, apenas campos static
final exportando uma constante. As classes usando essas constantes
implementam a interface para não precisarem utilizar um nome de
classe na referência a cada constante. Porém, isso
está expondo a
implementação interna da classe e criando um compromisso
futuro de que a classe sempre implemente esta interface, para garantir
a compatibilidade binária de outras classes que passem a
depender deste fato. Algumas classes da plataforma Java utilizam esta
técnica, porém são anomalias que devem ser
evitadas. Portanto, as constantes devem ficar na própria classe
que são utilizadas ou em uma utility
class (item 3).
Nota da tradução:
a partir do Java 1.5 poderá ser utilizado o mecanismo de static
import.
Item 18: Prefira classes internas static
às não static
Uma classe aninhada é
uma classe definida dentro de outra classe.
Uma
classe interna static é a forma mais simples de classe aninhada.
Ela é uma classe comum que por acaso foi declarada dentro de
outra classe, tendo acesso a todos os membros static da classe externa,
incluindo os privados. Serve como uma classe auxiliar para a classe
externa.
No caso de classes não static, elas ficam associadas a uma
instância da classe externa. Desta forma os métodos
não static da classe interna conseguem acessar, através
da palavra this, membros de
instância da classe externa. A associação entra a
instância da classe externa e interna surge na
criação da segunda, através do construtor ou da
expressão enclosingInstance.newMemberClass(args), e não
pode ser alterada posteriormente. Um uso comum para esse tipo de classe
é para definir um Adapter,
que permite que a instância da classe externa seja vista como
instância de uma classe não relacionada. Exemplo:
public
class MySet extends AbstractSet {
...
public
Iterator iterator() {
return new MyIterator();
}
private class
MyIterator implements Iterator {
...
}
}
Se você declarar uma classe que não requer acesso à
instância da classe externa, marque a classe interna como static,
pois assim as instâncias não precisarão manter a
referência para a classe externa, o que consome tempo e
espaço. Além disso, só no caso de
classes static é
possível obter uma instância da classe interna sem possuir
uma instância da classe externa.
Uma classe anônima não possui nome e é instanciada
juntamente com sua declaração. Seu comportamento é
static quando declarada em um contexto static e não static
quando declarada em um contexto de instância. As classes
anônimas normalmente apenas implementam os métodos
já declarados em sua interface ou superclasse e são
utilizadas apenas no ponto em que são instanciadas. Usos comuns
são objetos de função, como por exemplo uma
instância de Comparator ou objetos de
processos, como Thread, Runnable ou TimerTask. Outros usos
são em métodos static factory ou inicializadores de
campos static final.
Classes locais são o tipo menos utilizado. Elas obedecem
às mesmas regras de escopo de variáveis locais e
só possuem uma instância exterior se declaradas em um
contexto não static. Seu uso é semelhante ao de classes
anônimas, mas podem ter o código reaproveitado em mais de
um ponto.
Bibliografia:
Bloch, Joshua. Effective Java.
Resumo por: Vanessa Sabino