Запретить поток управления на основе исключений на верхнем уровне шаблона репозитория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, чтобы сделать их доступными
  • логика предметной области будет просачиваться в уровень приложения
  • логика предметной области будет дублироваться в каждое приложение
РЕДАКТИРОВАТЬ
В этой статье 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»