
Введение
Здравствуйте! Идея написать эту статью пришла мне в голову абсолютно спонтанно. Я работаю в компании и, так сложилось, что нас имеет мы имеем DRF монолит на писят два миллиона строк кода. И вот однажды, чью-то светлую голову посетила мысль — «а давайте писать код одинаково». Идея прозвучала чертвоски просто и соблазнительно. С этого момента мы завели себе ишака по имени «Django Service Layer», и все дружно начали на него наваливать. Теперь навалю и вам. Би-бу-бип.
ДИСКЛЕЙМЕР: Статья не претендует на звание серебряной пули. Не является инвестиционной рекомендацией. Учитывайте ваши риски и архитектурные возможности.
Сервисы — место, где живет бизнес-логика
Мы старались, насколько это возможно, не изобретать велосипед. В основу легли проверенные паттерны и подходы: шаблонный метод, DTO, сервисный класс с декоратором @dataclass и еще по мелочи. Если говорить о сути, то вся концепция строится на простоте и доступности. Ее легко объяснить, ее легко понять, предлагаю нам всем в этом убедиться.
Требования к сервису
Чтобы разработчикам было легче проводить ревью, мы также разработали ряд требований, которым должен соответствовать каждый сервис.
Единая ответственность. Каждый сервис реализует законченный кусок бизнес-логики — от простого создания объекта до комплексных операций с вызовами других сервисов или внешних API.
Единственный публичный метод. Вся работа с сервисом ведётся через метод
__call__. Избавляемся от проблемы когда разработчики начинают придумыватьrun-ы, execute-ы, process-ыи т.д.Только именованные аргументы. Все методы сервиса принимают либо
kwargs, либо не имеют аргументов вовсе.Строгая типизация. Во всех методах обязательно указываются типы возвращаемых значений. Это не просто «для mypy» — это документация.
Именование по функции. Название сервиса должно сразу говорить, что он делает:
SendSmsService,CreateAppointmentService,BuyProductService.Свой огород исключений. Сервис определяет специфичные исключения (например,
NotEnoughBalanceError), которые документируют бизнес-правила и делают обработку ошибок осмысленной.Чёткие контракты через DTO. Результатом работы сервиса является Data Transfer Object или примитивный тип данных (
int, float, bool, None).Реализация через dataclass. Это выбор по умолчанию: минимум boilerplate, автоматическая реализация
__init__,__repr__, поддержка frozen-режима для иммутабельности + slots=True для экономии памяти.
Реализация
@dataclass(kw_only=True, slots=True, frozen=True)
class BuyProductService:
price: int
amount: int
@log_service_error
def __call__(self) -> SoldProductDTO:
if self.amount < self.price:
raise NotEnoughBalanceError(price=price, amount=amount)
# ...other business logic...
return SoldProductDTO(message="Thank you for byuing!")
Чертовски просто, но все-таки позвольте пару уточнений:
SoldProductDTO— датакласс, в данном случае я сделал заглушку сmessage. Вместо него может быть все, что валидно в рамках вашей бизнес логики. Он может аккумулировать количество оставшихся товаров/остаточный баланс пользователя и все в таком духе.Исключение
NotEnoughBalanceErrorкоторое документирует бизнес-правило и сразу завершает работу сервиса при его нарушении.С помощью декоратора
@log_service_errorмы разделяем зону ответственности и переносим логирование ошибок в отдельную небольшую функцию. Цель — не засорять основной код логированием и не раздувать тем самым кодовую базу.
Хотел бы поподробнее остановиться на работе с исключениями внутри класса-сервиса — далее об этом.
Обработка ошибок внутри сервиса
Исключение внутри сервиса должно вызываться при нарушении какого-либо бизнес-правила. Цели, которые мы преследуем при рейзе ошибки:
Сообщить вызывающей стороне о том, что бизнес-правило не прошло.
Залогировать нарушение бизнес-правила.
Унифицировать процесс логирования и сделать одинаковую структуру лога для всех ошибок бизнес-правил (да поможем эластику более эффективно индексировать логи).
Позволить вызывающей стороне решить, как правильно обработать данную ошибку. Другими словами — сервис не в праве решать, вернуть 400-ый или 200-ый код ответа, например, при вызове во «вьюхе».
Чтобы добиться поставленных целей, внедряем два простых компонента:
Общий класс для сервисных исключений
Это первый компонент. От данного класса должны наследоваться все другие сервисные исключения.
class BaseServiceError(Exception):
"""Base service error."""
def __init__(self, *, message: str = None, **context) -> None:
self.message = self.__doc__ or message
self.context = context
class NotEnoughBalanceError(BaseServiceError):
"""There are insufficient funds in the balance to complete the transaction."""
Немого пояснений:
self.message = self.__doc__ or message— заставляем разработчиков описывать док-стринги для всех исключений и избавляемся от необходимости каждый раз прописывать одинаковое сообщение об ошибке. Код самодокументируется — и это прекрасно.**context— немного забегая вперед скажу, что так мы добавляем гибкость при логировании ошибок. Например, если у пользователя не хватило денег на балансе, чтобы купить товар — мы залогируем ошибку и дополнительно добавим контекст, что у него было «столько-то» рублей, а товар стоил «столько-то» денег.
Специальный сервисный декоратор
Это второй компонент. Он умеет работать только с нашими сервисами и только с ошибками, которые унаследованы от BaseServiceError.
def log_service_error(__call__: Callable) -> Callable:
"""Use this decorator on __call__ service method."""
@wraps(__call__)
def wrapper(self, **kwargs) -> Any:
try:
return __call__(self, **kwargs)
except BaseServiceError as exc:
logger.error(
{
"error_in": self.__class__.__name__,
"error_name": exc.__class__.__name__,
"error_message": exc.message,
**exc.context,
}
)
raise exc
return wrapper
Такой декоратор залогирует всю необходимую информацию об ошибке:
Он залогирует ГДЕ произошла ошибка (
error_in)Он залогирует КАКАЯ произошла ошибка (
error_name)Он залогирует ПОЧЕМУ произошла ошибка (
error_message)Он залогирует ДОПОЛНИТЕЛЬНЫЙ КОНТЕКСТ, который передал разработчик при рейзе исключения (
**exc.context)
Преимущества такого подхода:
Удобно фильтровать и искать нужные логи в кибане.
Можно строить дашборды и смотреть, какие сервисы часто «падают» по их названиям, при желании — провести более глубокую аналитику.
Плюс к чистоте кода — не засоряем сервисный слой постоянным логированием ошибок.
Он универсален для любого сервиса, но не обязателен. Мы без проблем можем не вешать декоратор, если нечего логировать или мы не хотим этого делать по какой-либо причине.
В конце концов мы экономим время разработчика, ему не нужно думать о том в каком формате залогировать ошибки, и как это лучше сделать. Достаточно дописать одну строчку над методом
__call__.Мы заранее готовы к некоторым расширениям. Возможно, в будущем вы захотите замерять время работы вашей бизнес логики? Создайте новый декоратор и вешайте его поверх логгера. Самое главное — не вмешивайтесь в логику работы сервиса.
Ну, и последнее, что хочется сказать про обработку — это использование встроенного в DRF хендлера — централизированный на уровне API способ обрабатывать сервисные ошибки и отдавать валидные ответы в одинаковом формате. Ссылка на доку и пример ниже:
def service_exception_handler(exc, context):
if isinstance(exc, BaseServiceError):
return Response(
data={
"error_message": exc.message,
"error_context": dict(**exc.context),
},
status=status.HTTP_400_BAD_REQUEST,
)
return exception_handler(exc, context)
Что-то подобное есть у каждого фреймворка, так что, думаю, при желании можно найти альтернативу.
С учетом всего вышесказанного, нам, в большинстве случаев, не нужно писать никакие try-except-ы внутри нашихviews. Также нам не нужно задумываться над логированием ошибок. В то же время такой код очень легко тестируется. При необходимости любой из сервисом можно «замокать». При помощи декоратора @dataclass(kw_only=True, slots=True, frozen=True) мы, насколько это возможно, делаем код потокобезопасным, более понятным и, в конце концов, такой класс весит меньше памяти. Можете в этом сами убедится при помощи sys.getsizeof. Далее — немного практики, чтобы еще лучше понять идею.
Как не нужно делать — примеры
1. Нельзя чтобы ваш сервис возвращал:
Более чем одно значение.
Словарь/список/кортеж и тем подобные структуры.
ПРИМЕР:
@dataclass(kw_only=True, slots=True, frozen=True)
class GetCoordinateService:
@log_service_error
def __call__(self) -> tuple[int, int]:
# ...business logic...
return x, y # ❌ Возвращает кортеж, неявный контракт
Вместо этого следует использовать DTO для возврата структурированных данных:
@dataclass(kw_only=True, slots=True, frozen=True)
class GetCoordinateService:
@log_service_error
def __call__(self) -> PointDTO:
# ...business logic...
return PointDTO(x=x, y=y) # ✅ возвращает DTO, явный контракт
2. Нельзя чтобы ваш сервис принимал в методе __call__:
Более чем два параметра
Словарь/неограниченное количество аргументов
ПРИМЕР:
@dataclass(kw_only=True, slots=True, frozen=True)
class CalculatePaymentService:
@log_service_error
def __call__(self, **kwargs) -> int:
deposit = kwargs["deposit"]
percent = kwargs["percent"]
term = kwargs["term"]
# ❌ Очень неявное поведение
Вместо этого следует использовать DTO:
@dataclass(kw_only=True, slots=True, frozen=True)
class CalculatePaymentService:
@log_service_error
def __call__(self, *, calculate_data: CalculateDTO) -> int:
# ✅ понятный контракт
Как нужно делать — примеры
1) Использование абстрактных классов и шаблонного метода:
class AbstractExportIssueService(abc.ABC):
system: ExportToEnum
@log_service_error
def __call__(self, *, issue: Issue) -> None:
"""Export issue to external system."""
built_export_model = self._build_export_model(issue)
self._export(to_export=built_export_model, issue=issue)
logger.info(
{
"message": "Issue successfully exported.",
"export_system": self.system,
"export_id": issue.pk,
"export_type": issue.type,
"export_data": built_export_model.model_dump(exclude_unset=True),
}
)
@final
def _build_export_model(self, issue: Issue) -> ExportDTO:
builder_class = self._get_builder_class(issue)
return builder_class()(issue=issue)
@abc.abstractmethod
def _get_builder_class(self, issue: Issue) -> Type[IExportBuildService]:
"""Resolving builder class."""
@abc.abstractmethod
def _export(self, to_export: ExportDTO, issue: Issue) -> None:
"""Business logic implementation."""
@final
@dataclass(kw_only=True, slots=True, frozen=True)
class JiraExportService(AbstractExportIssueService):
system = ExportToEnum.JIRA
def _export(self, to_export: JiraExportDTO, issue: Issue) -> None:
create_order(to_export.model_dump(by_alias=True))
issue.is_export_to_jira = True
issue.save(update_fields=["is_export_to_jira"])
def _get_builder_class(self, issue: Issue) -> Type[IExportBuildService]:
if issue.export_to_jira_kb:
return JiraBuildExportKBDataService
return JiraBuildExportDataService
@final
@dataclass(kw_only=True, slots=True, frozen=True)
class AnotherOneExportService(AbstractExportIssueService):
system = ExportToEnum.ONE_MORE_SYSTEM
def _export(self, to_export: AnotherSystemDTO, issue: Issue) -> None:
# ...other business logic impl...
def _get_builder_class(self, issue: Issue) -> Type[IExportBuildService]:
# ...resolving builder class...
2) Использование декоратора @final при наследовании от абстрактного сервиса.
3) Разделение логики на небольшие приватные методы, вместо «раздувания» одного большого метода __call__.
4) Декомпозиция бизнес-логики на уровне сервисов, определение зоны ответственности конкретного сервиса. Не стоит смешивать в одном сервисе следующую логику:
обращение во внешний сервис
сбор данных перед запросом
любая сложная калькуляция
Итого
Если после прочтения вашей статьи у вас остался привкус незавершенности — так и должно быть. Мы ни слова не сказали про DI (Dependency Injection). Почти ничего не сказали про то, как лучше строить общение между сервисами. Однако, это уже не так важно. Я надеюсь, что у меня получилось донести идею нашего подхода. Если вам в целом симпатизирует то, о чем вы прочитали, но в голове родились предложения по улучшению — мы можем обсудим это в моем гитхаб репозитории. Заводите ишуес. Би-бу-бип.
Комментарии (31)

Lewigh
30.12.2025 14:10Единая ответственность. Каждый сервис реализует законченный кусок бизнес-логики — от простого создания объекта до комплексных операций с вызовами других сервисов или внешних API.
Единственный публичный метод. Вся работа с сервисом ведётся через метод
call. Избавляемся от проблемы когда разработчики начинают придумыватьrun-ы, execute-ы, process-ыи т.д.
Вы определенно двигаетесь в правильном направлении. Позвольте, сэкономлю Вам немного времени.
У Вас не сервис а команда и она Вам не нужна. Если Вам нужен класс с одной функцией
- это значит что класс здесь в принципе не нужен. Выкиньте ненужное, оставьте старую добрую функцию и все еще упроститься.
NewSouth
30.12.2025 14:10Единственный плюс, который вижу у подхода автора - что для входящих данных отдельный dto не нужен (т.к. уже есть init у dataclassa).

Skif47
30.12.2025 14:10У класса есть как минимум 2 преимущества:
Возможность декомпозиции. При этом результат будет представлять единый юнит, а не россыпь разрозненных функций, гоняющих между собой набор одинаковых параметров.
Возможность применения DI фреймворков. Вот это та часть, которую как раз интересно было бы узнать в продолжении.

youngWishes Автор
30.12.2025 14:10Если Вам нужен класс с одной функцией- это значит что класс здесь в принципе не нужен.
Спасибо за комментарий! Возможно, тут возникло недопонимание - в классе может быть несколько методов, просто публичный - только один - и это метод
__call__.
Lewigh
30.12.2025 14:10Спасибо за комментарий! Возможно, тут возникло недопонимание - в классе может быть несколько методов, просто публичный - только один - и это метод
call.Вы можете сделать хоть тысячу маленьких функций, а можете собрать все воедино и результат у вас будет один и тот же и изменится не может, потому что функция на самом деле одна а так как все остальные функции приватные то для пользователя Вашего класса их нет и быть не может.
Объект - это история про некое состояние, про применение методов к этому состоянию для его изменения или создания новых. У Вас, класс(объект) - это просто банка с одной функцией. Если Вы вытащите функцию из банки - ничего не поменяется но станет чуть чуть проще так как останеться разделение на простые функции и простые данные. Подход - в функцию передается и возвращается DTO сильно проще чем Ваш, не нужно объяснять кому-то что это вроде бы сервис но на самом деле команда с состоянием, а еще там может быть только одна функция и она должна быть такой то, не нужно объяснять не нужно следить за таким стилем чтобы никто не начал еще функции публичные писать, не нужно никого учить и объяснять почему класс должен имитировать функцию. Будьте проще - просто используйте функции и не нужно будет ничего объяснять и проверять.

youngWishes Автор
30.12.2025 14:10У Вас, класс(объект) - это просто банка с одной функцией.
Пожалуйста объясните на основании чего вы сделали такой вывод? В статье нигде не сказано что класс должен содержать одну функцию.

Lewigh
30.12.2025 14:10Единственный публичный метод. Вся работа с сервисом ведётся через метод
call. Избавляемся от проблемы когда разработчики начинают придумыватьrun-ы, execute-ы, process-ыи т.д.

Tishka17
30.12.2025 14:10А в чем проблема ранов и прочих хэндлов?

youngWishes Автор
30.12.2025 14:10А в чем проблема ранов и прочих хэндлов?
Когда называют метод process - назовут класс DoSomeProcessor. Когда называют метод execute - назовут класс DoSomeExecutor. И все в таком духе, по итогу получается «50 оттенков сервисов», в которых сложнее разобраться, особенно при онбординге новым разрабом
Небольшим, но плюсом является, что мы не сталкиваем лбами ценителей одного нейминга с ценителями другого, мы его просто убираем, потенциально экономя человеко-часы.
Делаем код немного короче и лаконичнее, т.к. больше не нужно прописывать название конкретного метода при вызове сервиса.

youngWishes Автор
30.12.2025 14:10Единая точка входа ≠ один метод на весь класс.

Lewigh
30.12.2025 14:10У Вас одна публичная функция а значит для любого пользователя класса - у Вас только одна функция. То что Вы написали внутри 10-20 приватных для своего удобства это никак не меняет семантики - можете все скомпоновать в одну или разбирать на 1000, для пользователя класса ничего не измениться. Более того, если Вы перепишите класс на обычную функцию которая будет вызывать вспомогательные функции, по большей части ничего не измениться, потому что никакие особые свойства объектов не используете.

youngWishes Автор
30.12.2025 14:10Имхо, гораздо легче воспринимать и читать код, когда разработчик позаботился о том, чтобы завернуть его в класс. Наличие класса дает всем методам единый контекст их использования. Кстати именно такую идею продвигают, например, те же самые Enum-ы:
SOLD = 0 FREE = 1 BOOK = 2или же
class ProductStatus(enum.IntEnum): SOLD = 0 FREE = 1 BOOK = 2во втором случае все три переменные объединены общим контекстом и в этом сила использования класса над никак не связанными фрагментами кода (функциями или чем бы то ни было)
кстати, эту же идею Вы можете обнаружить в книге "Чистый Код" от Роберта Мартина

Lewigh
30.12.2025 14:10Имхо, гораздо легче воспринимать и читать код, когда разработчик позаботился о том, чтобы завернуть его в класс.
Гораздо проще читать код, когда в нем нет ничего лишнего, например класса, который используется не по прямому назначению а только для того чтобы в него что-то завернуть.
кстати, эту же идею Вы можете обнаружить в книге "Чистый Код" от Роберта Мартина
Лучше, вместо того чтобы читать старые книги сомнительного содержания, почитать свежий блог автора этой книги. Откроете для себя много нового, например что у него сейчас любимый язык на котором он пишет это Clojure, и что он за минималистичность простоту и про прочее за все хорошего против всего плохого.

youngWishes Автор
30.12.2025 14:10Гораздо проще читать код, когда в нем нет ничего лишнего, например класса, который используется не по прямому назначению а только для того чтобы в него что-то завернуть.
Вы удобным себе образом процитировали только часть моего комментария. Не вижу смысла дальше заниматься буквоедством.
у него сейчас любимый язык на котором он пишет это Clojure, и что он за минималистичность простоту и про прочее за все хорошего против всего плохого.
дай Бог здоровьечка

l1onsun
30.12.2025 14:10старались, насколько это возможно, не изобретать велосипед
и изобрели... функции
Нет, может в каком-то контексте имеет смысл так необычно организовывать код, но из статьи по моему этот контекст не понятен.
Ещё есть аргумент против использования
__call__- если не ошибаюсь с ним невозможно вызвать goto-definition в месте вызова метода, чтобы перейти к определению__call__
youngWishes Автор
30.12.2025 14:10Подскажите, пожалуйста, что вы имеете ввиду когда пишите «функции»? В нашем случае класс может содержать > 1 метода, просто один публичный и по умолчанию это метод __call__. Возможно, из статьи это не очевидно, хотя я приложил примеры с абстрактным классом, где используется шаблонный метод.
По поводу goto-definition - справедливое замечание, спасибо

Tishka17
30.12.2025 14:10Нельзя чтобы ваш сервис возвращал:
Словарь/список/кортеж и тем подобные структуры.
Вместо этого следует использовать DTO для возврата структурированных данных:
Вспоминая, что DTO - любая структура данных без логики, которую можно сериалзовать и передать по сети, я не понимаю почему вы считаетете что list[int] или tuple[int, int] - это не DTO. Идея описать выходные данные и дать им структуру - хорошая, но если вы хотите вернуть просто список, почему бы не вернуть его как етсь?
Тут могут возникнуть другие рассуждения в духе "одного списка никогда не хватит, появятся метаданные" и они имеют смысл, но вы же вообще не про это

youngWishes Автор
30.12.2025 14:10Когда мы возвращаем кортеж/список/словарь, проблема возникает не в самом сервисе, а за его пределами, когда кто-то пытается взаимодействовать с результатом работы сервиса. Начинают появляться подобные конструкции:
is_success, message = DoSomeThing()() # или user_data = GetUserData()(id=id) name = user_data["name"] phone = user_data["phone"]Приходится лишний раз напрягаться чтобы вспомнить какие там ключи у словаря.. или сколько же в списке/кортеже элементов чтобы их правильно распаковать и что они вообще значат.
В качестве исключения могу согласиться, если нам не нужно никаким образом распаковывать структуру, например, если сервис вернул список каких-то айдишников и это можно воспринимать как одну логическую единицу.

Tishka17
30.12.2025 14:10Вы приводите пример гетерогенных структур. Для них естественно не надо юзать списки и словари. Они нужны чтобы вернуть, внезапно, набор однотипных сущностей или набор пар где значения однотипные и сопоставлены с некоторыми ключами

Masnin
30.12.2025 14:10Писят два миллиона строк... Страшно представить... Это же как два ядра Линукс в сумме. Точно писят два? Не просто "два"?
Ну а по теме скажу так, мы тоже пробовали такой подход и нам не зашло. Из проблем было: проблемы переиспользования сервисов, глубокая вложенность сервисов (когда один вызывает второй, затем третий и в итоге - лапша), большой шанс дублирования и нелепых попыток его избегания, когда связанность проекта растет как на дрожжах.
В общем-то идея хорошая, но только для ситуаций когда такие сервисы сводятся к функциям без состояния (в том числе без работы с БД), вот тогда подход раскрывается просто за счёт того, что тестами такие функции покрыть легко, они не имеют эффектов и фактически это уже ФП.
Мы в итоге остановились на более гибком подходе - Feature slice. Эт когда для каждой фичи есть блок "сервисов", который более гибок чем стандартный шаблон одного класса(с точки зрения организации кода), но при этом сервисы не могут переиспользоваться между фичами. Таким образом high cohesion достигается из той жёлтой книжки из превью к статье. А вот переиспользуемый код уже как раз в функциональном стиле и он пишется в отдельных блоках.
Честно говоря - это не просто. Обилие логики всегда порождает связи, которые потом влияют на развитие, мешают вносить изменения и "ломают то, что мы даже не меняли*. Кажется, это проблема больших систем в целом, а не подхода в частности. Одно из решений - повальное дублирование и жёсткие контракты (формата HTTP API). Но микросервисами такой подход попахивает, а ими мы тоже уже наелись.
Что дальше? Может быть модульный монолит где каждый модуль - отдельная фича? Может быть...

youngWishes Автор
30.12.2025 14:10Благодарю за подробный комментарий
Писят два миллиона строк... Страшно представить... Это же как два ядра Линукс в сумме. Точно писят два? Не просто "два"?
"писят два" локальный мем у нас в команде, конечно, не настолько много)

ammo
30.12.2025 14:10Как уже справедливо заметили, для описанного в статье лучше подходят функции, а не датаклассы.
Однако, бизнес-логика на датаклассах +
__call__все еще имеет смысл - в контексте зависимостей@dataclass class JiraExportService: jira: JiraRepository # зависимость def __call__(self, issue: Issue) -> None: # аргументы if self.jira.issue_exists(issue.key): self.jira.update_description(issue.key, issue.description) else: self.jira.create_issue(issue)

mike_shapovalov
30.12.2025 14:10Такой подход однозначно лучше, чем полное отсутствие отделения бизнес логики от других слоёв, но существует мнение, что если мы используем ООП а не процедурное программирование то бизнес логика все же должна находиться внутри объекта с бизнес данными. Концентрация бизнес логики в сервисах считается анти-паттерном "Anemic model". Тут можно почитать об этом подробнее https://martinfowler.com/bliki/AnemicDomainModel.html

youngWishes Автор
30.12.2025 14:10Спасибо за комментарий, прочитал статью. Кажется, что в ней автор говорит о доменной логике, а не бизнес-логике, с чем я полностью согласен, что она должна находиться в доменных моделях.
Мое упущение что я забыл включить это в статью, постараюсь дополнить в следующей.

mike_shapovalov
30.12.2025 14:10Доменная логика это и есть бизнес логики, разве что есть какая то другая интерпретация (как это часто бывает в IT) о которой я не слышал :)

youngWishes Автор
30.12.2025 14:10Доменная логика это и есть бизнес логики, разве что есть какая то другая интерпретация (как это часто бывает в IT) о которой я не слышал :)
Приведу пример чтобы было понятно, что я имею в виду:
Допустим у нас есть доменная модель квартиры, которую пользователь может купить в ипотеку, и эта модель имеет свою доменную логику - она может подсчитать, например, итоговую стоимость в зависимости от способа оплаты - ипотека/рассрочка/кеш. Предположим что каждый способ оплаты имеет свою наценку. И в качестве упрощения я просто сделаю поле "markup".
class Appartment(models.Model): area = FloatField("полезная площадь") price = FloatField("цена за квадратный метр") markup = FloatField("наценка на area * price") def get_total_price(self) -> float: return (area * price) * (markup + 1)Также представим что квартира может быть в разных статусах:
class Appartment(models.Model): FREE, BOOKED, SOLD = range(1, 4) status = SmallIntegerField("статус (FREE/BOOKED/SOLD)") area = FloatField("полезная площадь") price = FloatField("цена за квадратный метр") markup = FloatField("наценка на area * price") def get_total_price(self) -> float: return (area * price) * (markup + 1) def is_available(self) -> bool: return self.status == FREEИ наша задача написать сервис который будет бронировать квартиру и вносить первоначальный взнос - это уже бизнес-логика, конкретный бизнес-процесс. Здесь мы можем напридумывать сколько угодно бизнес правил, например, что квартиру нельзя забронировать если первоначальный взнос меньше 10% от ее стоимости.
@dataclass(slots=True, kw_only=True, frozen=True) class BookAppartmentService: deposit: int @log_service_error def __call__(self, *, appartment: Appartment) -> None: deposit_in_percent = self._get_deposit_in_percent(appartment=appartment) if deposit_in_percent < 0.1: raise DepositNotEnoughError( deposit=deposit, appartment_price=appartment.get_total_price(), ) if appartment.is_available: appartment.status = BOOK appartment.save(update_fields=["status"]) def _get_deposit_in_percent(self, *, appartment: Appartment) -> float: return self.deposit / appartment.get_total_price()Таким образом получается что сервис по сути один за другим проверяет бизнес-правила. В моем понимании это и есть бизнес-логика. Примеры доменной модели и сервиса дико упрощены, сервис может работать с несколькими доменными моделями при необходимости - в зависимости от бизнес требований. А доменная модель иметь более сложную логику чем то что я написал, но сути дела от этого не изменится.
PyLounge
Братан, хорош, давай, давай, вперёд! Контент в кайф, можно ещё? Вообще красавчик! Можно вот этого вот почаще?