20 março, 2013

Resumo Effective Java - Parte 3 (Classes e Interfaces)

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.

Nenhum comentário: