Фабрика классов в Python - PullRequest
60 голосов
/ 19 января 2009

Я новичок в Python и мне нужен совет по реализации приведенного ниже сценария.

У меня есть два класса для управления доменами у двух разных регистраторов. Оба имеют одинаковый интерфейс, например

class RegistrarA(Object):
    def __init__(self, domain):
        self.domain = domain

    def lookup(self):
        ...

    def register(self, info):
        ...

и

class RegistrarB(object):
    def __init__(self, domain):
        self.domain = domain

    def lookup(self):
        ...

    def register(self, info):
        ...

Я хотел бы создать класс Домена, который, учитывая имя домена, загружает правильный класс регистратора на основе расширения, например,

com = Domain('test.com') #load RegistrarA
com.lookup()

biz = Domain('test.biz') #load RegistrarB
biz.lookup()

Я знаю, что это можно сделать с помощью заводской функции (см. Ниже), но является ли это лучшим способом сделать это или есть лучший способ использования функций ООП?

def factory(domain):
  if ...:
    return RegistrarA(domain)
  else:
    return RegistrarB(domain)

Ответы [ 7 ]

72 голосов
/ 19 января 2009

Я думаю, что использование функции - это нормально.

Более интересный вопрос: как определить, какой регистратор загрузить? Один из вариантов - иметь абстрактный базовый класс Registrar, который является подклассом конкретных реализаций, а затем выполнить итерацию по его __subclasses__(), вызывая метод класса 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')

Это позволит вам прозрачно добавлять новые Registrar s и делегировать им решение о том, какие домены поддерживает каждый из них.

21 голосов
/ 19 января 2009

Предполагая, что вам нужны отдельные классы для разных регистраторов (хотя это не очевидно в вашем примере), ваше решение выглядит нормально, хотя RegistrarA и RegistrarB , вероятно, имеют общие функции и могут быть получены из Абстрактный базовый класс .

В качестве альтернативы вашей factory функции вы можете указать dict, сопоставленный с классами вашего регистратора:

Registrar = {'test.com': RegistrarA, 'test.biz': RegistrarB}

Тогда:

registrar = Registrar['test.com'](domain)

Одно замечание: вы на самом деле не делаете здесь фабрику классов, поскольку возвращаете экземпляры, а не классы.

10 голосов
/ 13 февраля 2009

В Python вы можете напрямую изменить класс:

class Domain(object):
  def __init__(self, domain):
    self.domain = domain
    if ...:
      self.__class__ = RegistrarA
    else:
      self.__class__ = RegistrarB

И тогда сработает следующее.

com = Domain('test.com') #load RegistrarA
com.lookup()

Я успешно использую этот подход.

6 голосов
/ 27 мая 2016

Вы можете создать класс-оболочку и перегрузить его метод __new__(), чтобы он возвращал экземпляры специализированных подклассов, например ::10000

class Registrar(object):
    def __new__(self, domain):
        if ...:
            return RegistrarA(domain)
        elif ...:
            return RegistrarB(domain)
        else:
            raise Exception()

Кроме того, чтобы справиться с не взаимоисключающими условиями, проблемой, которая поднималась в других ответах, первый вопрос, который нужно задать себе, - хотите ли вы, чтобы класс-обертка, который играет роль диспетчера, управлял условия, или он будет делегировать его специализированным классам. Я могу предложить общий механизм, в котором специализированные классы определяют свои собственные условия, но оболочка выполняет проверку, как это (при условии, что каждый специализированный класс предоставляет метод класса, который проверяет, является ли он регистратором для определенного домена, is_registrar_for (. ..) как предложено в других ответах):

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)
2 голосов
/ 05 июня 2013

У меня есть эта проблема все время. Если у вас есть классы, встроенные в ваше приложение (и его модули), вы можете использовать функцию; но если вы загружаете плагины динамически, вам нужно нечто более динамичное - автоматически регистрировать классы на фабрике с помощью метаклассов.

Вот шаблон, который, я уверен, я изначально поднял из StackOverflow, но у меня до сих пор нет пути к исходному сообщению

_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)
1 голос
/ 05 июня 2013

как насчет чего-то вроде

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()
0 голосов
/ 25 мая 2018

Здесь метакласс неявно собирает классы регистраторов в 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'
...