Остановитесь!!! Вам не нужны Repository

 

Есть такое понятие, как 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 на практике

Слои, Луковицы, Гексогоны, Порты и Адаптеры — всё это об одном

Anemic Domain Model

Anemic Repositories, MVI and RxJava-induced design damage, and how AAC ViewModel is silently killing your app

Middle Man