segunda-feira, 2 de abril de 2007

Classes singleton

Semestre passado estávamos conversando sobre singleton (veja também a página em inglês), que é quando queremos que uma determinada classe tenha somente uma instância, e esta instância seja retornada na tentativa de se criar novas instâncias.

Na época escrevi um artigo sobre o assunto e agora resolvi atualizá-lo aqui.

A idéia inicial era demonstrar como é implementado singleton em algumas linguagens e vou seguir novamente o mesmo princípio.

Java


Em Java singleton é feito «bloqueando» o construtor da classe e usando um método para intermediar esta requisição (aliás esta é a abordagem da maioria das linguagens):
class Conexao {
//Atributos

private static Conexao inst = null;

//Metodos
private Conexao() {
// O construtor é privado!

}

public static Conexao nova() {
// Este método fará as vezes do construtor
if (inst == null)
inst = new Conexao();
return inst;
}

}


Então, para criar uma conexão, não será usado new, mas o método nova():
Conexao conn = Conexao.nova();


Funciona. =)

O grande problema é que singleton não é transparente em Java. É preciso estar atento e não tentar usar new, que é a forma natural de se obter uma instância de classe. =P

Ruby


É extremamente simples fazer singleton em Ruby:
requires 'singleton'

class Conexao
include Singleton

end


Bastou um include! Isto é incrivelmente simples, prático. Quem me explicou seu funcionamento foi o Walter:

A solução de Ruby é parecida com Java e Python.

A inclusão do módulo singleton torna o construtor da classe privado. A instância é criada no momento de instanciação. Os métodos clone e dup retornam erro.

E isso tudo escondidinho, em 343 linhas de código invísiveis ao usuário :)


Python (usando herança)


Uma forma de fazer singleton em Python é usando herança. Assim podemos criar uma classe Singleton que implemente a funcionalidade (feature) desejada e herdar as demais classes dela.

Bem, em Python o construtor é __init__(), mas há um método que é chamado antes do construtor, que é __new__(), e funciona de forma muito parecida com new de Perl. Ele cria a nova instância e a retorna.

Na verdade, em Python quando fazemos:
a = X(b, c)


Por trás o que está acontecendo é:
a = X.__new__(X, b, c)
a.__init__(b, c)


Então podemos sobrescrever este método para termos singleton. A idéia é que, se já houver uma instância, o __new__() retorne esta instância em vez de uma nova:
class Singleton(object):

__inst = None

def __new__(cls, *args, **kw):
if cls.__inst is None:
cls.__inst = object.__new__(cls)
return cls.__inst

def __copy__(self):
return self

def __deepcopy__(self, memo=None):
return self


Então temos o atributo de classe privado __inst que contém nada (None) na criação da classe, mas depois será uma referência à instância única.

O método __new__() está preparado para receber argumentos genéricos (*args, **kw), mas não fará nada com eles – é problema do __init__(). A função do __new__() é verificar se já existe a instância única, se não existe, cria, depois retorna ela.

Podemos usar a herança desta forma:
class Conexao(Singleton):


Ou seja, funciona exatamente como em Ruby.

Agora vamos à crítica!

O grande problema aqui é justamente a sobrescrita um método geralmente esquecido…

Isso pode gerar uma série de problemas quando surge a herança múltipla ou quando o método é novamente sobrescrito – ou foi também sobrescrito por outra classe pai.

Para contornar estes problemas uma boa saída é o uso de metaclasses.

Python usando metaclasse


Em Python classes são objetos! São instâncias de type. Metaclasses são classes filhas de type, portanto suas instâncias também são classes.

Podemos criar uma metaclasse que tenha procedimentos que precedam o construtor e o chamem só se for preciso.

Assim as classes instância da metaclasse podem ser filhas de outras classes e possuir até herança múltipla sem problemas com sobrescrita de métodos – pois todo tratamento é realizado num escopo ainda mais protegido.

Numa metaclasse o método que precede o construtor das classes instância é – por motivos óbvios para programadores Python – __call__().

Vamos criar então a metaclasse! Vou chamar de unique em vez de singleton (Por quê? Porque eu gosto, ora pois!):
class unique(type):

def __init__(cls, name, base, dict):
super(unique, cls).__init__(name, base, dict)
cls.__inst = None
cls.__copy__ = lambda self: self
cls.__deepcopy__ = lambda self, memo=None: self

def __call__(cls, *args, **kw):
if cls.__inst is None:
cls.__inst = super(unique, cls).__call__(*args, **kw)
return cls.__inst


O construtor chama o construtor da classe pai (super) e então a única coisa diferente que ele faz é a criação de um atributo privado de classe __inst e de duas funções para tratar as cópias. Só que este atributo __inst é «mais privado» do que os normais. =)

Aqui o escopo para o atributo privado não é a classe, mas unique (a metaclasse). Podem imaginar o nível de proteção disso? É como em C++, só que funciona. =)

Bem, o método __call__() faz exatamente o mesmo que __new__() no exemplo que usa herança, só que de forma mais eficiente. A implementação disso poderia ser:
class Conexao(object):

__metaclass__ = unique



Simples! E sem os problemas que poderiam vir com o uso de herança.

Mas aí vem um espírito de porco qualquer e pergunta:

Peraí! E se eu quiser usar também outra metaclasse, como autoprop?

Ahá! Não é tão complicado assim! Veja só:
class Conexao(object):

class __metaclass__(autoprop, unique):
pass



Lua


Em Lua, assim como em Perl, o construtor é um método de classe, o que facilita muito nosso trabalho, pois cada instância deve ser criada nele.

Uma classe é uma metatabela cuja chave __index referencia a si mesma:
local Conexao = {}
Conexao.__index = Conexao


O construtor padrão (nu e cru) costuma ser:
function Conexao:new(o)
o = o or {}
return setmetatable(o, self)
end


Vamos então criar uma função unique (porque eu gosto!) que retorne uma classe cujo construtor retorne sempre a mesma instância (usando closure para isso):
local function unique()
local inst
local cmt = {}
cmt.__index = cmt

cmt.new = function(self, o)
if not inst then
o = o or {}
inst = setmetatable(o, self)
if self.__init then
inst.__init()
end
end
return inst
end

return cmt
end


Então podemos criar nossa classe Conexao assim:
local Conexao = unique()


Os demais métodos podem ser implementados normalmente e, caso seja necessário implementar alguma coisa no construtor, é possível implementando o método de instância __init().

[]'s
Rodrigo Cacilhas
blog comments powered by Disqus