Является ли это действительным стратегическим шаблоном?

#python #design-patterns #strategy-pattern

Вопрос:

Мне нужно улучшить поведение класса с помощью внешних методов, поэтому я использую шаблон стратегии.

Сначала я определяю интерфейс для подписи методов:

 class ILabel(ABC):

    @abstractmethod
    def get_label(self, obj):
        pass

 

и реализация этого интерфейса:

 class Label(ILabel):
    def __init__(self, prefix):
        self.prefix = prefix

    def get_label(self, merchandise, obj):
        return self.prefix   str(obj)   merchandise.name
 

И класс, который я хотел бы дополнить своим алгоритмом:

 class Merchandise:

    def __init__(self):
        self.name = "__a_name"

    def get_label(self, obj):
        return str(obj)   self.name

    def display(self, obj, get_label=None):
        if get_label:
            self.get_label = types.MethodType(get_label, self)
        print(self.get_label(obj))
 

И, наконец, звонивший:

 # default behavior
x = Merchandise().display("an_obj")

# augmented behavior
label = Label("a_prefix__")
y = Merchandise().display("an_obj", label)

print(f"Default output: {x}")
print(f"Augmented output: {y}")
 

результат должен быть:

 Default output: an_obj__a_name
Augmented output: a_prefix__an_obj__a_name
 

Два вопроса:

  • Учитывая, что вместо «сиротского» метода (за неимением лучшего слова) я отправляю метод внутри класса со ссылкой на себя; это все еще считается шаблоном стратегии или другой шаблон ближе к этому дизайну?
  • Поскольку я передаю ссылку на Merchandise при регистрации метода (т. Е. types.MethodType(get_label, self) ), get_label метод в Label классе имеет ссылку на экземпляр Merchandise . т. е.:
     def get_label(self, merchandise, obj):
     

    Вопрос в том, существует ли какое-либо лучшее соглашение об именах для merchandise справки?

Обновить

В попытке предоставить минимально работающий пример используется приличное количество контекста, что может привести к мысли get_label , что метод может быть без состояния (т. Е. Без ссылки на экземпляр Merchandise ). Он Label.get_label обновлен, чтобы прояснить этот момент.

Комментарии:

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

Ответ №1:

Учитывая, что вместо «сиротского» метода (за неимением лучшего слова) я отправляю метод внутри класса со ссылкой на себя; это все еще считается шаблоном стратегии или другой шаблон ближе к этому дизайну?

Я бы все равно назвал это Стратегией, но продолжайте читать.

Поскольку я передаю ссылку на Товар при регистрации метода (т. Е. типов.MethodType(get_label, self)), правильное определение get_label в классе Label таково:

Из-за этой путаницы вам следует использовать другой подход.

Ваша логика реализации поведения стратегии по умолчанию обратная. Нет никаких причин, по которым «Стратегия получения метки для obj » должна быть методом Merchandise класса, за исключением того, что у вас там хранится реализация по умолчанию. Даже это не обязательно должен быть обычный метод, так как он ничего не делает self .

Это означает , что код слишком сложен (потому что вы напрасно используете types.MethodType механизм и динамически исправляете класс), а также имеет неожиданное поведение с сохранением состояния: когда вы вызываете display с ненулевым None значением для get_label , эта стратегия повлияет на будущие вызовы туда display , куда None передается.

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

 class Merchandise:
    @staticmethod
    def get_label(obj):
        return str(obj)

    def display(self, obj, get_label=None):
        if get_label is None:
            get_label = Merchandise.get_label
        print(get_label(obj))
 

Хотя на самом деле нам здесь не нужен шаблон «заменить значением None по умолчанию», так как мы не собираемся изменять параметр:

 class Merchandise:
    @staticmethod
    def get_label(obj):
        return str(obj)

    def display(self, obj, get_label=Merchandise.get_label):
        print(get_label(obj))
 

И этот игрушечный пример может быть еще проще:

 class Merchandise:    
    def display(self, obj, get_label=str):
        print(get_label(obj))
    # although *this* doesn't rely on `self`, either....
 

Если вам действительно нужно поведение с отслеживанием состояния, то вам следует задать состояние либо при инициализации, либо явно позже, либо и то и другое:

 class Merchandise:
    def __init__(self, get_label=str):
        self.get_label = get_label

    @property
    def get_label(self): return self._get_label
    @get_label.setter
    def get_label(self, value):
        # may as well do a little verification
        if not callable(value):
            raise TypeError("get_label strategy must be callable")
        self._get_label = value

    def display(self, obj):
        print(self.get_label(obj))
 

Обратите внимание, что здесь это self.get_label(obj) не вызов метода; Python найдет get_label как атрибут экземпляра, прежде чем попытается найти его в классе; найдя вызываемый объект, он затем вызовет этот объект.

Комментарии:

1. Вы можете обнаружить, что последний код очень похож на реализацию состояния в учебнике. На самом деле нет никакого различия в языках, где функции являются объектами первого класса. Кроме того, на самом деле в шаблоне на таких языках совсем немного — все, что мы действительно делаем,-это используем функции более высокого порядка, сохраняя одну из них и используя ее позже. Эквивалентно, это просто еще один пример использования композиции и делегирования, а не наследования.

2. Merchandise сам по себе выглядит как контейнер в стиле Java для чего-то, что в противном случае могло бы быть обычной функцией.

3.Обратите внимание, что если реализация get_label in по умолчанию Merchandise использовала другие self атрибуты, вы все равно можете использовать ее в любом из этих вариантов — просто назначьте связанный метод ( self.get_label ) get_label в первом подходе или разрешите свойству затенять обычный метод во втором.

4. @chepner Я согласен, но я предполагаю, что усилия по созданию минимального воспроизводимого примера исключают контекст, оправдывающий применение шаблона.

5. Я хотел бы повторить, что создание минимального воспроизводимого примера создает массу контекста. В моем приложении, в частности, Label Merchandise в методе используются другие свойства классов get_label и. Поэтому наличие состояния в обоих случаях имеет важное значение, поэтому я был бы наиболее признателен за проекты, которые сохраняли бы оба состояния.