Singleton

Objetivo

Restringir a instanciação de objetos de uma classe a um único objeto.

Propósito

Ao definir uma classe normalmente esperamos utilizá-la para criar vários objetos dessa mesma categoria cada um com o seu estado. Por outro lado, em uma linguagem orientada a objetos esperamos poder manipular qualquer recurso como sendo um objeto. Acontece, que às vezes, o recurso que estamos utilizando é único e portanto apenas um objeto pode ser criado para o acessar.

Mas o que significa o recurso ser único ? Significa que não será possível existir mais do que um em qualquer ponto do tempo. Poderíamos pensar, por exemplo que o CPU é único. Isso é verdade hoje, mas pode não ser verdade amanhã; logo, o CPU não é um recurso candidato a ser modelado por um único objeto. Um exemplo de recurso único é o ambiente em que a aplicação é executada ( o runtime). Cópias da aplicação podem ser executada em vários ambientes mas do ponto de vista da aplicação ela apenas executa em um único. No caso do java as aplicações correm em uma JVM e apenas uma. Quando temos sistemas distribuídos temos na realidade cópias da aplicação,ou aplicações diferentes que se conversam rodando em JVM diferentes.

O propósito do padrão Singleton é possibilitar o uso do objeto que mapeia este tipo de recurso único, mas simultaneamente impedir que mais do que uma instancia desse tipo de objeto possa existir em qualquer ponto do tempo.

Porque é único, o objeto será global, ou seja, sempre o mesmo em qualquer ponto da aplicação. Contudo, objetos globais não são necessariamente únicos e por isso não ha uma equivalência entre objetos que seguem o padrão Singleton e objetos globais.

Implementação

Implementar um Singleton não é tão fácil como parece. Ao implementar um Singleton é necessário cuidar de não permitir formas de duplicar o objeto que existe nem permitir criar outros.Como vimos um Singleton é um objeto global acessível em qualquer ponto da aplicação.Isso significa que ele pode ser utilizado em simultâneo em várias threads.

Outro cuidado é com a serialização. Normalmente se o recurso se refere a algum componente físico da máquina ou do ambiente de execução não faz sentido serializar esse objeto. Se o recurso em causa diz respeito a um domínio de aplicação em que o recurso é único apenas do ponto de vista lógico da abstração, e não tem um vinculo ao mundo físico, então pode ser possível serializar o objeto. Contudo esta necessidade é, na prática, rara. O problema é que, quando é necessária, ela abre a porta para mecanismos que permitem duplicar o objeto ou criar vários objetos da classe , e isto viola diretamente o propósito do padrão. Um outro mecanismo que pode ser usado para driblar a unicidade do objeto Singleton é a API de Reflection.

Para implementarmos um Singleton corretamente temos que ter atenção a todos estes aspectos. Analizemos então as propriedades que a implementação deve ter:

  1. Apenas a classe pode controlar a instanciação do objeto

  2. Protegida de duplicação por introspecção (Reflection)

  3. Protegida de duplicação por serialização

A partir do Java 5 a linguagem conta com o tipo enum. Objetos do tipo enum não são singletons mas têm todas as propriedades necessárias para serem singletons. São resistentes a serialização, introspeção e a classe é a única a controlar a instanciação.Aliás, no caso do enum existe o suporte da própria JVM. Por isto, a forma mais fácil de criar um Singleton hoje em dia é utilizar um enum que só tenha uma instância possivel.

public enum Desktop {
	INSTANCIA;

	// métodos normais
}

E nem ha necessidade de declarar como final porque não é possível criar subclasses de enum

Utilizar esta forma de implementação é ideal no caso do Singleton ser único a nível logico sem nenhuma ligação a um artefato físico. Para essas outras ocasiões especiais este mecanismo não serve tão bem, como o caso do próprio Desktop.

Um desktop é único, mas ele pode não existir. Ou seja, ou existem zero instancias de desktop ou existe exatamente uma,e uma apenas. O desktop é um recurso do sistema operacional (SO) e portanto existe uma dependência inerente com o SO. Além disso para cada SO, o desktop pode ter funcionalidades diferentes. Então, para este tipo de singletons, ou quando estiver usando uma versão do java anterior à 5, a implementação seria mais como a mostrada a seguir:

public final class Desktop {
	private static final Desktop ME = new Desktop();

	public static Desktop getDesktop(){
		return ME;
	}

	// Esconde o construtor.
	private Desktop(){}

	// métodos normais
}

Repare-se como a instancia é criada com new e colocada numa variável final o que a torna imutável pelo tempo de execução da aplicação. Sendo um atributo da classe ( static ) é garantido que será inicializado antes de qualquer método da classe ser invocado.

O construtor privado assegura que apenas a classe pode criar o objeto e de quebra que a classe não poderá ser estendida por uma subclasse. O final na declaração da classe parece redundante mas reforça o mesmo conceito especialmente quando reflexão ou outra técnica de introspecção for usada.

Inicialização e Lazy loading

Quando o singleton mapeia um recurso do SO ou até um recurso físico ou periférico, é comum ser necessário vincular o objeto ao recurso através de algum processo de inicialização. Este processo em si mesmo pode ser demorado e consumir memória,processamento,ou requerer sincronismo entre threads diferentes. Por isso, é desejável que ele seja executado apenas quando necessário.

A inicialização pode acontecer no construtor, mas preferivelmente deveria acontecer em um método. Contudo, dado que a classe não pode ser estendida, não ha vantagem em usar um método para isto pois ele nunca será sobre-escrito. Isto não modifica o design anterior, pois basta colocar o codigo de inicialização dentro do construtor.

Para adiar o processo de inicialização é comum utilizar um processo conhecido como Lazy Loading e é comum ver implementações dele desta forma:

public final class Desktop {
	private static final Desktop ME;

	public static Desktop getDesktop(){
		if ( ME == null ){
			ME = new Desktop();
		}
		return ME;
	}

	// Esconde o construtor.
	private Desktop(){
		//inicialização
	}

	// métodos normais
}

Só que esta forma de implementação tem vários problemas [[1]] . Por exemplo, não é determinista em ambientes multi-thread podendo levar à criação de mais do que um objeto. Isso leva a considerar que o método getDesktop deveria ser sincronizado e optaríamos por uma versão onde adicionaríamos a palavra synchronized ao método getDesktop.

Mas, analisando melhor, o que realmente interessa sincronizar é a criação do objeto. Uma vez criado não teremos mais problemas com o sincronismo. Esta conclusão leva a inplementar o método desta forma:

public final class Desktop {
	private static final Desktop ME;

	public static  Desktop getDesktop(){
		if ( ME == null ){
			synchronized(Desktop.class) {
		      ME = new Desktop();
		    }
		}
		return ME;
	}

	// o resto do codigo

}

Só que esta implementação, ainda não é suficiente para garantir sincronismo [[2]] o que leva as pessoas a criarem um anti-padrão desta forma:

public final class Desktop {
	private static final Desktop ME;

	public static  Desktop getDesktop(){
		if ( ME == null ){
			synchronized(Desktop.class) {
				if ( ME == null ){
		      		ME = new Desktop();
		      	}
		    }
		}
		return ME;
	}

	// o resto do codigo

}

A dupla verificação não funciona, embora pareça que sim, e constitui um anti-padrão grave. A solução real para este problema passa pelo uso da palavra reservada volatile:

public final class Desktop {

	private static final volatile Desktop ME;

	public static  Desktop getDesktop(){
		if ( ME == null ){
			synchronized(Desktop.class) {
				if ( ME == null ){
		      		ME = new Desktop();
		      	}
		    }
		}
		return ME;
	}

	// o resto do codigo

}

Só que também esta solução era problemática por que nem todas as JVM implementavam o controle associado a volatile da mesma forma. Apenas após a versão 5 do Java, quando foi especificado o comportamento esperado associado a esta palavra é que se tornou seguro usá-la. Portanto, para código escrito em java 1.4 ou mais antigo, a solução é tornar todo o método sincronizado , para a versão 5 ou superior é utilizar a palavra volatile . Para singletons lógicos , em java 5 ou superior é melhor utilizar enum . Singletons lógicos são raros e provavelmente sempre os pode substituir pelo uso de outro padrão como Registry ou Shared Object.

Discussão

Singleton é um dos padrões mais incompreendidos de sempre. O objetivo não é simplesmente limitar o numero de objetos que podem ser criados e muito menos é tornar o objeto acessível globalmente (existem outros padrões para isto como Registry). O objetivo é que só seja possível criar um e nunca mais do que um objeto.

O problema vem da confusão entre Singleton e Shared Object. Um Shared Object é um objeto partilhado entre vários outros objetos. Ele também é acessivel globalmente e normalmente também basta ter apenas uma instancia. Qualquer objeto, de qualquer classe, pode ser usado como Shared Object. Poderiamos escolher instanciar vários objetos dessa classe, apenas escolhemos usar um. A diferença para um Singleton é que não podemos escolher criar mais do que um objeto, a classe simplesmente não permite. Todo o Singleton é um Shared Object, mas o inverso não é verdade.

Na prática e na grande maioria dos casos o que precisamos é de um Shared Object e não de um Singleton. Motores de injeção modernos automatizam o uso de Shared Object controlando que cada classe só é instanciada uma vez. Poderíamos instanciar mais do que uma, mas uma vez é suficiente e isto nos ajuda a poupar memoria. Contudo, se precisarmos muito, podemos configurar o motor de injeção para injetar uma instancia nova cada vez. Quando a classe é um Singleton não temos como criar mais do de uma instancia. Simplesmente não é possível.

Exemplos na API padrão

O uso do padrão Singleton é raro mas podemos encontrar o seu uso em locais em que o JSE precisa utilizar recursos do SO ou da JVM. Exemplos de Singleton na JSE são a classe Runtime - associada ao ambiente onde a aplicação está rodando e a classe Desktop associada à área de trabalho do usuário. De notar que Desktop é um singleton opcional, ou seja, pode não existir um (para OS que rodam sem modo de janelas).

Padrões associados

O padrão Singleton é relacionado à criação do objeto que garante que apenas um, e um só, objeto existe em qualquer ponto da execução da aplicação e a qualquer momento. Ele se relaciona ao padrão Static Factory Method * por utilizar um método estático para dar acesso ao objeto em vez de utilizar uma variável publica. O uso de *Static Factory Method permite fazer lazy loading e contornar problemas relacionados a sincronismo em ambiente multi-thread.

O padrão Singleton define um objeto único para toda a execução acessado através de métodos estáticos. Isto significa que esse objeto é global. Contudo, se o seu objetivo é apenas ter um objeto global cuja classe não tem qualquer restrição a quantas instancias podem ser criadas, utilizar o padrão Shared Object será bem mais útil. Caso precisa utilizar vários objetos globais a melhor opção recai no padrão Registry.

Bibliografia

[1] Joshua Bloch Effective Java, 2nd Edition: Fonenix inc (2008)

[2] Double-checked locking and the Singleton pattern, Peter Haggar (http://www.ibm.com/developerworks/java/library/j-dcl.html)

[3] [.book=0201743973] Design Patterns Java Workbook

Scroll to Top