Capítulo 4 – Classes e
Interfaces
Item 13 – Minimize a acessibilidade de
classes e seus membros
Item 14 – Em classes públicas, utilize
métodos de acesso, não campos públicos
Item 15 – Minimize a mutabilidade
·
Sempre
que possível, crie classes imutáveis (não possuem métodos mutators (setters),
todos os campos são private, marcar os campos como final, a classe não pode ser
estendida, etc). Caso existam operações sobre seus atributos, retorne cópias da
classe com os novos valores e não modifique o estado interno.
·
Pode-se
disponibilizar fabricas estáticas (Item 1) e prover cache de instancias que já
foram criadas. Todos os wrapper de primitivos funcionam dessa forma.
OBS: Observe que, ser imutável não implica
automaticamente que a classe é um singleton.
Uma classe imutável pode ter várias instancias com valores diferentes na
memória. Diferente do singleton onde só existe uma instância da classe em
memória e seu estado interno não muda na criação. A abordagem de criar uma fábrica estática
pode ser utilizada em ambas as soluções. No singleton por motivos óbvios, e nas
classes imutáveis para fazer um controle mais refinado das instancias que serão
retornadas.
·
Concluindo,
resista à tentação de escrever um método set para cada get criado. Classes
devem ser imutáveis a não ser que exista uma boa razão para ser diferente disso.
Item 16 – Utilize composição ao invés da
herança
Herança viola o
encapsulamento. Subclasses dependem de detalhes de implementação de suas
superclasses e assim são suscetíveis às mudanças que podem ocorrer nestas. Vejamos
um exemplo
// Broken - Inappropriate use of inheritance!
public class InstrumentedHashSet extends HashSet {
// The number of attempted element insertions
private int addCount = 0;
public InstrumentedHashSet() {}
public InstrumentedHashSet(int initCap, float loadFactor) {
super(initCap, loadFactor);
}
@Override public boolean add(E e) {
addCount++;
return
super.add(e);
}
@Override public boolean addAll(Collection c) {
addCount += c.size();
return
super.addAll(c); // Na implementação
original o addAll utiliza outro método publico
// da classe, o add. Quando
super.addAll é chamado vai executar o
// add. Por conta do polimorfismo, o add sobrescrito
aqui é que será
// chamado e incrementará o addCount duas vezes.
}
public int getAddCount() {
return
addCount;
}
}
Perceba como o problema é sutil. Perceba também
o efeito do polimorfismo! Mesmo chamando super.addAll quando esse método
executar vai chamar o add sobrescrito pela classe InstrumentedHashSet. O fato
do método addAll original utilizar um outro método publico (ou protegido) da
classe é um fator de fragilização da implementação. Deveria estar bem
documentado e quem fosse
·
estender
a classe HashSet tem a obrigação de levar em consideração. Mas não se cobre
tanto. A API do Java tinha e ainda tem diversas classes com inúmeros problemas
de projeto.
·
Novos
métodos podem ser acrescentados na superclasse e isso também pode quebrar o
funcionamento da subclasse.
·
Um
bom substituto na maior parte dos casos onde herança pode ser aplicada é a
utilização do Decorator Pattern.
Item 17 – Projete e documente para herança
ou então proíba
Herança é segura
quando utilizada em classes internas da API, sobre o controle da mesma equipe.
Se deixar uma classe da API passível de extensão deve-se tomar algumas precauções.
Uma classe
projetada para ser estendida deve documentar com precisão detalhes de
implementação de seus métodos públicos e protegidos. Mais precisamente, deve
documentar quando ela mesma fizer uso de seus próprios métodos. A documentação
deve incluir quais métodos utiliza, em qual sequencia e como o resultado de
cada invocação afeta o próximo processamento. Esta documentação aparece por
padrão iniciando pela frase “Esta implementação ...”.
Uma classe mais
segura para extensão nunca invoca algum método passível de sobreescrita (que é
a fonte de todo mal).
public class Super {
// Broken - constructor invokes an overridable method
private int array[];
public Super() {
overrideMe();
}
public void overrideMe() {
array = {1,2,3,4}; //O array nunca sera
inicializado quando a classe Sub for instanciada. O
//polimorfismo faz com que o
método chamado seja o overrideMe
//sobrescrito.
}
}//fim da classe Super
public final class Sub extends Super {
private final Date date;
Sub() {
date = new Date(); //Quando a classe Sub é
instanciada, primeiramente o constructor de Super é
// chamado. Este por sua vez
chama o método overrideMe sobrescrito na
// classe Sub, o qual depende da inicialização
de date que ainda não ocorreu.
}
// Overriding method invoked by superclass constructor
@Override public void overrideMe() {
System.out.println(date);
}
}
//fim da classe Sub
Item 18 – Prefira interfaces a classes
abstratas
Há duas maneiras de se definir um tipo abstrato de dado em Java: com
classes abstratas ou através de interfaces. As principais diferenças são:
- ·
Interface
não admite implementar algum dos métodos declarados, uma classe abstrata sim.
- ·
Para
definir um tipo usando classe abstrata é preciso fazer uso de herança, enquanto
que com interfaces basta implementar todos os métodos do contrato.
- ·
Como
Java admite apenas herança simples, o uso de classes abstratas para definir
tipos fica seriamente comprometido.
- ·
Classes
podem ser facilmente refatoradas para implementar uma interface, basta
acrescentar o implements e
implementar os métodos adicionais. Classes abstratas geram um efeito colateral
muito maior, forçando todos os descendentes estender a nova classe abstrata,
sendo isso apropriado ou não para eles.
- ·
Interfaces
podem ser usadas para criar misturas de comportamentos não necessariamente
relacionados. Comparable é um tipo
que gera esse efeito. O tipo primário de uma classe pode não ter nada a ver com
o fato dela poder ser comparada com outros objetos mutualmente
equivalentes. Uma interface permite ir
adicionando comportamentos desejáveis sem ferir a coesão. Com a herança simples
isso não pode ser feito com o uso de classes abstratas.
- ·
Criar
hierarquias de tipos através de herança é arriscado pois é uma decisão muito
forte que vai restringir seu projeto para sempre. Isso na maioria das vezes é
ruim porque, em geral, você não tem todas as informações sobre como uma classe
vai evoluir e nem quais serão todas as necessidades dos usuários num futuro
próximo. Na maioria das vezes, criar hierarquias mais flexíveis através de
interfaces é a melhor solução.
Apesar de interfaces não permitirem a implementação de métodos, isso
não impossibilita prover assistência aos programadores. Você pode combinar as
virtudes das interfaces e classes abstratas para criar classes abstratas com
esqueletos de implementação para cada interface não trivial que você pretende
exportar. A API de Collections utiliza extensivamente esse recurso através de
suas implementações padrão de AbstractCollection, AbstractList, AbstractSet e
AbstractMap.
//
Skeletal Implementation
public
abstract class AbstractMapEntry
implements Map.Entry {
// Primitive operations
public abstract K getKey();
public abstract V getValue();
// Entries in modifiable maps must override
this method
public V
setValue(V value) {
throw new UnsupportedOperationException();
}
// Implements the general contract of
Map.Entry.equals
@Override public boolean equals(Object o) {
if (o == this)
return true;
if (! (o instanceof Map.Entry))
return false;
Map.Entry arg = (Map.Entry) o;
return equals(getKey(), arg.getKey())
&& equals(getValue(), arg.getValue());
}
private static boolean equals(Object o1,
Object o2) {
return o1 == null ? o2 == null :
o1.equals(o2);
}
// Implements the general contract of
Map.Entry.hashCode
@Override
public int hashCode() {
return hashCode(getKey()) ^
hashCode(getValue());
}
private static int hashCode(Object obj) {
return obj == null ? 0 : obj.hashCode();
}
}
A grande vantagem dessa abordagem é que ela provê a assistência das
classes abstratas sem impor as restrições de quando se define um tipo somente
através destas. Para muitos implementadores da interface a escolhe óbvia é estender
a classe esqueleto, porém isso é estritamente opcional. Se uma classe não pode
estender a implementação esqueleto, ainda poderá implementar a interface e
delegar a execução de seus métodos para uma instância privada de uma classe
interna que estende da classe abstrata esqueleto (semelhante ao que foi feito
no item 16).
Para concluir, projete com cuidado suas interfaces. Uma vez que esta
for disponibilizada, é quase impossível alterá-la. Você tem que fazer certo da
primeira vez. Acrescentar um método tardiamente irá quebrar qualquer código que
faça uso da interface. Ter vários programadores na definição das interfaces é
uma boa estratégia.
Item 19 – Use interfaces apenas para
definir tipos
Não usar interfaces como um container para acomodar constantes.
// Constant interface
antipattern - do not use!
public interface
PhysicalConstants {
// Avogadro's number
(1/mol)
static final double
AVOGADROS_NUMBER = 6.02214199e23;
// Boltzmann constant (J/K)
static final double
BOLTZMANN_CONSTANT = 1.3806503e-23;
// Mass of the electron
(kg)
static final double
ELECTRON_MASS = 9.10938188e-31;
}
Esse é um exemplo de mal uso de interfaces
Item 20 – Prefira hierarquia de classes a
usar flags
É comum encontrarmos implementações inexperientes onde o comportamento
de uma classe é regido pelo valor de uma flag. Essa flag geralmente é passada
como parâmetro no construtor e vai decidir o comportamento da classe dentro de
seus métodos. Os métodos terão comandos if
ou switch em cada um dos valores
possíveis que a flag pode assumir.
Esse tipo de artifício é uma forma muito pobre de simular hierarquia
de classes, um dos principais conceitos da orientação a objetos.
Item 21 – Use function objects para representar estratégias
·
Algumas
linguagens provêm maneiras para referenciar funções. Isso é útil, por exemplo,
para implementar o design pattern Strategy. Um método pode receber um ponteiro
para uma função a qual será chamada para executar alguma estratégia específica.
Uma classe que realiza ordenação poderia ser implementada dessa forma,
recebendo como parâmetro uma função com a implementação da estratégia de
comparação adequada.
·
Como
Java não possui ponteiros, o padrão Strategy pode ser implementado
declarando-se uma interface para representar a estratégia e uma classe que
implementa esta interface para cada estratégia concreta que exista. Essa classe
terá apenas um método e nenhum estado e é conhecida como um Function Object. No
exemplo do comparator (acima), uma interface definirá o método compare e para cada estratégia concreta
que exista haverá uma implementação dessa interface. Quando essa implementação
concreta é utilizada apenas uma vez, usualmente é definida como classe anônima.
Quando houver mais de uma utilização, poderá ser uma variável estática privada
exportada por um campo public static
final cujo tipo é a interface da estratégia.
Item 22 – Favoreça o uso de classes
internas estáticas
Existem 4 tipos de
classes internas (nested): classes estáticas, não estáticas, anônimas e classes
locais.
·
Classes
estáticas: se a classe interna não precisa fazer nenhuma referência à sua
classe encapsuladora, então você deve
fazê-la estática. Isso evita que uma referência extra à classe encapsuladora
seja criada e gerenciada pelo gc. Um exemplo seria uma classe enum que descreve
as operações suportadas por uma calculadora. A classe de enum Operation poderia ser criada e ser pública e
estática na classe Calculator. Clientes da classe Calculator poderiam
referenciar as operações usando nomes como Calculator.Operations.PLUS.
·
Classes
não estáticas: cada classe interna não estática possui uma referência implícita
à sua classe encapsuladora e assim pode chamar os métodos de instância ou usar
o this. Abaixo segue um exemplo de
seu uso:
// Typical use of a nonstatic member class
public class MySet extends AbstractSet {
... // Bulk of the class omitted
public Iterator iterator() {
return new MyIterator();
}
/*
Uma classe utilitaria
que implementa um iterador para os elementos da classe principal. Como precisará
acessar os dados dessa classe principal, foi feita não estática.
*/
private class MyIterator implements Iterator {
...
}
}
·
Classes
anônimas: são úteis para a criação de function
objetcs (item 21).
·
Classes
locais: é a menos utilizada. Pode ser declarada da mesma maneira que variáveis
locais.