Запретить поток управления на основе исключений на верхнем уровне шаблона репозиторияPython

Программы на Python
Ответить Пред. темаСлед. тема
Anonymous
 Запретить поток управления на основе исключений на верхнем уровне шаблона репозитория

Сообщение Anonymous »

Хотя я использую здесь Python, мой вопрос не связан с конкретным языком. Для краткости я опустил многие детали реализации, на данный момент это почти псевдокод; и я не привязан к конкретному фреймворку или ORM. Кроме того, поскольку шаблон репозитория часто связан с DDD, я использую термины DDD. Я не претендую на полное понимание DDD. Мой проект довольно простой и DDD, возможно, совсем не подходит, но это была возможность узнать о нем. И DDD или нет, я все равно, вероятно, получу схожие уровни: модели, восстановление экземпляров модели из базы данных и бизнес-логику.

В недавнем проекте мне пришлось добавить бизнес-представление (и концептуальное объектно-ориентированное) поверх необработанной схемы SQL (которая не может быть изменена по причинам устаревшего характера; она исходит из широко распространенного инструмента с открытым исходным кодом). Этот «бизнес-уровень» также должен реализовать некоторую тривиальную логику, например:
  • Пользователя нельзя создать, если он уже существует
  • Пользователя нельзя добавить в несуществующую группу
  • Группу нельзя удалить, если она все еще содержит пользователи
  • удаление пользователя приведет к каскадному удалению некоторых связанных с пользователем элементов
Я наткнулся на шаблон «Репозиторий»: в моем случае — и по определению — он помогает эмулировать концептуальное представление, построенное на основе необработанной схемы базы данных. Здесь я могу воссоздать объекты (агрегированные корни согласно DDD), взятые из базы данных, и добавить или удалить их из коллекции, скрывая фактическое оборудование базы данных.
Так я и закончил. наличие UserRepository:

Код: Выделить всё

# User model (aggregate root)
class User:
def __init__(self, name: str, groups: List[Group] = [], items: List[Item] = []):
# some invariant
if not (groups or items):
raise ValueError("A user must have at least one item or must belong to at least one group")
self.name = name
self.groups = groups
self.items = items

# User repository
class UserRepository:
def __init__(self, db_conn):
self.db_conn = db_conn

def exists(self, username: str) -> bool: # ...
def find_all(self) -> list[User]: # ...
def find_one(self, username: str) -> User:
if not self.exists(username):
return None
groups = self.db_conn.execute(f"SELECT FROM usergroup WHERE ...")
items = self.db_conn.execute(f"SELECT FROM useritem WHERE ...")
return User(username, groups, items)

def add(self, user: User):
for group in user.groups:
self.db_conn.execute(f"INSERT INTO usergroup ...") # insert group belongings
for item in user.items:
self.db_conn.execute(f"INSERT INTO useritem ...") # insert user items

def remove(self, username: str):
self.db_conn.execute(f"DELETE FROM usergroup WHERE ...") # delete group belongings
self.db_conn.execute(f"DELETE FROM useritem WHERE ...") # delete items
С точки зрения базы данных объект User фактически существует через несколько таблиц (скажем, две таблицы, для упрощения). Вот почему я нашел здесь полезным шаблон «Репозиторий»: я скрываю эти детали в репозитории, который отвечает за восстановление объектов, взятых из базы данных, и каким-то образом сглаживает их во время вставки в базу данных.
А что насчет пользовательской логики? Согласно DDD, он находится в UserService:

Код: Выделить всё

class UserAlreadyExistsException(Exception): # ...
class UserNotFoundException(Exception): # ...
class GroupNotFoundException(Exception): # ...

class UserService:
def __init__(self, user_repo: UserRepository, group_repo: GroupRepository):
self.user_repo = user_repo
self.group_repo = group_repo

def get(self, username: str) -> User:
user = self.user_repo.find_one(username)
if not user:
raise UserNotFoundException
return user

def create(self, user: User) -> User:
if self.user_repo.exists(user.name):
raise UserAlreadyExistsException

for group in user.groups:
if not self.group_repo.exists(group.name):
raise GroupNotFoundException(f"'{group.name}' has not been found")

self.user_repo.add(user)
return user

def delete(self, username: str):
if not self.user_repo.exists(username):
raise UserNotFoundException
self.user_repo.remove(username)
Эта служба использует репозитории как пользователей, так и групп. Он содержит бизнес-логику, о которой я упоминал вначале. Меня беспокоит (и вы можете это предвидеть) по поводу возникающих исключений: будет ли это означать поток управления на основе исключений на верхнем уровне приложения? Если да, то как это предотвратить?
У меня есть два приложения, которые, в свою очередь, используют эту службу: веб-приложение (REST API) и приложение Python. Они будут использовать эту службу и доверять ей, чтобы обеспечить соблюдение бизнес-правил (не дублировать пользователей, не создавать пользователей в несуществующих группах и т. д.).

Код: Выделить всё

# Web app consuming UserService

@router.get("/users/{username}", status_code=200)
def get_user(username: str) -> User:
try:
return user_service.get(username)
except UserNotFoundException:
raise HTTPException(404, "Given user does not exist")

@router.post("/users", status_code=201)
def post_user(user: User) -> User:
try:
return user_service.create(user)
except UserAlreadyExistsException:
raise HTTPException(409, "Given user already exists")
except GroupNotFoundException as exc:
raise HTTPException(422, str(exc))

@router.delete("/users/{username}", status_code=204)
def delete_user(username: str):
try:
user_service.delete(username)
except UserNotFoundException:
raise HTTPException(404, detail="Given user does not exist")
Но в итоге я получаю этот поток управления на основе исключений, который мне не нравится и который, как известно, является антишаблоном.
Глядя на этот популярный пример проекта DDD на Java, мы видим ту же проблему: CustomerService выдает исключение, которое перехватывается и повторно вызывается в связанном веб-сервисе. Кто-то спрашивал об этом, но не получил ответа.
Альтернативой была бы повторная реализация логики в моих приложениях, но это привело бы к множеству проблем:
  • UserService должен будет реплицировать операции UserRepository, чтобы сделать их доступными
  • логика предметной области будет просачиваться в уровень приложения
  • логика предметной области будет дублироваться в каждое приложение
РЕДАКТИРОВАТЬ
  • В этой статье 2019 года очень хорошо описана проблема, а также описана альтернатива
  • Эта статья 2023 года также страдает от потока управления на основе исключений:

Код: Выделить всё

class PaymentController {

public Response handlePayment(Request request, int amountInCents) {
try {
this.paymentService.createPaymentForUser(request.getUser(), amountInCents);
return new Response(200, new HashMap());
} catch (PaymentValidationException ex) {
throw new ResponseStatusException(400, ex.getMessage())
} catch (UserNotFoundException ex) {
throw new ResponseStatusException(401, "You are not authorized to create payments for this user");
}
}
}
На самом деле это очень похоже на мой вопрос. Это может заинтересовать некоторых читателей, хотя здесь не упоминается, как предотвратить это или существующие альтернативы.


Подробнее здесь: https://stackoverflow.com/questions/790 ... ry-pattern
Реклама
Ответить Пред. темаСлед. тема

Быстрый ответ

Изменение регистра текста: 
Смайлики
:) :( :oops: :roll: :wink: :muza: :clever: :sorry: :angel: :read: *x)
Ещё смайлики…
   
К этому ответу прикреплено по крайней мере одно вложение.

Если вы не хотите добавлять вложения, оставьте поля пустыми.

Максимально разрешённый размер вложения: 15 МБ.

  • Похожие темы
    Ответы
    Просмотры
    Последнее сообщение

Вернуться в «Python»