Применение принципа единой ответственности в Python

Spread the love

Перевод статьи Никиты Соболева Enforcing Single Responsibility Principle in Python (Применение принципа единой ответственности в Python).

Принцип единой ответственности (или SRP) является одним из наиболее важных понятий в разработке программного обеспечения. Основная идея этой концепции: каждая часть программного обеспечения должны быть ответственна только за одну задачу.

Почему SRP важен? Можно сказать то принцип SRP демонстрирует основную идею, стоящей за разработкой программного обеспечения. Разложите сложные задачи на набор простых строительных блоков, и составьте из них нужно вам программное обеспечение. Точно так же, как мы используем встроенные функции:

print(int(input('Input number: ')))

Эта статья проведет вас через сложный процесс написания простого кода. Лично я считаю эту статью довольно сложной и трудной для восприятия, если у вас нет твердого бэкграунда в Python. Поэтому я разбил ее на несколько частей:

  • Определение простого строительного блока
  • Проблемы с функциональной композицией в Python
  • Введение в вызываемые объекты для решения задач функциональной композиции
  • Пример использования внедрения зависимостей

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

Определение строительных блоков

Давайте начнем с определения того, что такое «части программного обеспечения» и «простые строительные блоки».

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

Мы ожидаем, что эти простейшие строительные блоки (читай «функции») будут компонуемыми. И чтобы быть легко составляемыми, они должны соблюдать принцип единой ответственности. Иначе у нас будут проблемы. Потому что вы не можете создать функции, которые делают несколько вещей, в то время когда вам нужна только часть из них.

Функции тоже могут быть сложными

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

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

def create_objects(name, data, send=False, code=None):
    data = [r for r in data if r[0] and r[1]]
    keys = ['{}:{}'.format(*r) for r in data]

    existing_objects = dict(Object.objects.filter(
        name=name, key__in=keys).values_list('key', 'uid'))

    with transaction.commit_on_success():
        for (pid, w, uid), key in izip(data, keys):
            if key not in existing_objects:
                try:
                    if pid.startswith('_'):
                        result = Result.objects.get(pid=pid)
                    else:
                        result = Result.objects.filter(
                            Q(barcode=pid) | Q(oid=pid)).latest('created')
                except Result.DoesNotExist:
                    logger.info("Can't find result [%s] for w [%s]", pid, w)
                    continue

                try:
                    t = Object.objects.get(name=name, w=w, result=result)
                except:
                    if result.container.is_co:
                        code = result.container.co.num
                    else:
                        code = name_code
                    t = Object.objects.create(
                        name=name, w=w, key=key,
                        result=result, uid=uid, name_code=code)

                    reannounce(t)

                    if result.expires_date or (
                          result.registry.is_sending
                          and result.status in [Result.C, Result.W]):
                        Client().Update(result)

                if not result.is_blocked and not result.in_container:
                    if send:
                        if result.status == Result.STATUS1:
                            Result.objects.filter(
                                id=result.id
                            ).update(
                                 status=Result.STATUS2,
                                 on_way_back_date=datetime.now())
                        else:
                            started(result)

            elif uid != existing_objects[key] and uid:
                t = Object.objects.get(name=name, key=key)
                t.uid = uid
                t.name_code = name_code
                t.save()
                reannounce(t)

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

  • Цикломатическая сложность
  • Сложность по Холстеду
  • Подсчет количества аргументов, количество операторов
  • Размер функции

Более подробно про “Цикломатическая сложность” и “Сложность по Холстеду” можно почитать здесь (примечание переводчика)

После того, как мы рассмотрим все эти критерии, нам станет ясно, что эта функция слишком сложна. Можно (и рекомендуется) пойти дальше и автоматизировать этот процесс. В качестве примера можно привести линтер wemake-python-styleguide. Он позволяет обнаружит всю скрытую сложность и тем самым облегчает контроль над качеством кода.

Далее рассмотрим менее очевидный пример функции, который делает несколько вещей и нарушает SRP (и, к сожалению, такие вещи вообще не могут быть автоматизированы, ручное ревью кода – единственный способ найти такие проблемы):

def calculate_price(products: List[Product]) -> Decimal:
    """Returns the final price of all selected products (in rubles)."""
    price = 0
    for product in products:
       price += product.price

    logger.log('Final price is: {0}', price)
    return price

Посмотрите на переменную logger. Как она попала в тело функции? Это не аргумент. Это просто жестко запрограммированное поведение. Но что если я не захочу логировать определенный price по какой-либо причине? Должен ли я отключить его с помощью дополнительного флага?

В случае, если я попытаюсь сделать это, я получу что-то вроде этого:

def calculate_price(products: List[Product], log: bool = True) -> ...
    ...

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

Кроме того, как я могу проверить эту функцию? До появления вызова logger.log это была бы абсолютно тестируемая чистая функция. Что-то входит, и я могу предсказать, что выйдет. И теперь это грязный код. Чтобы проверить, что logger.log действительно работает, мне нужно каким-то образом его смоделировать и проверить, что журнал лога был действительно создан.

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

Такой беспорядок только из-за одной строки! Проблема с этой функцией заключается в том, что трудно заметить эту двойную ответственность. Если мы переименуем эту функцию из calculate_price в надлежащее ей имя calc_and_log_price, станет очевидным, что эта функция не поддерживает SRP.

Мое правило рефакторинга: если «правильное и полное» имя функции содержит и/или, тогда – это хороший кандидат на рефакторинг.

Так, теперь мы поняли что это не хорошо и все такое.., но что с этим делать? Как мы можем изменить поведение этой функции, чтобы она в конечном итоге учитывала SRP?

Я бы сказал, что единственный способ достичь SRP – это композиция: создать такие функции, чтобы каждая из них выполняла только одну задачу а комбинация их вместе выполняло бы нужное нам общую задачу.

Давайте рассмотрим различные шаблоны, которые мы можем использовать для создания композиции в Python.

Декораторы

Мы можем использовать шаблон декоратора для составления несколько функций вместе.

@log('Final price is: {0}')
def calculate_price(...) -> ...:
    ...

Какие последствия использования этого паттерна?

  • Он не только составляет, но и склеивает функции. Таким образом, у вас не будет возможности реально запустить calculate_price без log.
  • Это связь статична. Вы не можете изменить вызов функции. Или вы должны будете передать аргументы функции декоратора.
  • Такое использование создает визуальный шум. Когда количество декораторов будет расти – это будет загрязнять наши функции огромным количеством лишних строк

В общем, декораторы имеют смысл в определенных ситуациях, но не подходят для всех возможных. Хорошие примеры: @login_required, @contextmanager.

Функциональная композиция

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

from logger import log

def controller(products: List[Product]):
    final_price = log(calculate_price, message='Price is: {0}')(products)
    ...
  • При таком подходе мы можем легко вызывать функции по отдельности (и при необходимости изменить вызов)
  • С другой стороны, такой подход создает много шаблонного (boilerplate) кода и визуального шума
  • Трудно проводить рефакторинг из-за большого количества шаблонов (boilerplate) и из-за того, что вы делегируете композицию вызывающей стороне вместо объявления

Но это также хорошо подходит для некоторых случаев. Например, я постоянно использую функцию @safe (что бы избавиться от исключений):

from returns.functions import safe

user_input = input('Input number: ')

safe_number = safe(int)(user_input)

Передача аргументов

Мы всегда можем просто передать нужные аргументы. Да вот так легко решить проблему!

def calculate_price(
    products: List[Product],
    callback=Callable[[Decimal], Decimal],
) -> Decimal:
    """Returns the final price of all selected products (in rubles)."""
    price = 0
    for product in products:
       price += product.price

    return callback(price)

И тогда мы можем использовать это таким образом:

from functools import partial

from logger import log

price_log = partial(log, 'Price is: {0}')
calculate_price(products_list, callback=price_log)

И это прекрасно работает. Теперь наша функция ничего не знает о регистрации. Он только рассчитывает цену и возвращает ее. Теперь мы можем использовать любой обратный вызов, а не просто log. Это может быть любая другая функция, например которая получает один Decimal и возвращает обратно другой:

def make_discount(price: Decimal) -> Decimal:
    return price * 0.95

calculate_price(products_list, callback=make_discount)

Видите потенциальную проблему? Скрытый недостаток этого метода заключается в природе аргументов функции. Мы должны явно передать их. И если стек вызовов будет большой, нам нужно передать много параметров различным функциям. И потенциально возможно разные случаи: так нам нужен обратный вызов A в случае X и обратный вызов B в случае Y и т.д..

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

Нерешаемые проблемы:

  • Смешение логических аргументов и аргументов зависимости, потому что мы передаем их вместе, и трудно будет разбираться, что к чему
  • Явные аргументы, которые трудно или невозможно поддерживать, если ваш стек вызовов будет большим

Чтобы исправить эти проблемы, позвольте мне познакомить вас с концепцией вызываемых объектов.

Разделение логики и зависимостей

Прежде чем мы начнем обсуждать вызываемые объекты, нам нужно обсудить объекты и ООП в целом, имея в виду SRP. Я вижу главную проблему в ООП как раз в ее основной идее: «Давайте объединим данные и поведение вместе». Для меня это явное нарушение SRP, потому что объекты по замыслу делают две вещи одновременно: они содержат свое состояние и выполняют некоторое прикрепленное поведение. И поэтому на нужно исправить этот недостаток с помощью вызываемых объектов.

Вызываемые объекты выглядят как обычные объекты с двумя открытыми методами: __init__ и __call__. И они следуют определенным правилам, которые делают их уникальными:

  • Обрабатывать только зависимости в конструкторе
  • Обрабатывать только логические аргументы в методе __call__
  • Нет изменяемого состояния
  • Нет других открытых методов или каких-либо открытых атрибутов
  • Нет родительских классов или подклассов

Пример прямого способа реализации вызываемого объекта:

class CalculatePrice(object):
    def __init__(self, callback: Callable[[Decimal], Decimal]) -> None:
        self._callback = callback

    def __call__(self, products: List[Product]) -> Decimal:
        price = 0
        for product in products:
            price += product.price
        return self._callback(price)

Основное различие между вызываемыми объектами и обычными функциями заключается в том, что вызываемые объекты имеют явный шаг для передачи зависимостей, в то время как функции смешивают обычные логические аргументы с зависимостями:

# обычная функция:
calculate_price(products_list, callback=price_log)

# вызываемый объект:
CalculatePrice(price_log)(products_list)

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

from typing_extensions import final

from attr import dataclass


@final
@dataclass(frozen=True, slots=True)
class CalculatePrice(object):
    _callback: Callable[[Decimal], Decimal]

    def __call__(self, products: List[Product]) -> Decimal:
        ...

Теперь с добавлением декоратора @final, который ограничивает подклассы этого класса, и декоратора @dataclass с замороженными свойствами и слотами, наш класс соблюдает все правила, которые мы навязываем в начале.

  • Обрабатывать только зависимости в конструкторе. Правда, у нас есть только декларативные зависимости, конструктор для нас создан attrs
  • Обрабатывать только логические аргументы в методе __call__.
  • Нет изменяемого состояния. Так как мы используем frozen и slots
  • Нет никаких других открытых методов или каких-либо открытых атрибутов. В большинстве случаев мы не можем иметь открытые атрибуты, объявляя свойство slots и декларативные атрибуты защищенного экземпляра, но у нас все еще могут быть открытые методы. Рассмотрите использование линтера для этого.
  • Нет родительских классов или подклассов. Мы явно наследуемся от object и помечаем этот класс как final, поэтому создание любых подклассов будут ограничены

Теперь наша функция может выглядеть как объект, но это, безусловно, не реальный объект. Она не может иметь никакого состояния, открытых методов или атрибутов. И это прекрасно для принципа единой ответственности. Прежде всего, у нее нет данных и скрытого поведения. Просто чистое поведение. Во-вторых, сложно что либо в ней испортить. У вас всегда будет один метод для вызова всех объектов, которые у вас есть. И это то, что требует SRP . Просто убедитесь, что этот метод не слишком сложен и делает всего одну вещь. И помните, никто не мешает вам создавать защищенные методы для декомпозиции поведения __call__.

Однако мы не устранили вторую проблему передачи зависимостей в качестве аргументов функциям (или вызываемым объектам): что создает много шумности.

Внедрение зависимости

Шаблон DI хорошо известен и широко используется за пределами мира питона. Но, почему-то не очень популярен среди питонистов. Я думаю, что это ошибка, которая должна быть исправлена.

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

from project.postcards.repository import PostcardsForToday
from project.postcards.services import (
   SendPostcardsByEmail,
   CountPostcardsInAnalytics,
)

@final
@dataclass(frozen=True, slots=True)
class SendTodaysPostcardsUsecase(object):
    _repository: PostcardsForToday
    _email: SendPostcardsByEmail
    _analytics: CountPostcardInAnalytics

    def __call__(self, today: datetime) -> None:
        postcards = self._repository(today)
        self._email(postcards)
        self._analytics(postcards)

Далее мы должны вызвать этот класс:

# Injecting dependencies:
send_postcards = SendTodaysPostcardsUsecase(
    PostcardsForToday(db=Postgres('postgres://...')),
    SendPostcardsByEmail(email=SendGrid('username', 'pass')),
    CountPostcardInAnalytics(source=GoogleAnalytics('google', 'admin')),
)

# Actually invoking postcards send:
send_postcards(datetime.now())

В этом примере хорошо видно проблему. У нас есть много шаблонов использования (boilerplate), связанных с зависимостями. Каждый раз, когда мы создаем экземпляр SendTodaysPostcardsUsecase – мы должны создавать все его зависимости.

И весь этот шаблон кажется излишне громоздким. Мы уже указали все типы ожидаемых зависимостей в нашем классе. Но нам придется дублировать этот код каждый раз при использование. Как нам избавиться от этого? Мы можем использовать структуру DI. Я могу лично рекомендовать dependencies  или punq. Их основное отличие заключается в том, как они разрешают зависимости: dependencies используют имена, а punq использует типы. Мы использовали punq для этого примера.

Не забудьте установить его:

pip install punq

Теперь наш код может быть упрощен. Мы создаем единое место, где регистрируются все зависимости:

# project/implemented.py

import punq

container = punq.Container()

# Low level dependencies:
container.register(Postgres)
container.register(SendGrid)
container.register(GoogleAnalytics)

# Intermediate dependencies:
container.register(PostcardsForToday)
container.register(SendPostcardsByEmail)
container.register(CountPostcardInAnalytics)

# End dependencies:
container.register(SendTodaysPostcardsUsecase)

И затем используйте это везде где нужно:

from project.implemented import container

send_postcards = container.resolve(SendTodaysPostcardsUsecase)
send_postcards(datetime.now())

Теперь у нас нет повторяющихся шаблонов, зато мы получили удобочитаемость и безопасности типов прямо из коробки. Нам не нужно вручную связывать какие-либо зависимости. Они будут связаны аннотациями от punq. Просто введите декларативные поля в вызываемых объектах так, как вам нужно, зарегистрируйте зависимости в контейнере, и вы готовы к работе.

Конечно, для улучшения Inversion of Control есть несколько продвинутых шаблонов, но это лучше описано в документации punq.

Когда не нужно использовать вызываемые объекты

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

Заключение

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

Но были ли напрасны все наши усилия? Самый важный вопрос для себя: стал ли мой код лучше после всего этого рефакторинга?

Мой ответ: да. Я могу с легкостью собрать простые строительные блоки в сложные варианты использования.

Что вы думаете об этом? Поделитесь своим мнением в комментариях.

Основные выводы статьи:

  • Используйте простые строительные блоки, которые легко составить
  • Чтобы быть составными, все сущности должны отвечать только за одну вещь
  • Используйте инструменты проверки качества кода, чтобы убедиться, что эти блоки действительно «просты»
  • Чтобы сделать вещи высокого уровня ответственными только за одну вещь – используйте составные простые блоки
  • Для обработки зависимостей композиции используйте вызываемые объекты
  • Использовать зависимую инъекцию для уменьшения шаблонного (boilerplate) кода



Статья оказалось интересной, но вызвало некоторые противоречивые комментарии. Некоторые из них я перевел. Я сделал это для того что бы вы не рассматривали идею описанную в статье как однозначно верное. Как говориться “сколько людей, столько мнений”:

michael: Хорошая статья. Написано ясно и сжато. Тем не менее, я думаю, что такой подход лишает нас простоты использования Python. Когда я читал вашу статью, все это мне напомнило использование JAVA. Что я не считаю преимуществом 😉
Nikita Sobolev: Я уверен, что это вовсе не похоже на Java! Это больше похоже на функциональное программирование. Но концепция взята непосредственно из Ruby (full route: FP -> Ruby -> Python), где такой подход очень популярен. Смотрите https://www.slideshare.net/…
michael: Ой. Я еще не работал с Ruby. Это довольно интересно. Я хотел бы увидеть больше статей на эту тему. Тем не менее, я чувствую, что это все таки не Python. Хотя возможно у меня просто не хватает опыта.

Curt J. Sampson:
>Поздравляем, теперь у нас есть хорошо известный анти-шаблон в нашем коде. Не используйте логические флаги. Это плохо.

В Python логические флаги отлично работают и их использование не являются антипаттернами, если делать все правильно. Antipattern - это вызовы функций, которые выглядят как myfunc(true, false, true). Это явно плохо, потому что вы не можете сказать, что означают все эти булевы значения. Предлагаемый стандартный рефакторинг состоит в том, чтобы иметь две функции, имена которых описывают вызов: то есть заменить doSomething(true) на doSomethingWell() и doSomething(false) на doSomethingPoorly(). Но Python имеет гораздо лучшую способ для рефакторинга этого - именные аргументы:

def dosomething(*, fast, well=True):
....

Теперь вызовов этой функции может выглядят как dosomething(fast = True) или dosomething(fast = False, well = False), которые отлично читаются. Символ * в списке параметров заставляет fast быть именным аргументом, поэтому эту функция нельзя просто вызвать как dosomething(True). Если разработчик попытается пропустить именной аргумент, у которого нет значения, он будет проинформирован ясным сообщением о том, что отсутствует аргумент: TypeError: dosomething() missing 1 required keyword-only argument: 'fast'.

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

Nikita Sobolev: Я не согласен с использованием логических флагов. Для меня не имеет значения, как именно я передаю этот флаг некоторым функциям, это также не зависит от выбранного языка. Правило простое: если у вас есть логический флаг, у вас есть функция, которая меняет свое поведение из-за этого. Лучше преобразовать его в две отдельные функции, которые будут выполнять только одну задачу.
Но я согласен с использованием именных аргументами kwargs. Если вам необходимо использовать логические флаги (сторонние API, устаревший код и т. д.), только нужно использовать корректные имена переменных. У нас для этого даже есть правило линтера: https://wemake-python-style

Curt J. Sampson: Если переменная с логическим флагом может иметь только два разных значения (True/False), то да, конечно, лучше иметь две отдельные функции. Но для случаев, когда по сути одна и та же операция выполняется для любого параметра, в Python мы можем делать то, что я предложил выше, вместо рефакторинга, приведенного в разделе «Запутанная реализация», которую вы связали со статьей https://martinfowler.com/bliki/FlagArgument.html:


public Booking regularBook(Customer aCustomer) {
return hiddenBookImpl(aCustomer, false);
}
public Booking premiumBook(Customer aCustomer) {
return hiddenBookImpl(aCustomer, true);
}
private Booking hiddenBookImpl(Customer aCustomer, boolean isPremium) {...}


В статье Фаулера, ссылка на которую приведена выше, также упоминаются случаи, когда ваше правило линтера было бы неправильным. В разделе «Boolean Setting Method»:

Если вы извлекаете данные из логического источника, такого как элемент управления пользовательского интерфейса или источник данных, я бы предпочел использовать setSwitch(aValue), чем
if (aValue)
setOn();
else
setOff();


Wayne Werner: Внедрение зависимостей совсем не популярно в Python, потому что это ужасный мусор. Я имею в виду, что DI-фреймворки - это не идеальное решение. Минута, когда вы начинаете использовать DI-фреймворки, - это минута, когда вы перестаете рассуждать об использовании вашего кода (по крайней мере, для .NET DI-фреймворка этого было достаточно, чтобы навсегда отвлечь меня от текущей задачи). Если у вас *действительно* есть причина, по который вы не можете использовать фабричный шаблон. Я никогда не видел, чтобы использования DI улучшало бы кодовую базу. Но я бы приветствовал доказательства обратного;)
Sebastian Buczyński: Я думаю, что в этой статье отсутствует более полное объяснение того, что означает единый принцип ответственности. Это не просто «все части программного обеспечения должны иметь только одну ответственность». Было бы неплохо если вы определите, что такое ответственность в контексте кодирования или конкретных строительных блоков, таких как функции или классы.
Nikita Sobolev: Да, в самом деле. Мне действительно нравится определение, данное дядей Бобом: http://butunclebob.com/Arti
Curt J. Sampson: 
> Я вижу главную проблему в ООП как раз в ее основной идее: «Давайте
> объединим данные и поведение вместе». Для меня это явное нарушение
> SRP, потому что объекты по замыслу делают две вещи одновременно:
> они содержат свое состояние и выполняют некоторое прикрепленное
> поведение. Мы исправим этот недостаток с помощью вызываемых
> объектов.
Хотя я согласен с тем, что смешивание состояния и поведения имеет свои проблемы, ваши решения продолжают смешивать поведение и состояние. Это видно если мы ваше первое решение на два вызова:

calc_log = CalculatePrice(price_log)
calc_log(products_list)


Если я установлю calc_otherlog = CalculatePrice(other_log), у меня теперь будет два объекта: calc_log и calc_otherlog, каждый с (различным) состоянием и (одинаковым) поведением. То, что состояние здесь не изменяемо, не означает, что оно не существует.
Это не значит, что ваша идея использовать функции каррирования для частичного применения (стандартная методика функционального программирования) не имеет значения, но есть более легкий способ сделать это:

def calculate_price(callback, products):
price = 0
for p in products:
price += product.price
return callback(price)


Nikita Sobolev: Спасибо за ваш комментарий. Удивительно, что вы упомянули функциональный способ использования DI (через частичное применение), я сомневался, стоит ли мне добавлять его в статью. То же самое касается "map".
Как насчет состояния внутри вызываемых объектов? Это похоже на замыкания. Что, опять же, техника для создания основного состояния внутри функции. Но это другой тип состояний.
1. неизменяемое состояние (immutable) (ну, в Python нет ничего неизменного)
2. состояние ограниченное только DI
Таким образом, у вас не будет никакого self.property = value внутри вашего кода. И это явное ограничение, которое мы накладываем на состояние нашего объекта.
Curt J. Sampson: Ну, «состояние внутри вызываемого объекта» в Python не обязательно является замыканием. Использование замыканий для хранения состояния для «объектов» является стандартным способом реализации «объектной» части объектно-ориентированного программирования в языках, которые не обеспечивают явной поддержки этого (и даже некоторых, которые это делают). С моральной точки зрения нет никакой разницы между «объектом» с внутренним состоянием и «замыканием». Неизменность(Immutablilty), как правило, хорошо, там где вы можете использовать это; это не зависит от того, есть ли у вас объекты, хранящие состояние, или нет.

Spread the love

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

Ваш e-mail не будет опубликован.