среда, 29 ноября 2017 г.

Ленивы ли ваши объекты?

Конструкторы не должны содержать кода. Так думает Егор Бугаенко, и я с ним согласен. И у меня возникли некоторые соображения по этому поводу.

Для начала представляю вам объект:

class HardCalculationValue:
    def __init__(self):
        # Точка 1
        pass

    def value(self):
        # Точка 2
        return None

Этот объект - абстракция. Он может делать тяжелые операции разной природы. Работа с внешними ресурсами, как-то БД, файлы, сеть. Какие-то тяжелые преобразования. Конечно это могут быть и не очень тяжелые преобразования, но на тяжелых преобразованиях сильнее чувствуется боль. :)

И нам нужно вставить тяжелую операцию. Какую точку вы выберите 1 или 2?
Почему-то вспомнилось: ... я покажу тебе, как глубоко ведет кроличья нора. :)

Мы все подсознательно стремимся к оптимальным решениям с точки зрения производительности. Мы постоянно оптимизируем. И в данном случае многие наверное подумали - Если метод value() будут дергать много раз, то это будет жутко неэффективно... Значит правильный ответ - точка 1.

class HardCalculationValue:
    def __init__(self):
        self.value = calculateHardValue()

    def value(self):
        return self.value

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

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

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

Правильный объектно ориентированный ответ - 2

class HardCalculationValue:
    def __init__(self):
        pass

    def value(self):
        return calculateHardValue()

Это легко тестировать. Нам не нужна фабрика, поскольку мы можем сам объект передать в качестве внешней зависимости в те классы, которые хотят им пользоваться. Мы можем передать один объект сразу в несколько мест, поскольку в процессе работы его внутреннее состояние не меняется.

Вопрос с производительностью можно решить с помощью кеширующих декораторов в необходимых местах.

PS: Это получается, что фабрика - признак агрессивных классов...