Класс factory в Python

Я новичок в 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()

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

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

Ответы

Ответ 1

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

Более интересный вопрос: как вы определяете, какой регистратор загружать? Одним из вариантов является наличие абстрактного базового класса регистратора, который представляет собой подклассу конкретных реализаций, затем перебирает его __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 и делегировать решение о том, какие из них поддерживаются для них.

Ответ 2

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

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

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

Тогда:

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

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

Ответ 3

В 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()

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

Ответ 4

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

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)

Ответ 5

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

Вот шаблон, который я уверен, что я снял с 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)

Ответ 6

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

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()

Ответ 7

Здесь метакласс неявно собирает классы Registars в поле ENTITIES

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'