Ситуация: Я реализовал фабричный шаблон проектирования на 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 ответа
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
Заводская регистрация
Есть два разных способа создания фабрик:
- Фабрика (или основная функция) знает все свои классы и отвечает за регистрацию классов,
- Каждый класс знает фабрику и может автоматически регистрироваться, как плагин.
Кажется, вы выбираете первый случай. Вам также может потребоваться добавить метод отмены регистрации. Во втором случае регистрация обычно является окончательной: отменить регистрацию сложнее…
Оба хороши, так что.
Чтобы зарегистрировать класс на фабрике, мы использовали имя класса (или экземпляр класса) вместо произвольного имени.
Пример использования имени класса:
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