← Назад к вопросам
Как создается сервис?
2.0 Middle🔥 111 комментариев
#REST API и HTTP#Архитектура и паттерны
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Как создается сервис (Service в Clean Architecture)
Сервис — это ключевой компонент в архитектуре приложения. Вот полное руководство по создании.
1. Что такое сервис?
Сервис — это класс, который:
- Содержит бизнес-логику приложения
- Взаимодействует с репозиториями для доступа к данным
- Не зависит от деталей реализации (БД, API, UI)
- Можно тестировать в изоляции
- Используется контроллерами/представлениями
Пирамида архитектуры (от внешнего к внутреннему):
┌─────────────────────────┐
│ Presentation (UI/API) │ ← REST endpoints, web pages
├─────────────────────────┤
│ Application (Services) │ ← Бизнес-логика ← ЭТО ТО, ЧТО НАМ НУЖНО
├─────────────────────────┤
│ Domain (Models) │ ← Сущности, правила
├─────────────────────────┤
│ Infrastructure │ ← БД, HTTP клиенты, файлы
└─────────────────────────┘
2. Простой пример: UserService
# models.py (Domain layer)
from dataclasses import dataclass
from datetime import datetime
@dataclass
class User:
"""Модель пользователя (domain entity)"""
id: int
email: str
username: str
password_hash: str
created_at: datetime
is_active: bool
# repositories.py (Infrastructure layer)
from abc import ABC, abstractmethod
from typing import Optional
class UserRepository(ABC):
"""Интерфейс репозитория для работы с пользователями"""
@abstractmethod
def find_by_email(self, email: str) -> Optional[User]:
"""Найти пользователя по email"""
pass
@abstractmethod
def find_by_id(self, user_id: int) -> Optional[User]:
"""Найти пользователя по ID"""
pass
@abstractmethod
def create(self, user: User) -> User:
"""Создать новго пользователя"""
pass
@abstractmethod
def update(self, user: User) -> User:
"""Обновить пользователя"""
pass
class DatabaseUserRepository(UserRepository):
"""Реальная реализация через БД"""
def __init__(self, db_session):
self.db = db_session
def find_by_email(self, email: str) -> Optional[User]:
user_model = self.db.query(UserModel).filter_by(email=email).first()
return self._to_domain(user_model) if user_model else None
def find_by_id(self, user_id: int) -> Optional[User]:
user_model = self.db.query(UserModel).filter_by(id=user_id).first()
return self._to_domain(user_model) if user_model else None
def create(self, user: User) -> User:
user_model = UserModel(
email=user.email,
username=user.username,
password_hash=user.password_hash,
is_active=user.is_active
)
self.db.add(user_model)
self.db.commit()
return self._to_domain(user_model)
def update(self, user: User) -> User:
user_model = self.db.query(UserModel).get(user.id)
user_model.username = user.username
user_model.is_active = user.is_active
self.db.commit()
return self._to_domain(user_model)
@staticmethod
def _to_domain(user_model) -> User:
return User(
id=user_model.id,
email=user_model.email,
username=user_model.username,
password_hash=user_model.password_hash,
created_at=user_model.created_at,
is_active=user_model.is_active
)
# services.py (Application layer) ← ГЛАВНЫЙ СЛОЙ
from typing import Optional
from passlib.context import CryptContext
class UserService:
"""Сервис для работы с пользователями"""
def __init__(self, user_repo: UserRepository):
"""Инъекция зависимости"""
self.repo = user_repo
self.pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def register_user(self, email: str, username: str, password: str) -> User:
"""Зарегистрировать нового пользователя"""
# Проверяем что email не занят
if self.repo.find_by_email(email):
raise ValueError(f"Email {email} already registered")
# Хешируем пароль
password_hash = self.pwd_context.hash(password)
# Создаем пользователя
user = User(
id=None,
email=email,
username=username,
password_hash=password_hash,
created_at=datetime.now(),
is_active=True
)
# Сохраняем через репозиторий
return self.repo.create(user)
def authenticate(self, email: str, password: str) -> Optional[User]:
"""Проверить email и пароль"""
user = self.repo.find_by_email(email)
if not user:
return None
# Проверяем пароль
if self.pwd_context.verify(password, user.password_hash):
return user
return None
def get_user_by_id(self, user_id: int) -> Optional[User]:
"""Получить пользователя по ID"""
return self.repo.find_by_id(user_id)
def deactivate_user(self, user_id: int) -> User:
"""Деактивировать пользователя"""
user = self.repo.find_by_id(user_id)
if not user:
raise ValueError(f"User {user_id} not found")
user.is_active = False
return self.repo.update(user)
3. Использование сервиса в контроллере
# controllers/user_controller.py (Presentation layer)
from flask import request, jsonify
from services import UserService
class UserController:
def __init__(self, user_service: UserService):
self.service = user_service
def register(self):
"""POST /users/register"""
data = request.json
try:
user = self.service.register_user(
email=data['email'],
username=data['username'],
password=data['password']
)
return jsonify({'id': user.id, 'email': user.email}), 201
except ValueError as e:
return jsonify({'error': str(e)}), 400
def login(self):
"""POST /users/login"""
data = request.json
user = self.service.authenticate(
email=data['email'],
password=data['password']
)
if user:
return jsonify({'id': user.id, 'email': user.email}), 200
else:
return jsonify({'error': 'Invalid credentials'}), 401
def get_profile(self, user_id: int):
"""GET /users/{user_id}"""
user = self.service.get_user_by_id(user_id)
if not user:
return jsonify({'error': 'User not found'}), 404
return jsonify({
'id': user.id,
'email': user.email,
'username': user.username,
'is_active': user.is_active
}), 200
4. Инъекция зависимостей
# main.py (Composition root)
from flask import Flask
from database import get_db_session
from repositories import DatabaseUserRepository
from services import UserService
from controllers import UserController
app = Flask(__name__)
# Создаем зависимости
db_session = get_db_session()
user_repo = DatabaseUserRepository(db_session)
user_service = UserService(user_repo)
user_controller = UserController(user_service)
# Регистрируем маршруты
@app.route('/users/register', methods=['POST'])
def register():
return user_controller.register()
@app.route('/users/login', methods=['POST'])
def login():
return user_controller.login()
@app.route('/users/<int:user_id>', methods=['GET'])
def get_profile(user_id):
return user_controller.get_profile(user_id)
if __name__ == '__main__':
app.run()
5. Тестирование сервиса
# tests/test_user_service.py
import pytest
from unittest.mock import Mock, MagicMock
from services import UserService
from models import User
from datetime import datetime
class MockUserRepository:
"""Мок репозитория для тестирования"""
def __init__(self):
self.users = {}
self.next_id = 1
def find_by_email(self, email):
for user in self.users.values():
if user.email == email:
return user
return None
def find_by_id(self, user_id):
return self.users.get(user_id)
def create(self, user):
user.id = self.next_id
self.users[self.next_id] = user
self.next_id += 1
return user
def update(self, user):
self.users[user.id] = user
return user
def test_register_user_success():
"""Тест: успешная регистрация"""
# Arrange
repo = MockUserRepository()
service = UserService(repo)
# Act
user = service.register_user('alice@example.com', 'alice', 'password123')
# Assert
assert user.id == 1
assert user.email == 'alice@example.com'
assert user.username == 'alice'
assert user.is_active == True
def test_register_duplicate_email():
"""Тест: дублирование email"""
repo = MockUserRepository()
service = UserService(repo)
# Регистрируем первого пользователя
service.register_user('alice@example.com', 'alice', 'password123')
# Пытаемся зарегистрировать с тем же email
with pytest.raises(ValueError, match="already registered"):
service.register_user('alice@example.com', 'alice2', 'password456')
def test_authenticate_success():
"""Тест: успешная аутентификация"""
repo = MockUserRepository()
service = UserService(repo)
# Регистрируем пользователя
service.register_user('alice@example.com', 'alice', 'password123')
# Аутентифицируемся
user = service.authenticate('alice@example.com', 'password123')
assert user is not None
assert user.email == 'alice@example.com'
def test_authenticate_wrong_password():
"""Тест: неверный пароль"""
repo = MockUserRepository()
service = UserService(repo)
service.register_user('alice@example.com', 'alice', 'password123')
user = service.authenticate('alice@example.com', 'wrongpassword')
assert user is None
def test_authenticate_nonexistent_user():
"""Тест: несуществующий пользователь"""
repo = MockUserRepository()
service = UserService(repo)
user = service.authenticate('nonexistent@example.com', 'password')
assert user is None
6. Сложный пример: OrderService
# models.py
@dataclass
class Order:
id: int
user_id: int
items: list # [{'product_id': 1, 'quantity': 2}]
total_amount: float
status: str # 'pending', 'paid', 'shipped', 'delivered'
created_at: datetime
# services.py
class OrderService:
def __init__(
self,
order_repo,
product_repo,
payment_service, # Внешний сервис
email_service # Для отправки уведомлений
):
self.order_repo = order_repo
self.product_repo = product_repo
self.payment_service = payment_service
self.email_service = email_service
def create_order(self, user_id: int, items: list) -> Order:
"""Создать заказ с проверками"""
# 1. Валидация
if not items:
raise ValueError("Order must have at least one item")
# 2. Проверить наличие товаров
total_amount = 0
order_items = []
for item in items:
product = self.product_repo.find_by_id(item['product_id'])
if not product:
raise ValueError(f"Product {item['product_id']} not found")
if product.stock < item['quantity']:
raise ValueError(f"Insufficient stock for {product.name}")
total_amount += product.price * item['quantity']
order_items.append(item)
# 3. Создать заказ
order = Order(
id=None,
user_id=user_id,
items=order_items,
total_amount=total_amount,
status='pending',
created_at=datetime.now()
)
order = self.order_repo.create(order)
# 4. Отправить email
self.email_service.send_order_confirmation(user_id, order)
return order
def pay_for_order(self, order_id: int, payment_token: str) -> bool:
"""Обработать платеж"""
order = self.order_repo.find_by_id(order_id)
if not order:
raise ValueError(f"Order {order_id} not found")
if order.status != 'pending':
raise ValueError(f"Order already {order.status}")
try:
# Проверить платеж через внешний сервис
payment_result = self.payment_service.process_payment(
token=payment_token,
amount=order.total_amount
)
if not payment_result['success']:
return False
# Обновить статус заказа
order.status = 'paid'
self.order_repo.update(order)
# Отправить уведомление
self.email_service.send_payment_confirmation(order.user_id, order)
return True
except Exception as e:
logger.error(f"Payment error for order {order_id}: {e}")
raise
7. Правила создания сервиса
# ✅ ХОРОШО: Одна ответственность
class UserService:
"""Только логика работы с пользователями"""
def register_user(self, ...): pass
def authenticate(self, ...): pass
def get_user(self, ...): pass
# ❌ ПЛОХО: Смешивание ответственности
class UserService:
"""Слишком много разных функций"""
def register_user(self, ...): pass
def send_email(self, ...): pass # Это должно быть в отдельном сервисе
def save_to_cache(self, ...): pass # Это тоже
def log_activity(self, ...): pass # И это
# ✅ ХОРОШО: Использовать интерфейсы
class UserService:
def __init__(self, user_repo: UserRepository): # Интерфейс!
self.repo = user_repo
# ❌ ПЛОХО: Жесткие зависимости
class UserService:
def __init__(self):
self.repo = DatabaseUserRepository() # Сложно тестировать
# ✅ ХОРОШО: Валидация в сервисе
class UserService:
def register_user(self, email, password):
if '@' not in email:
raise ValueError("Invalid email")
if len(password) < 8:
raise ValueError("Password too short")
# ✅ ХОРОШО: Использовать domain exceptions
class EmailAlreadyRegisteredException(Exception):
pass
class UserService:
def register_user(self, email, password):
if self.repo.find_by_email(email):
raise EmailAlreadyRegisteredException(email)
8. Структура проекта
project/
├── app.py # Entry point
├── requirements.txt
│
├── domain/ # Ядро - бизнес-логика
│ ├── __init__.py
│ ├── models.py # Entities, Value Objects
│ ├── exceptions.py # Domain exceptions
│ └── repositories.py # Интерфейсы репозиториев
│
├── application/ # Бизнес-процессы
│ ├── __init__.py
│ ├── services/
│ │ ├── __init__.py
│ │ ├── user_service.py
│ │ ├── order_service.py
│ │ └── payment_service.py
│ ├── dto.py # Data Transfer Objects
│ └── use_cases.py # Use Cases
│
├── infrastructure/ # Реализации
│ ├── __init__.py
│ ├── repositories/
│ │ ├── __init__.py
│ │ ├── user_repository.py
│ │ └── order_repository.py
│ ├── clients/
│ │ ├── payment_client.py
│ │ └── email_client.py
│ └── database.py
│
├── presentation/ # API/Web
│ ├── __init__.py
│ ├── controllers/
│ │ ├── __init__.py
│ │ ├── user_controller.py
│ │ └── order_controller.py
│ └── schemas.py
│
└── tests/
├── test_user_service.py
├── test_order_service.py
└── conftest.py
Заключение
Сервис в Clean Architecture:
- Содержит бизнес-логику (главная ценность приложения)
- Не зависит от деталей реализации (БД, API, фреймворк)
- Легко тестировать (благодаря инъекции зависимостей)
- Одна ответственность (Single Responsibility Principle)
- Использует интерфейсы (Dependency Inversion Principle)
Это делает код:
- Более понятным
- Легче поддерживаемым
- Проще тестируемым
- Более модульным и переиспользуемым