Reputation:
I'm new to Python and need some advice implementing the scenario below.
I have two classes for managing domains at two different registrars. Both have the same interface, e.g.
class RegistrarA(Object):
def __init__(self, domain):
self.domain = domain
def lookup(self):
...
def register(self, info):
...
and
class RegistrarB(object):
def __init__(self, domain):
self.domain = domain
def lookup(self):
...
def register(self, info):
...
I would like to create a Domain class that, given a domain name, loads the correct registrar class based on the extension, e.g.
com = Domain('test.com') #load RegistrarA
com.lookup()
biz = Domain('test.biz') #load RegistrarB
biz.lookup()
I know this can be accomplished using a factory function (see below), but is this the best way of doing it or is there a better way using OOP features?
def factory(domain):
if ...:
return RegistrarA(domain)
else:
return RegistrarB(domain)
Upvotes: 75
Views: 89966
Reputation: 2002
Okay, here is an answer based on the answer of Alec Thomas, modified and extended: taking care of multi-level inheritance and ambiguity. If _resolve should be something more complicated than simple check of uniqueness and is likely to change it may be supplied as an argument and not be a class method.
base class module bbb.py:
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Sequence, Type
class Base(ABC):
def __init__(self, *args, **kwargs):
...
@classmethod
def isit(cls, _s: str) -> bool:
return False
@classmethod
def from_str(cls, s: str, *args, **kwargs) -> Base:
subs = cls._findit(s)
sc = cls._resolve(s, subs)
return sc(*args, **kwargs)
@classmethod
def _findit(cls, s: str) -> Sequence[Type[Base]]:
subs = [cls] if cls.isit(s) else []
subs += [ssc for sc in cls.__subclasses__() for ssc in sc._findit(s)]
return subs
@classmethod
def _resolve(cls, s: str, subs: Sequence[Type[Base]]) -> Type[Base]:
if len(subs) == 0:
raise Exception(f'Cannot find subclass for {s}')
if len(subs) > 1:
raise Exception(
f'Cannot choose unique subclass for {s}: {subs}')
sc = subs[0]
return sc
class B(Base):
@classmethod
def isit(cls, s: str) -> bool:
res = s == 'b class'
return res
enter code here
derived class module ccc.py:
from bbb import Base
class C(Base):
@classmethod
def isit(cls, s: str) -> bool:
res = s == 'c class'
return res
class CC(Base):
@classmethod
def isit(cls, s: str) -> bool:
res = s == 'cc class'
return res
How to use:
In [4]: from bbb import Base
In [5]: import ccc
In [6]: Base.from_str('b class')
Out[6]: <bbb.B at 0x1adf2665288>
In [7]: Base.from_str('c class')
Out[7]: <ccc.C at 0x1adf266a908>
In [8]: Base.from_str('cc class')
Out[8]: <ccc.CC at 0x1adf2665608>
Upvotes: 0
Reputation: 6037
Since the methods are probably shared, using some base class would make sense.
getattr
can be used in the factory function to dynamically call another class.
The logic to figure out the registrartype should not be part these classes, but should be in some helper function.
import sys
class RegistrarBase():
"""Registrar Base Class"""
def __init__(self, domain):
self.name = domain
def register(self, info):
pass
def lookup(self):
pass
def __repr__(self):
return "empty domain"
class RegistrarA(RegistrarBase):
def __repr__(self):
return ".com domain"
class RegistrarB(RegistrarBase):
def __repr__(self):
return ".biz domain"
def create_registrar(domainname, registrartype):
try:
registrar = getattr(sys.modules[__name__], registrartype)
return registrar(domainname)
except:
return RegistrarBase(domainname)
domain = create_registrar(domainname = 'test.com', registrartype='RegistrarA')
print(domain)
print(domain.name)
#.com domain
#test.com
Upvotes: 0
Reputation: 14080
Assuming you need separate classes for different registrars (though it's not obvious in your example) your solution looks okay, though RegistrarA and RegistrarB probably share functionality and could be derived from an Abstract Base Class.
As an alternative to your factory
function, you could specify a dict, mapping to your registrar classes:
Registrar = {'test.com': RegistrarA, 'test.biz': RegistrarB}
Then:
registrar = Registrar['test.com'](domain)
One quibble: You're not really doing a Class Factory here as you're returning instances rather than classes.
Upvotes: 24
Reputation: 597
Here a metaclass implicitly collects Registars Classes in an ENTITIES dict
class DomainMeta(type):
ENTITIES = {}
def __new__(cls, name, bases, attrs):
cls = type.__new__(cls, name, bases, attrs)
try:
entity = attrs['domain']
cls.ENTITIES[entity] = cls
except KeyError:
pass
return cls
class Domain(metaclass=DomainMeta):
@classmethod
def factory(cls, domain):
return DomainMeta.ENTITIES[domain]()
class RegistrarA(Domain):
domain = 'test.com'
def lookup(self):
return 'Custom command for .com TLD'
class RegistrarB(Domain):
domain = 'test.biz'
def lookup(self):
return 'Custom command for .biz TLD'
com = Domain.factory('test.com')
type(com) # <class '__main__.RegistrarA'>
com.lookup() # 'Custom command for .com TLD'
com = Domain.factory('test.biz')
type(com) # <class '__main__.RegistrarB'>
com.lookup() # 'Custom command for .biz TLD'
Upvotes: -1
Reputation: 347
You can create a 'wrapper' class and overload its __new__()
method to return instances of the specialized sub-classes, e.g.:
class Registrar(object):
def __new__(self, domain):
if ...:
return RegistrarA(domain)
elif ...:
return RegistrarB(domain)
else:
raise Exception()
Additionally, in order to deal with non-mutually exclusive conditions, an issue that was raised in other answers, the first question to ask yourself is whether you want the wrapper class, which plays the role of a dispatcher, to govern the conditions, or it will delegate it to the specialized classes. I can suggest a shared mechanism, where the specialized classes define their own conditions, but the wrapper does the validation, like this (provided that each specialized class exposes a class method that verifies whether it is a registrar for a particular domain, is_registrar_for(...) as suggested in other answers):
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)
Upvotes: 9
Reputation: 1025
I have this problem all the time. If you have the classes embedded in your application (and its modules) then you can use a function; but if you load plugins dynamically, you need something more dynamic -- registering the classes with a factory via metaclasses automatically.
Here is a pattern I'm sure I lifted from StackOverflow originally, but I don't still have the path to the original post
_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)
Upvotes: 4
Reputation: 766
how about something like
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()
Upvotes: 1
Reputation: 21463
In Python you can change the actual class directly:
class Domain(object):
def __init__(self, domain):
self.domain = domain
if ...:
self.__class__ = RegistrarA
else:
self.__class__ = RegistrarB
And then following will work.
com = Domain('test.com') #load RegistrarA
com.lookup()
I'm using this approach successfully.
Upvotes: 10
Reputation: 21033
I think using a function is fine.
The more interesting question is how do you determine which registrar to load? One option is to have an abstract base Registrar class which concrete implementations subclass, then iterate over its __subclasses__()
calling an is_registrar_for()
class method:
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')
This will let you transparently add new Registrar
s and delegate the decision of which domains each supports, to them.
Upvotes: 87