Mappers

Преобразование между слоями

Что такое Mapper?

Mapper — объект, который преобразует данные из одного представления в другое (например, Entity → DTO, DTO → Entity).

🔹 Проблема: преобразование данных

# ❌ Преобразование размазано по коду
class UserController:
    def get_user(self, user_id: int):
        user = user_service.get_user(user_id)
        # Преобразование в контроллере
        return {
            "id": user.id,
            "name": user.name,
            "email": user.email.address,
            "created_at": user.created_at.isoformat()
        }

# Проблемы:
# - Дублирование кода
# - Сложно изменить представление
# - Нарушение Single Responsibility

✅ Решение: Mapper

# ✅ Преобразование в Mapper
class UserMapper:
    @staticmethod
    def to_dto(user: User) -> UserDTO:
        return UserDTO(
            id=user.id,
            name=user.name,
            email=user.email.address,
            created_at=user.created_at.isoformat()
        )
    
    @staticmethod
    def to_entity(dto: UserDTO) -> User:
        return User(
            id=dto.id,
            name=dto.name,
            email=Email(dto.email),
            created_at=datetime.fromisoformat(dto.created_at)
        )

# Использование
class UserController:
    def get_user(self, user_id: int):
        user = user_service.get_user(user_id)
        return UserMapper.to_dto(user)

Типы Mappers

🔹 Entity → DTO

class UserMapper:
    @staticmethod
    def to_dto(user: User) -> UserDTO:
        return UserDTO(
            id=user.id,
            name=user.name,
            email=user.email.address,
            created_at=user.created_at.isoformat()
        )
    
    @staticmethod
    def to_dto_list(users: List[User]) -> List[UserDTO]:
        return [UserMapper.to_dto(user) for user in users]

# Использование
users = user_repository.find_all()
user_dtos = UserMapper.to_dto_list(users)

🔹 DTO → Entity

class UserMapper:
    @staticmethod
    def to_entity(dto: UserDTO) -> User:
        return User(
            id=dto.id,
            name=dto.name,
            email=Email(dto.email),
            created_at=datetime.fromisoformat(dto.created_at)
        )
    
    @staticmethod
    def from_request(request: CreateUserRequest) -> User:
        return User.create(
            name=request.name,
            email=Email(request.email),
            password=request.password
        )

# Использование
request = CreateUserRequest(name="Alice", email="alice@example.com", password="password123")
user = UserMapper.from_request(request)

🔹 Database Row → Entity

class UserMapper:
    @staticmethod
    def from_db_row(row: dict) -> User:
        return User(
            id=row['id'],
            name=row['name'],
            email=Email(row['email']),
            created_at=datetime.fromisoformat(row['created_at'])
        )
    
    @staticmethod
    def to_db_dict(user: User) -> dict:
        return {
            'id': user.id,
            'name': user.name,
            'email': user.email.address,
            'created_at': user.created_at.isoformat()
        }

# Использование
row = db.fetch_one("SELECT * FROM users WHERE id = ?", (user_id,))
user = UserMapper.from_db_row(row)

Автоматические Mappers

🔹 Dataclasses

from dataclasses import dataclass, asdict

@dataclass
class UserDTO:
    id: int
    name: str
    email: str

class UserMapper:
    @staticmethod
    def to_dto(user: User) -> UserDTO:
        return UserDTO(
            id=user.id,
            name=user.name,
            email=user.email.address
        )
    
    @staticmethod
    def to_dict(dto: UserDTO) -> dict:
        return asdict(dto)

# Использование
user = User(1, "Alice", Email("alice@example.com"))
dto = UserMapper.to_dto(user)
dto_dict = UserMapper.to_dict(dto)

Когда использовать мапперы?

  • Слои изолированы: требуется явное преобразование между Domain, DTO, DB-rows.
  • Контракты стабильны: API/DTO версионируется отдельно от доменных моделей.
  • Кросс-сечения: логирование/маскирование/локализация при преобразовании.
  • Необязательны при простом CRUD и Active Record, но полезны при DDD/Data Mapper.

🎯 Ключевые принципы Mappers

  1. Единая ответственность — Mapper только преобразует данные
  2. Двунаправленность — может преобразовывать в обе стороны
  3. Неизменяемость — не изменяет исходные данные
  4. Централизация — вся логика преобразования в одном месте
  5. Тестируемость — легко тестировать изолированно