Почему DDD — это не про "ещё один паттерн"
Вы когда-нибудь переписывали проект с нуля только потому, что "бизнес-логика размазалась по всем контроллерам"? Или, может быть, ваш UserManager разросся до 2000 строк, и никто не понимает, что он на самом деле делает? Знакомо?
В мире Swift-разработки мы привыкли к MVC, VIPER или MVVM. Но все эти паттерны решают одну и ту же проблему: как отделить UI от логики. Однако они почти ничего не говорят о том, как организовать саму бизнес-логику. И тут на сцену выходит Domain-Driven Design (DDD) — подход, который ставит в центр не фреймворк, а предметную область вашего приложения.
В этой статье мы разберём, как применить Domain-Driven Design в Swift на реальных примерах. Вы увидите, что DDD — это не абстрактная теория, а мощный инструмент, который делает код читаемым, тестируемым и устойчивым к изменениям.
Что такое DDD простыми словами
Представьте, что вы пишете приложение для интернет-магазина. MVC скажет вам: "Сделай модель Product, контроллер ProductViewController и вьюхи". А DDD спросит: "А что такое 'товар' в вашем бизнесе? Может ли он быть без цены? Что происходит, когда его добавляют в корзину?"
Domain-Driven Design — это способ моделирования программного обеспечения, при котором код максимально точно отражает язык и правила бизнеса. Вместо "технических" сущностей (типа DataManager) вы работаете с доменными объектами: Order, Customer, Invoice.
Основные кирпичики DDD
Чтобы не заблудиться в терминах, запомните три главных понятия:
- Entity (Сущность) — объект, который имеет идентичность. Два объекта с одинаковыми полями — это разные сущности (как два разных человека с одинаковыми именами).
- Value Object (Объект-значение) — объект, который определяется только своими атрибутами. Если у двух объектов все поля совпадают — это один и тот же объект (как дата или сумма денег).
- Aggregate (Агрегат) — группа связанных объектов, которая рассматривается как единое целое. Агрегат имеет корень (root entity), через который происходит доступ ко всем внутренним объектам.
Звучит сухо? Давайте на Swift.
Пишем доменную модель на Swift
Допустим, мы делаем приложение для управления задачами (TODO-лист). Начнём с самого простого — Value Object для статуса задачи.
// Value Object: статус задачи
struct TaskStatus: Equatable {
let rawValue: String
static let todo = TaskStatus(rawValue: "todo")
static let inProgress = TaskStatus(rawValue: "inProgress")
static let done = TaskStatus(rawValue: "done")
// Бизнес-правило: можно перейти только в следующий статус
func canTransition(to newStatus: TaskStatus) -> Bool {
switch (self, newStatus) {
case (.todo, .inProgress), (.inProgress, .done):
return true
default:
return false
}
}
}
Обратите внимание: мы не используем enum, потому что в реальном бизнесе статусы могут прилетать с сервера или храниться в БД. TaskStatus — это неизменяемый объект, который сам знает свои правила.
Теперь создадим Entity — саму задачу:
// Entity: задача
final class Task {
let id: UUID
var title: String
var description: String
private(set) var status: TaskStatus
let createdAt: Date
init(id: UUID = UUID(), title: String, description: String, status: TaskStatus = .todo) {
self.id = id
self.title = title
self.description = description
self.status = status
self.createdAt = Date()
}
// Доменный метод: изменить статус с проверкой бизнес-правила
func changeStatus(to newStatus: TaskStatus) throws {
guard status.canTransition(to: newStatus) else {
throw DomainError.invalidTransition(current: status, new: newStatus)
}
status = newStatus
}
}
enum DomainError: Error {
case invalidTransition(current: TaskStatus, new: TaskStatus)
}
Видите? Вся логика изменения статуса живёт внутри домена. Никакой контроллер не может просто так взять и присвоить статус "done" задаче со статусом "done". Бизнес-правило защищено на уровне модели.
Агрегаты: собираем всё вместе
Теперь представьте, что у нас есть проект, в котором живут задачи. Проект — это Агрегат, корнем которого является сам проект. Задачи внутри проекта не могут существовать отдельно.
// Агрегат: Project
final class Project {
let id: UUID
var name: String
private(set) var tasks: [Task] = []
init(id: UUID = UUID(), name: String) {
self.id = id
self.name = name
}
// Добавление задачи через корень агрегата
func addTask(title: String, description: String) -> Task {
let task = Task(title: title, description: description)
tasks.append(task)
return task
}
// Удаление задачи
func removeTask(_ task: Task) {
tasks.removeAll { $0.id == task.id }
}
}
Ключевой принцип: внешние объекты никогда не обращаются напрямую к tasks. Только через методы addTask и removeTask. Это гарантирует целостность агрегата.
Репозитории: как достать данные
В DDD репозиторий — это интерфейс для получения доменных объектов. Он скрывает детали хранения (CoreData, Realm, API). В Swift это отлично ложится на протоколы.
protocol TaskRepository {
func getAllTasks() async throws -> [Task]
func getTask(by id: UUID) async throws -> Task?
func save(_ task: Task) async throws
func delete(_ task: Task) async throws
}
final class CoreDataTaskRepository: TaskRepository {
func getAllTasks() async throws -> [Task] {
// Здесь код для CoreData
return []
}
func getTask(by id: UUID) async throws -> Task? {
// Запрос к CoreData
return nil
}
func save(_ task: Task) async throws {
// Сохранение
}
func delete(_ task: Task) async throws {
// Удаление
}
}
Теперь, если вы захотите перейти с CoreData на SwiftData или Firebase, вам нужно будет реализовать только новый класс, соответствующий протоколу TaskRepository. Вся остальная логика останется нетронутой.
Доменные сервисы: когда логика не помещается в одну сущность
Иногда бизнес-правила затрагивают несколько агрегатов. Например, "перенести задачу из одного проекта в другой". Это не метод Task и не метод Project — это отдельный доменный сервис.
Блоги
Рекомендаций по книгам
Читайте также
Мониторинг и логирование Kotlin: как не пропустить баг в production
Асинхронное программирование в PHP: полное руководство для начинающих
Работа с JSON в C#: парсинг и сериализация для начинающих
Serverless функции на Kotlin: AWS Lambda продвинутый уровень
Работа с датами и временем в JavaScript: полное руководство для Node.js