«Clean Architecture» на собесах спрашивают почти всегда, и почти всегда ждут не цитату из книги, а понимание слоёв, направления зависимостей и того, когда это перебор.
Три слоя
- Presentation: экраны, view model, UI-модели. Знает о UI-фреймворке.
- Domain: сущности, бизнес-правила, use case, интерфейсы репозиториев. Чистый язык без фреймворков (корутины или Swift Concurrency допустимы).
- Data: реализации репозиториев, сеть, база, кеш, DTO и мапперы.
Официальный гайд Android называет domain-слой опциональным: его добавляют, когда появляется логика, которую нужно переиспользовать или изолировать.
Правило зависимостей
Зависимости в коде направлены внутрь, к домену. Домен не знает ни о Retrofit, ни о Core Data, ни о SwiftUI. Data-слой реализует интерфейсы, объявленные в домене:
// domain
interface OrderRepository { suspend fun place(cart: Cart): PlacedOrder }
class PlaceOrderUseCase(private val orders: OrderRepository) {
suspend operator fun invoke(cart: Cart): PlacedOrder {
require(cart.items.isNotEmpty()) { "Пустая корзина" }
return orders.place(cart)
}
}
// data
class RemoteOrderRepository(private val api: OrderApi) : OrderRepository {
override suspend fun place(cart: Cart) = api.place(cart.toDto()).toDomain()
}
Это инверсия зависимостей: верхний уровень зависит от абстракции, реализация подставляется через DI. Отсюда тестируемость: use case проверяется с фейковым репозиторием без сети.
Репозиторий
Фасад над источниками данных. Решает, откуда взять данные (кеш, база, сеть), как их объединить и когда обновить. Остальной код не знает, есть ли кеш вообще.
Модели и маппинг
- DTO повторяет формат API: snake_case, строки вместо дат, опциональные поля.
- Entity базы повторяет формат хранения.
- Доменная модель описывает понятия приложения:
Date, enum статуса, обязательные поля. - UI-модель содержит готовые строки, цвета, форматированные суммы.
Маппинг на границе data изолирует приложение от изменений API. Но четыре модели на каждую сущность нужны не всегда: если форматы совпадают, модели объединяют.
Ошибки на границах
Data переводит технические ошибки (HttpException, URLError, ошибки базы) в доменные: «нет связи», «сессия истекла», «не найдено». Presentation решает, как их показать. Если HttpException доходит до view model, презентационный слой начинает зависеть от сетевой библиотеки.
Use case: когда нужен
Use case-прокладка, которая лишь вызывает repository.get(), добавляет файл и ничего не даёт. Use case оправдан, когда в нём бизнес-правило (лимиты, скидки, валидация) или координация нескольких репозиториев, и это нужно нескольким экранам.
Слои внутри фич
В больших приложениях код группируют по фичам, а слои живут внутри:
feature/cart/data
feature/cart/domain
feature/cart/ui
Изменение фичи касается одной папки или модуля, и фичу легко вынести в отдельный модуль.
Когда это перебор
В маленьком приложении, которое в основном показывает ответы API, полный набор слоёв замедляет разработку. Минимум, который стоит делать всегда: сеть и база за интерфейсами, зависимости через конструктор, тестируемые view model, структура по фичам. Остальное добавляется по мере роста.
Частые ошибки
- Домен импортирует UIKit, Android SDK или сетевую библиотеку.
- Use case показывает алерты или держит контроллер.
- Строковые статусы API в бизнес-логике.
- Слои ради слоёв без логики внутри.
Что спросят на собесе
- Какие слои вы выделяете и что в каждом?
- Что такое правило зависимостей и инверсия зависимостей?
- Где объявлять интерфейс репозитория?
- Зачем маппить DTO в доменные модели?
- Когда Clean Architecture избыточна?