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
1