Шаблон проектирования Factory / Builder в Python

Ситуация: Я реализовал фабричный шаблон проектирования на Python, как показано ниже. Существуют разные экраны с разной конфигурацией. Можно зарегистрировать экран на фабрике и получить экран, если правильная конфигурация передана соответствующему методу фабричного класса.

Вопрос: Мне интересно, является ли это чистым решением, поскольку каждый класс экрана (Screen1, Screen2, ..) имеет main-метод, который вызывает в определенном порядке соответствующие методы частного класса. Есть ли лучший / более чистый способ справиться с main методы в каждом классе? Будет ли проще реализовать комбинацию шаблона проектирования фабрики и компоновщика?

Высоко ценю любые комментарии и улучшения !!

Код:

import numpy as np
import pandas as pd

from abc import ABCMeta, abstractmethod 

df = pd.DataFrame({"ident": ["A1", "A2", "B3", "B4"], "other_col": np.random.randint(1, 6, 4)})


class IScreens(metaclass=ABCMeta):

    @abstractmethod
    def main(self):
        pass 
    

class Screen1(IScreens):
    
    def __init__(self, data, config):
        self.data = data
        self.batch = config["cfg1"]    
    
    def __get_letter(self):
        self.data["campaign"] = self.data[self.batch].str[:1]

        return self.data
    
    def __get_number(self):
        self.data["num"] = self.data[self.batch].str[1:]

        return self.data
    
    def __some_other_stuff(self):
        self.data["other_stuff"] = self.data["num"].astype(int) * 100 / 2

        return self.data
    
    def main(self):
        self.data = self.__get_letter()
        self.data = self.__get_number()
        self.data = self.__some_other_stuff()
        # some more processing steps follow 

        return self.data
    
    
class Screen2(IScreens):
    
    def __init__(self, data, config):
        self.data = data
        self.batch = config["cfg1"]    
    
    def __some_cool_stuff(self):
        self.data["cool_other_stuff"] = self.data[self.batch] + "_COOOOL"

        return self.data
    
    def main(self):
        self.data = self.__some_cool_stuff()
        # some more processing steps follow 
        
        return self.data
        

class ScreenFactory:

    def __init__(self):
        self._screens = {}

    def register_screen(self, screen_name, screen_class):
        self._screens[screen_name] = screen_class
    
    def get_screen(self, data, config):
        
        if "screen_indicator" not in config:
            raise AssertionError("Your config file does not include 'screen_indicator' key")
        
        screen = self._screens.get(config["screen_indicator"])
        if not screen:
            raise AssertionError("screen not implemented")
        return screen(data=data, config=config)
    

factory = ScreenFactory()

factory.register_screen(screen_name="s1", screen_class=Screen1)
config = {"screen_indicator": "s1", "cfg1": "ident"}
factory.get_screen(data=df, config=config).main()

factory.register_screen(screen_name="s2", screen_class=Screen2)
config = {"screen_indicator": "s2", "cfg1": "ident"}
factory.get_screen(data=df, config=config).main()
```

2 ответа
2

IScreens — это вызываемый

Собственно, ваш IScreens class (interface) содержит только один абстрактный метод ( main метод), поэтому этот класс является своего рода вызываемым. Вместо создания определенного класса вы можете использовать collections.Callable:

import collections

IScreens = collections.Callable

Чтобы использовать этот класс, вы можете:

class Screen1(IScreens):
    def __init__(self, data, config):
          ...

    def __call__(self, *args, **kwargs):  # instead of `main`
        return ...

s1 = Screen1(data=..., config=...)
result = s1()  # call the callable s1

IScreens может содержать общие данные

Я вижу что data и config переменные являются общими для Screen1 и Screen2. Рекомендуется разложить это на множители в базовом классе:

class IScreens(collections.Callable):
    def __init__(self, data, config):
        self.data = data
        self.batch = config["cfg1"]

ПРИМЕЧАНИЕ. Здесь я не понимаю, почему вы проходите config словарь, но получить только batch (config["cfg1"]).

Избегайте лишней сложности

Частные методы с двойным подчеркиванием, например __get_letter, труднее отлаживать, потому что их имена замаскированы под _{className}__{attr_name}.

Например:

class C:
    def __init__(self):
        self.__secret = "foo"

obj = C()
print(obj._C__secret)
# 'foo'

Очень короткие функции вроде __get_letter следует избегать, если не использовать повторно несколько раз. Здесь вы добавляете им сложность (и дополнительное время вычислений).

Получатели не должны изменять экземпляр

Получатель не должен изменять состояние экземпляра класса.

Здесь, в вашем __get_letter метод, вы извлекаете значение (self.data[self.batch].str[:1]) и изменение внутреннего состояния (self.data["campaign"]). Это намеренно?

Лучше инициализировать все в своем конструкторе:

class Screen1(IScreens):
    def __init__(self, data, config):
        self.data = data
        self.batch = config["cfg1"]
        self.data["campaign"] = self.data[self.batch].str[:1]
        self.data["num"] = self.data[self.batch].str[1:]
        self.data["other_stuff"] = self.data["num"].astype(int) * 100 / 2

Заводская регистрация

Есть два разных способа создания фабрик:

  1. Фабрика (или основная функция) знает все свои классы и отвечает за регистрацию классов,
  2. Каждый класс знает фабрику и может автоматически регистрироваться, как плагин.

Кажется, вы выбираете первый случай. Вам также может потребоваться добавить метод отмены регистрации. Во втором случае регистрация обычно является окончательной: отменить регистрацию сложнее…

Оба хороши, так что.

Чтобы зарегистрировать класс на фабрике, мы использовали имя класса (или экземпляр класса) вместо произвольного имени.

Пример использования имени класса:

class ScreenFactory:
    def __init__(self):
        self._screens = {}

    def register_screen(self, screen_class):
        self._screens[screen_class.__name__] = screen_class

    def get_screen_class(self, class_name: str):
        return self._screens[class_name]

Пример использования атрибута class:

class IScreens:
    kind = "unknown"


class Screen1(IScreens):
    kind = "s1"


class ScreenFactory:
    def __init__(self):
        self._screens = {}

    def register_screen(self, screen_class):
        self._screens[screen_class.kind] = screen_class

    def get_screen_class(self, kind: str) -> IScreens:
        return self._screens[kind]

Второй способ интересен, если вы хотите выбрать реализацию ( IScreens class) в соответствии с тегом / меткой, извлеченным из текстовых данных…

ПРИМЕЧАНИЕ: вы могли заметить, что я добавил get_screen_class метод.

Заводской доступ

Это сложная часть

В вашей get_screen , вы проверяете, действительна ли конкретная конфигурация (наличие «screen_indicator») и существует ли подходящий класс, удовлетворяющий второму условию (config["screen_indicator"] это Добрый из IScreens).

Я согласен с этим, но это можно упростить. Вот решение с обработкой ошибок:

class ScreenNotImplementedError(Exception):
    fmt = "Screen not implemented for this configuration: {config}"

    def __init__(self, *, config):
        msg = self.fmt.format(config=config)
        super(ScreenNotImplementedError, self).__init__(msg)


class ScreenFactory:
    ...

    def get_screen(self, data, config):
        try:
            kind = config["screen_indicator"]  # may raise KeyError
            cls = self.get_screen_class(kind)  # may raise KeyError
        except KeyError:
            raise ScreenNotImplementedError(config=config)
        else:
            screen = cls(data=data, config=config)
            return screen

    Класс может включать «вспомогательные» методы для реализации более сложных методов. Это похоже на разбиение сложной функции на более простые. Если вспомогательные методы не являются частью открытого интерфейса класса, по соглашению в имени используется одинарное подчеркивание в начале.

    Я люблю использовать __init_subclass__() в базовом классе, чтобы автоматически поддерживать реестр подклассов, а затем предоставить метод класса в базовом классе, который будет действовать как фабричная функция. __init_subclass__() вызывается после запуска кода, определяющего подкласс (то есть во время компиляции). Ему передается новый подкласс, который в приведенном ниже коде используется для регистрации подкласса с именем подкласса. __init_subclass__() это метод класса даже без @classmethod декоратор. Метод класса from_config() — это пример фабричного метода, который создает переданные ему аргументы на основе соответствующего подкласса. Здесь он основан на имени подкласса, но он может быть основан на других факторах, расчетах и ​​т. Д.

    class Screen(metaclass=ABCMeta):
        registry = {}
        
        def __init_subclass__(cls):
            if cls.__name__ not in Screen.registry:
                Screen.registry[cls.__name__] = cls
            else:
                raise RuntimeError('Screen subclass "{cls.__name__}" already defined.')
    
                
        def __init__(self, data, config):
            self.data = data
            self.config = config
            
            
        @classmethod
        def from_config(cls, data, config):
            if "screen_indicator" not in config:
                raise ValueError("config missing 'screen_indicator' key")
            
            screen = Screen.registry.get(config["screen_indicator"])
            if not screen:
                raise NameError(f"""No such screen: '{config["screen_indicator"]}'""")
                
            return screen(data=data, config=config)
            
            
        @abstractmethod
        def main(self):
            pass 
        
        
    class Screen1(Screen):
        
        def _do_stuff(self):
            self.data[0] *= 2
        
        def main(self):
            self._do_stuff()
            return self.data
        
        
    class Screen2(Screen):
            
        def _do_more_stuff(self):
            self.data[0] *= 3
        
        def main(self):
            self._do_more_stuff()
            return self.data
    
    
    print(Screen.registry)
    
    data=[42, 'what?']
    
    config = {"screen_indicator": "Screen1", "cfg1": "ident"}
    d1 = Screen.from_config(data=data, config=config).main()
    print(d1)
    
    config = {"screen_indicator": "Screen2", "cfg1": "ident"}
    d2 = Screen.from_config(data=data, config=config).main()
    print(d2)
    
    config = {"screen_indicator": "Screen3", "cfg1": "ident"}
    d3 = Screen.from_config(data=data, config=config).main()
    print(d3)
    

    Выход:

    {'Screen1': <class '__main__.Screen1'>, 'Screen2': <class '__main__.Screen2'>}
    [84, 'what?']
    [252, 'what?']
    
    NameError: No such screen: 'Screen3'  ## Note: I removed the stack trace
    

    Если вам нужно использовать в качестве индикатора что-то другое, кроме имени класса, вы можете использовать аргумент ключевого слова в определении подкласса следующим образом:

    class Screen(metaclass=ABCMeta):
        registry = {}
        
        def __init_subclass__(cls, indicator=""):  #### keyword gets passed in here
            if indicator:
                Screen.registry[indicator] = cls
            else:
                raise ValueError('Class subclass "{cls.__name__}" missing "indicator" argument')
            
        @classmethod
        def from_config(cls, data, config):
            if "screen_indicator" not in config:
                raise ValueError("config missing 'screen_indicator' key")
            
            screen = Screen.registry.get(config["screen_indicator"])
            if not screen:
                raise NameError(config["screen_indicator"])
                
            return screen(data=data, config=config)
            
        @abstractmethod
        def main(self):
            pass 
        
    
    class Screen1(Screen, indicator="s1"):  #### set the indicator here
        
        def __init__(self, data, config):
            self.data = data
            self.config = config
            
        def _do_stuff(self):
            self.data[0] *= 2
        
        def main(self):
            self._do_stuff()
            return self.data
    

      Добавить комментарий

      Ваш адрес email не будет опубликован. Обязательные поля помечены *