Fábrica de clase en Python
factory (6)
Soy nuevo en Python y necesito algunos consejos para implementar el escenario a continuación.
Tengo dos clases para administrar dominios en dos registradores diferentes. Ambos tienen la misma interfaz, por ejemplo
class RegistrarA(Object):
def __init__(self, domain):
self.domain = domain
def lookup(self):
...
def register(self, info):
...
y
class RegistrarB(object):
def __init__(self, domain):
self.domain = domain
def lookup(self):
...
def register(self, info):
...
Me gustaría crear una clase de dominio que, con un nombre de dominio, cargue la clase de registrador correcta en función de la extensión, por ejemplo
com = Domain(''test.com'') #load RegistrarA
com.lookup()
biz = Domain(''test.biz'') #load RegistrarB
biz.lookup()
Sé que esto se puede lograr utilizando una función de fábrica (ver a continuación), pero ¿es esta la mejor manera de hacerlo o hay una forma mejor de usar las características de OOP?
def factory(domain):
if ...:
return RegistrarA(domain)
else:
return RegistrarB(domain)
Creo que usar una función está bien.
La pregunta más interesante es ¿cómo se determina qué registrador cargar? Una opción es tener una clase base de registrador abstracta que subclase concreta de implementaciones, luego iterar sobre sus __subclasses__()
llamando a un método de clase is_registrar_for()
:
class Registrar(object):
def __init__(self, domain):
self.domain = domain
class RegistrarA(Registrar):
@classmethod
def is_registrar_for(cls, domain):
return domain == ''foo.com''
class RegistrarB(Registrar):
@classmethod
def is_registrar_for(cls, domain):
return domain == ''bar.com''
def Domain(domain):
for cls in Registrar.__subclasses__():
if cls.is_registrar_for(domain):
return cls(domain)
raise ValueError
print Domain(''foo.com'')
print Domain(''bar.com'')
Esto le permitirá agregar de forma transparente nuevos Registrar
y delegar la decisión de los dominios que cada uno admite.
En Python puedes cambiar la clase real directamente:
class Domain(object):
def __init__(self, domain):
self.domain = domain
if ...:
self.__class__ = RegistrarA
else:
self.__class__ = RegistrarB
Y luego, funcionará.
com = Domain(''test.com'') #load RegistrarA
com.lookup()
Estoy usando este enfoque con éxito.
¿Qué tal algo así como
class Domain(object):
registrars = []
@classmethod
def add_registrar( cls, reg ):
registrars.append( reg )
def __init__( self, domain ):
self.domain = domain
for reg in self.__class__.registrars:
if reg.is_registrar_for( domain ):
self.registrar = reg
def lookup( self ):
return self.registrar.lookup()
Domain.add_registrar( RegistrarA )
Domain.add_registrar( RegistrarB )
com = Domain(''test.com'')
com.lookup()
Tengo este problema todo el tiempo. Si tiene las clases integradas en su aplicación (y sus módulos), entonces puede usar una función; pero si carga los complementos dinámicamente, necesita algo más dinámico: registrar las clases con una fábrica a través de metaclases de forma automática.
Este es un patrón que estoy seguro saqué de originalmente, pero todavía no tengo el camino a la publicación original
_registry = {}
class PluginType(type):
def __init__(cls, name, bases, attrs):
_registry[name] = cls
return super(PluginType, cls).__init__(name, bases, attrs)
class Plugin(object):
__metaclass__ = PluginType # python <3.0 only
def __init__(self, *args):
pass
def load_class(plugin_name, plugin_dir):
plugin_file = plugin_name + ".py"
for root, dirs, files in os.walk(plugin_dir) :
if plugin_file in (s for s in files if s.endswith(''.py'')) :
fp, pathname, description = imp.find_module(plugin_name, [root])
try:
mod = imp.load_module(plugin_name, fp, pathname, description)
finally:
if fp:
fp.close()
return
def get_class(plugin_name) :
t = None
if plugin_name in _registry:
t = _registry[plugin_name]
return t
def get_instance(plugin_name, *args):
return get_class(plugin_name)(*args)
Suponiendo que necesita clases separadas para diferentes registradores (aunque no es obvio en su ejemplo) su solución se ve bien, aunque RegistrarA y RegistrarB probablemente compartan la funcionalidad y podrían derivarse de una Clase base abstracta .
Como alternativa a su función de factory
, puede especificar un dict, mapeo a sus clases de registrador:
Registrar = {''test.com'': RegistrarA, ''test.biz'': RegistrarB}
Entonces:
registrar = Registrar[''test.com''](domain)
Una objeción: en realidad no estás haciendo una Fábrica de clases porque estás devolviendo instancias en lugar de clases.
Puede crear una clase ''contenedora'' y sobrecargar su __new__()
para devolver instancias de las subclases especializadas, por ejemplo:
class Registrar(object):
def __new__(self, domain):
if ...:
return RegistrarA(domain)
elif ...:
return RegistrarB(domain)
else:
raise Exception()
Además, para tratar condiciones no mutuamente excluyentes, un problema que surgió en otras respuestas, la primera pregunta que debe hacerse es si desea que la clase contenedora, que desempeña el papel de despachador, gobierne las condiciones, o lo delegará en las clases especializadas. Puedo sugerir un mecanismo compartido, donde las clases especializadas definen sus propias condiciones, pero el contenedor hace la validación, así (siempre que cada clase especializada exponga un método de clase que verifique si es un registrador para un dominio particular, is_registrar_for (. ..) como se sugiere en otras respuestas):
class Registrar(object):
registrars = [RegistrarA, RegistrarB]
def __new__(self, domain):
matched_registrars = [r for r in self.registrars if r.is_registrar_for(domain)]
if len(matched_registrars) > 1:
raise Exception(''More than one registrar matched!'')
elif len(matched_registrars) < 1:
raise Exception(''No registrar was matched!'')
else:
return matched_registrars[0](domain)