Избавление от инструкций `if` при создании динамических подтипов

#python #oop #design-patterns

#python #ооп #шаблоны проектирования

Вопрос:

Это вопрос в основном о дизайне.

Давайте предположим, что у нас есть класс, который выполняет функцию инициализации различных подклассов определенного типа с помощью некоторых параметров, которые он повторяет. Проблема возникает, когда __init__ метод получает разные параметры для каждого подтипа. Есть ли какой-либо способ избежать if инструкций внутри функции, которая инициализирует классы, просто чтобы знать, какие параметры передавать? Возможно, какой-то шаблон проектирования, о котором я не знаю. Или это результат плохого дизайна?

ниже приведен пример того, что я имею в виду. обратите внимание на статический метод manage, который содержит if … else… в нем, и если бы было больше типов workers, у нас было бы больше if, чего я пытаюсь избежать. Имейте в виду, что пример минимален, а if инструкции могут быть намного сложнее.

 from abc import ABCMeta


class BaseWorker(metaclass=ABCMeta):
    def work(self):
        pass


class Worker1(BaseWorker):
    def __init__(self, name):
        self.name = name

    def work(self):
        pass


class Worker2(BaseWorker):
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def work(self):
        pass


class Manager(object):
    @staticmethod
    def manage(attributes_list):
        for attributes in attributes_list:
            if "age" in attributes:
                w = Worker2(name=attributes["name"], age=attributes["age"])
            else:
                w = Worker1(name=attributes["name"])
            w.work()


if __name__ == '__main__':
    dynamic_attributes = [
        {"name": "davay"},
        {"age": "55", "name": "ok"},
        # and so on...
    ]
    Manager.manage(dynamic_attributes)

  

И желаемое решение было бы

     @staticmethod
    def desired_manage(attributes_list):
        for attributes in attributes_list:
            w = worker_factory(attributes)
            w.work()
  

** Обратите внимание, что worker_factory это просто произвольное название способа решения этой проблемы, это не означает, что factory pattern — это правильный путь.
Более того, если мы попробуем factory pattern, из того, что я вижу, if операторы просто переместятся туда, и это ничего не решит.

Спасибо!

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

1. Вы могли бы поместить атрибуты map в соответствующие им классы в dict и сделать что-то вроде klass_ = mapping.get(attr, default_class) . Это то, что вы ищете?

2. @snakecharmerb это возможная идея, но что, если if инструкции были более сложными? Я ищу что-то более надежное.

3. Если условия более сложные, я бы, вероятно, использовал цепочку if / elif, хотя, возможно, вы могли бы что-то сделать с кортежами в качестве ключей. Я действительно не понимаю, почему вы не хотите использовать ifs: если код должен ветвиться, он должен ветвиться, а фабричная функция / метод — хорошее место для изоляции ветвящегося кода.

4. @snakecharmerb причина, по которой мне не нравятся операторы цепочки if if , заключается в том, что каждый раз, когда я захочу добавить новый класс (в данном случае новый worker), мне придется увеличивать цепочку, что означает, что в существующем коде будут изменения. Я не хочу изменять существующий код, это нарушает принципы SOLID и подвержено ошибкам.

Ответ №1:

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

Что-то вроде этого (непроверенное):

 class Manager(object):

    _registry = []
    _default = None

    @classmethod
    def register(cls, callable, klass):
        cls._registry.append((callable, klass))
        return

    @classmethod
    def register_default(cls, klass):
        cls._default = klass
        return

    @staticmethod
    def manage(attributes_list):
        for attributes in attributes_list:
            for callable, klass in Manager._registry:
                if callable(attributes):
                    w = klass(**attributes)
                    break
        else:
            w = Manager._default(**attrs)
        w.work()


class BaseWorker(metaclass=ABCMeta):
    def work(self):
        pass


class Worker1(BaseWorker):
    def __init__(self, name):
        self.name = name

Manager.register_default(Worker1)  


class Worker2(BaseWorker):
    def __init__(self, name, age):
        self.name = name
        self.age = age 

Manager.register(Worker2, lambda attrs: 'age' in attrs)
  

Этот метод имеет некоторые недостатки:

  • Условия тестируются в порядке вставки, поэтому вам нужно убедиться, что более строгие условия, например, 'name' in attrs and 'age' in attrs , тестируются перед более мягкими условиями, такими 'name' in attrs как. Это может быть непросто, если подклассы определены в разных модулях. Возможно, добавьте приоритет к каждому (callable, klass) кортежу, который можно использовать для сортировки реестра
  • Вложенный цикл может быть медленным, если attributes_list он большой (и / или существует много подклассов)

Ответ №2:

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

 from abc import ABCMeta


class BaseWorker(metaclass=ABCMeta):
    def work(self):
        pass


class Worker(BaseWorker):
    def __init__(self, name, age=None, address=None, sex=None):
        self.name = name
        self.age = age
        address= address
        sex= sex

    def work(self):
        pass


class Manager(object):
    @staticmethod
    def manage(attributes_list):
        for attributes in attributes_list:
            w = Worker(**attributes)
            w.work()


if __name__ == '__main__':
    dynamic_attributes = [
        {"name": "davay"},
        {"name": "aa", "age": "55"},
        {"name": "bb", "sex": "female"},
        {"name": "cc", "address": "123 street", "age": "43"},
        # and so on...
    ]
    Manager.manage(dynamic_attributes)
  

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

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

2. привет @max , нет необходимости располагать в определенном порядке. и это не json, выглядит как один, но это список словарей python.

3. вы правы, но все равно это не имеет значения, потому что у разных workers будет разная реализация метода work .

4. скажем, если вы инициализируете worker с именем, это будет работать иначе, чем инициализация worker с именем и возрастом? это будет глючный дизайн, imo. извините, я не смог помочь.