Есть такое понятие, как Anemic Domain Model, ака Бледная/Анемичная доманная модель. Оно может быть знакомо вам, если вы пробовали применять в своих проектах такой принцип, как DDD(Domain Driven Design). Луковая архитектура, Clean architecture - эти подходы базируются на идее использовать домен в качестве ядра приложения и следовать пятому принципу SOLID - Dependency Inversion.
Так о чем я. Anemic Domain Model - часто встречающийся антипаттерн, смысл которого в том, что разрабы неправильно понимают принципы DDD, вследствие чего создают модели с публичными геттерами/сеттерами, а логику помещают в сервисах и “хелперах”.
Anemic Repositories
Ставший стандартом в Android разработке, паттерн - Repository все же не имеет четкого определения и инструкций за что конкретно он должен отвечать. В итоге каждый дрочет как хочет использует его по-своему. Я слышал много определений что такое этот ваш репозиторий. Но в основном, все сводится к структуре, отвечающей за предоставление и сохранение сущностей, типа коллекции. getUser(): O, putUser(o), getUsersList(): List<O>
и все такое. Я промолчу про репозитории для Api, c методами типа authenticate(), login()
.
На самом деле, изначально в Android мире репозиторий предполагался как сервис, реализующий какой-то специфический сценарий. Например: подписаться на изменения в бд, и одновременно начать загрузку обновленных данных, инвалидировать кэш, если нужно и сохранить в базу. Ничего не напоминает? Да это же UseCase из Clean Architecture, только не реализующий паттерн “Команда”, следовательно менее гибкий. В целом, cойдет, если вы используете его так, но есть намного более извращенская реализация.
Что ты такое?
Если людям не нужно кэширование, либо у них всего один источник, чаще всего это выглядит так:
class UserRepository(private val userDao: UserDao) {
fun getAllUsersWithChanges(): LiveData<List<User>> =
userDao.getAllUserWithChanges()
fun getAllUsers(): List<User> =
userDao.getAllUsers()
fun getUserByIdWithChanges(userId: String): LiveData<User> =
userDao.getUserWithChanges(userId)
fun deleteUserById(userId: String) {
userDao.deleteUserById(userId)
}
}
Чувствуете этот code smell? Это вам не код с “запашком”, от него за версту несет. Этот код не делает ничего кроме переадресовки вызовов в Dao. Бесполезный Посредник
В теории, если UserDao - это интерфейс, код можно с тем же успехом переписать так:
class UserRepository(
private val userDao: UserDao
): UserDao by userDao {}
UseCase
Решешие простое - удаляем все репозитории к чертям и меняем их на юзкейсы, которые будут отдельную часть бизнес логики.
Приведу в пример UseCase для поиска друзей в приложении, из контактов телефонной книги. Конкретные реализации некоторых методов опущу для простоты. Про то, как я обрабатываю ошибки можно почитать здесь
class GetUserContactsUseCase(private val api: ApiContract.AccountApi) {
// Паттерн для отсеивания нерусских номеров
private val numberPattern: Regex by lazy {
Regex(RUSSIAN_NUMBER_REGEX)
}
suspend fun run(contacts: List<PhoneContact>): Either<Unit, List<AppContact>> {
val numbers = extractMultipleNumbersContacts(contacts)
val tels = numbers.keys.toList()
if (tels.isEmpty()) {
return Either.Left(NoNumbersMatchedFailure())
}
// Body запроса
val params = TelsRequestParams(tels)
try {
val response = api.getContacts(params)
// Маппинг в ContactResponse
return Either.Right(response.values.map(ContactResponse::toAppContact))
} catch (e: Exception) {
return Either.Left(ServerFailure())
}
}
// Делаем формат номера понятный серверу, вообще-то он мог бы и сам, но не хочет все дела...
private fun formatToRussian(number: String): String {
..
}
// Обрабатывем контакты с несколькими номерами в адресной книжке
private fun extractMultipleNumbersContacts(contacts: List<PhoneContact>): Map<String, String> {
..
}
}
Что получилось
- Никаких Middleman
- Максимально гибкий код, юзкейсы не привязаны к какому-либо экрану, в отличие от Interactor например.
- Наши классы следуют принципу единой ответственности solid
- Их легко расширять, например добавить кэщирование в базу данных в любой момент.
- Бизнес логика находится в нужном слое, и конкретном классе, а не размезана по Presenter/ViewModel
Ссылки
Domain Driven Design на практике
Слои, Луковицы, Гексогоны, Порты и Адаптеры — всё это об одном