Делаем архитектуру вашего Android‑приложения SOLID'нее...

Современные Kotlin (и Android) проекты часто следуют принципам «Чистой» архитектуры (Clean Architecture), чтобы сделать код более структурированным и удобными для тестирования. Суть Чистой архитектуры заключается в следующем:
Отделение бизнес‑логики от кода пользовательского интерфейса, что позволяет работать на каждой из этих частей независимо.
Использование интерфейсов и абстрактных классов для придания системе большей гибкости и открытости для новых реализаций.
Применение принципов SOLID, таких как принцип единой ответственности (SRP), принцип инверсии зависимостей (DIP) и принцип замены Лискова (LSP), для обеспечения поддерживаемости вашей кодой базы по мере ее роста.
Какое отношение юзкейсы имеют к Чистой архитектуре?
Юзкейс (Use Case) — это основной элемент в этой философии. Он представляет собой отдельную операцию с единственной ответственностью в рамках вашего приложения.
Как и остальные компоненты в Чистой архитектуре, юзкейсы соответствуют определенному шаблону: их интерфейсы определяются в слое домена, а реализации находятся в слое данных. Этот подход способствует соблюдению сразу нескольких принципов SOLID.
Чистая архитектура организует эти компоненты по слоям:
Слой домена (domain layer) содержит чистую бизнес‑логику, включая интерфейсы юзкейсов и другие абстракции. На этом слое мы определяем, что могут делать наши юзкейсы, не заботясь о том, как они это делают.
Слой данных (data layer) предоставляет конкретные реализации, включая реализации юзкейсов. Такое разделение позволяет нам изменять способ работы юзкейса, не затрагивая его интерфейс.
Слой пользовательского интерфейса (UI layer) или слой представления (Presentation Layer) взаимодействует со слоем домена посредством внедрение зависимостей.

Как видно на изображении, зависимости перетекают от слоя пользовательского интерфейса к слою домена, который содержит абстракции бизнес‑логики. Зависимости слоя данных, в свою очередь, также инвертированы в слой домена, чтобы скрыть детали реализации.
Это наглядная демонстрация принципа инверсии зависимостей (Dependency Inversion Principle — DIP), который я подробно описываю в своей статье о Kotlin и архитектуре Android. В ней я объясняю, что Чистая архитектура представляет собой реализацию DIP в широком масштабе.
Распространенное заблуждение об инверсии зависимостей
Многие разработчики заблуждаются, полагая, что принцип инверсии зависимостей на самом деле не изменяет направление стрелок зависимостей, как это показано на архитектурных диаграммах.
Они ошибочно полагают, что зависимости по‑прежнему должны указывать от слоя домена к слою данных, считая, что мы по‑прежнему используем функции слоя данных, но делаем это через абстрактные вызовы, а не через конкретные.
Если бы мы использовали конкретные вызовы, стрелки действительно указывали бы от слоя домена к слою данных (домен → данные), потому что слой домен будет напрямую зависеть от реализаций слоя данных.
Однако принцип инверсии зависимостей действительно инвертирует этот поток, заставляя слой домена определять интерфейсы, которые должен реализовывать слой данных. В итоге стрелки указывают внутрь (домен ← данные), потому что слой данных зависит от абстракций, определенных в слое домена. Вот почему мы называем это «инверсией» зависимости — мы буквально инвертировали направление зависимости!
Что мы рассмотрим в этой статье
В этой статье,
мы начнем с создания одного простого юзкейса, внедрим в него Koin и продемонстрируем это во ViewModel.
Затем, когда у нас будем несколько юзкейсов для домена, мы обсудим, как сгруппировать их в Manager, чтобы избежать загромождения наших ViewModels.
Мы рассмотрим пример работы с несколькими реализациями одного интерфейса (в разделе 7) — например, с разными платежными сервисами — чтобы продемонстрировать, как принцип подстановки Лисков позволяет легко менять или расширять эти реализации, без необходимости переписывать большие фрагменты кода.
Кроме того (в разделе 8), мы обсудим многомодульный подход к реализации нашего примера с оплатой.
Наконец, в разделе 10 мы поговорим о том, как поддерживать сбалансированную Чистую архитектуру, избегая оверинжиниринга.
2. Что такое Use Case?
Юзкейс, также известный как интерактор (Interactor), — это один элемент бизнес‑логики или одно действие, которое выполняет ваше приложение. Обычно он представляет собой логику, которая:
Не должна быть в коде вашего пользовательского интерфейса, поскольку смешивание этих двух аспектов приводит к трудностям в тестировании и обслуживании.
Не принадлежит к слою доступа к данным (репозиториям, API), так как этот слой должен быть сосредоточен на операциях с необработанными данными, а не на правилах, зависящих от конкретного приложения.
Создавая для каждой операции свой собственный юзкейс, вы достигаете ясности, модульности и тестируемости вашего кода. Например, FetchMemesUseCase
выполняет ровно одну задачу — извлекает мемы из определенного источника данных.
В чем заключается важность юзкейсов?
Представьте, что вы разрабатываете новую функцию. Без юзкейсов вы могли бы поместить всю логику прямо в ViewModel или Activity. Это может быть приемлемо для небольшого проекта, но по мере роста приложения вы рискуете столкнуться с огромными классами, которые будут выполнять слишком много функций.
Юзкейсы помогают избежать сложности в слое пользовательского интерфейса и делают логику домена более гибкой. Если вам нужно изменить способ извлечения мемов — например, перейти от REST API к локальной базе данных — ваш код пользовательского интерфейса останется нетронутым, так как сам интерфейс юзкейса не изменится.
Юзкейсы и принцип инверсии зависимостей
Когда вы пишете юзкейс, вы обычно используете специальные абстракции (например, интерфейс для репозитория), а не конкретные классы. Это реализация принципа инверсии зависимостей: ваш юзкейс зависит от интерфейса репозитория, а не от его конкретной реализации. Таким образом, ваша бизнес‑логика остается стабильной, даже если вы решите заменить репозиторий с REST на локальную базу данных.
Подытожим: Ключевые особенности юзкейсов
Единая ответственность: Каждый юзкейс отвечает только за одну задачу.
Простота: Позволяет избежать излишнего усложнения пользовательского интерфейса или слоев данных, используя правила, адаптированные к потребностям конкретного приложения.
Тестируемость: Легко поддается модульному тестированию, поскольку не зависит от пользовательского интерфейса.
DIP: расширяемость зависит от интерфейсов, а не от конкретных классов.
3. Реализация простого юзкейса
Давайте рассмотрим простой пример: подгрузку мемов. Хотя наш итоговый сценарий будет включать более сложный пример, связанный с платежами, процесс создания простого юзкейса остается точно таким же.
Создание интерфейса
interface FetchMemesUseCase {
suspend operator fun invoke(): List<Meme>
}
В этом определении говорится: «Класс, реализующий FetchMemesUseCase
, должен предоставить suspend функцию, которая называется invoke
и возвращает список Meme
„ов“.
Синтаксис operator fun invoke()
позволяет вызывать этот юзкейс так же, как обычную функцию (например, fetchMemesUseCase()
).
Реализация юзкейса
class FetchMemesUseCaseImpl(
private val memeRepository: MemeRepository
) : FetchMemesUseCase {
override suspend operator fun invoke(): List<Meme> {
return memeRepository.fetchMemes()
}
}
Мы внедряем
MemeRepository
для обработки операций с данными.FetchMemesUseCaseImpl
не содержит ссылок на Activities, Fragments или даже ViewModels, что делает его независимым от пользовательского интерфейса и идеально подходящим для тестирования.
Репозиторий и модель
interface MemeRepository {
suspend fun fetchMemes(): List<Meme>
}
class MemeRepositoryImpl(
private val memeApi: MemeApi
) : MemeRepository {
override suspend fun fetchMemes(): List<Meme> {
return memeApi.getMemes() // Гипотетический сетевой вызов
}
}
data class Meme(
val id: Int,
val title: String,
val imageUrl: String
)
Здесь MemeRepositoryImpl
сосредоточен на извлечении данных из memeApi.
Остальной части приложения все равно, как работает функция etchMemes()
— главное, чтобы она возвращала список Meme
«ов.
Подытожим: Реализация простого юзкейса
Подход, ориентированный на интерфейс: Начинаем с интерфейса (например,
FetchMemesUseCase
)Реализация: Держим бизнес‑логику отдельно от фреймворков пользовательского интерфейса.
Репозиторий: Скрывает детали получения данных (вызовы API, запросы к БД).
4. Внедрение юзкейсов с помощью Koin
Koin — это популярная библиотека для внедрения зависимостей в Kotlin, которая позволяет нам объявлять, как создавать наши классы, без необходимости вручную писать кучу шаблонного «монтажного» кода. Это идеально согласуется с DIP, так как мы настроим Koin для внедрения интерфейсов, а не конкретных реализаций.
Доменный модуль
val domainModule = module {
single<MemeRepository> { MemeRepositoryImpl(get()) }
factory<FetchMemesUseCase> { FetchMemesUseCaseImpl(get()) }
}
Примечание: Мы объявляем синглтон (single
) для MemeRepository
, но используем factory
для FetchMemesUseCase
. В зависимости от ваших шаблонов использования, вы можете выбрать тот или иной подход.
Почему мы использовали
single
дляMemeRepository
?
Обычно репозиторий управляет источниками данных (сетью, базой данных) или кэшированием. Наличие только одного экземпляра репозитория (single
) позволяет избежать повторного создания ресурсоемких подключений или кэшей, что значительно повышает производительность приложения.Почему мы использовали factory для FetchMemesUseCase?
Юзкейсы часто представляют собой быстрые действия и не предполагают большого количества состояний. Определяя их через фабрику (factory), мы получаем новый экземпляр каждый раз, когда они требуются, что вполне нормально для легковесных и не имеющих внутреннего состояния юзкейсов. В некоторых приложениях это помогает гарантировать, что каждое использование начинается с нуля, предотвращая любые возможные устаревшие данные или побочные эффекты, которые могли бы возникнуть из‑за внутреннего состояния юзкейса.
Когда стоит использовать single
, а когда — factory
?
Репозитории в производственном коде могут быть дорогостоящими или зависеть от потоков, поэтому обычно мы храним только один общий экземпляр. В то же время, юзкейсы могут быть достаточно эфемерными, чтобы безопасно обновляться каждый раз.
5. Использование юзкейса в ViewModel
Теперь давайте посмотрим, как мы можем вызвать наш юзкейс из ViewModel Android, чтобы при этом ответственность ViewModel
по‑прежнему заключалась только в управлении состоянием пользовательского интерфейса.
class MemeListViewModel(
private val fetchMemesUseCase: FetchMemesUseCase
) : ViewModel() {
private val _memes = MutableStateFlow<List<Meme>>(emptyList())
val memes: StateFlow<List<Meme>> = _memes
fun loadMemes() {
viewModelScope.launch {
try {
// Благодаря оператору fun invoke() мы вызываем это как функцию
val memeList = fetchMemesUseCase()
_memes.value = memeList
} catch (e: Exception) {
// Корректно обрабатываем ошибку
}
}
}
Инверсия зависимостей остается неизменной; эта ViewModel
зависит только от FetchMemesUseCase
, который представляет собой интерфейс.
6. Объединение похожих юзкейсов в менеджере
По мере роста вашего приложения у вас может появиться несколько юзкейсов, которые связаны с одним и тем же доменом, например, загрузка, удаление или обмен. Внедрение всех этих вариантов непосредственно в одну ViewModel может показаться сложным и запутанным.
Представляем менеджер
Чтобы упростить процесс, мы можем создать менеджер (иногда называемый фасадом), который будет группировать несколько юзкейсов для связанного домена. Для дальнейшего применения DIP мы можем определить интерфейс менеджера и его реализацию следующим образом:
// 1) Определяем интерфейс
interface MemeManager {
// Прямое делегирование юзкейсам
suspend fun fetchMemes(): List<Meme>
suspend fun deleteMemes(ids: Set<Int>)
suspend fun toggleFavorite(memeId: Int)
}
// 2) Предоставление конкретных реализаций
class MemeManagerImpl(
private val fetchMemesUseCase: FetchMemesUseCase,
private val deleteMemesUseCase: DeleteMemesUseCase,
private val toggleFavoriteUseCase: ToggleFavoriteUseCase
) : MemeManager {
override suspend fun fetchMemes(): List<Meme> {
// Менеджер вызывает подлежащий юзкейс
return fetchMemesUseCase()
}
override suspend fun deleteMemes(ids: Set<Int>) {
deleteMemesUseCase(ids)
}
override suspend fun toggleFavorite(memeId: Int) {
toggleFavoriteUseCase(memeId)
}
Таким образом, ваша ViewModel или DI система может использовать только MemeManager
(интерфейс), а не его конкретную реализацию MemeManagerImpl
. Это также согласуется с DIP на слое «менеджера».
Менеджер несет единую ответственность, сосредотачиваясь на исключительно делегировании задач для конкретных юзкейсов в одном домене. Он не содержит бизнес‑логику (это назначение юзкейсов) или логику пользовательского интерфейса (это назначение ViewModels) — он просто делегирует операции соответствующим юзкейсам.
Благодаря менеджеру, все операции с юзкейсами становятся доступны через единый интерфейс, позволяя ViewModel работать с единой зависимостью, не беспокоясь о конкретных реализациях.
Использование менеджера в ViewModel
class MemeListViewModel(
private val memeManager: MemeManager // Зависимость от интерфейса
) : ViewModel() {
fun loadMemes() {
viewModelScope.launch {
val memes = memeManager.fetchMemes()
// ...
}
}
}
Теперь вы можете внедрять MemeManager
точно так же, как и любой другой интерфейс:
val managerModule = module {
factory<MemeManager> { MemeManagerImpl(get(), get(), get()) }
}
Объедините этот модуль с другими вашими модулями в Koin, чтобы завершить настройку.
Единая ответственность не затронута
У каждого юзкейса по‑прежнему есть только одна задача: загрузка, удаление или переключение. Менеджер лишь группирует их вместе, что значительно упрощает конструктор ViewModel.
Благодаря наличию интерфейса‑менеджера, код более высокого уровня становится независимым от конкретной реализации менеджера, что дополнительно обеспечивает DIP.
Недостатки класса менеджера
Однако стоит отметить, что чрезмерное использование менеджеров может привести к усложнению, а не к упрощению. Если менеджер пытается охватить слишком много юзкейсов, это может привести к тому, что вы просто перенесете избыточный код из одного слоя в другой (из ViewModels в менеджер).
Краткое описание шаблона менеджера
Мы предложили шаблон менеджера для объединения нескольких юзкейсов в одном интерфейсе. Такой подход упрощает наши ViewModels, сохраняя при этом ясность в отношении каждого юзкейса с единой ответственностью.
Преимущества шаблона менеджера:
Меньше параметров конструктора в ViewModels
Единый интерфейс для каждого домена
Четкое разделение ответственности
Простой шаблон делегирования
Совет: внимательно следите за сферой ответственности каждого менеджера, чтобы убедиться, что принцип не нарушается принцип единой ответственности.
И не забывайте: согласно принципам Чистой архитектуры, менеджер не должен содержать бизнес‑логику.
Его задача заключается только в том, чтобы
— Группировать связанные юзкейсы
— Предоставлять более простой интерфейс ViewModel
— При необходимости управлять согласованностью между юзкейсами
7. Пример множественных реализаций: Принцип подстановки Лисков на примере
Даже при использовании подхода с менеджером вам может потребоваться поддерживать несколько реализаций одного и того же набора действий, таких как начисление, проверка и возврат средств. Именно здесь по‑настоящему проявляется принцип подстановки Лисков (Liskov Substitution Principle — LSP): каждый провайдер (Stripe, PayPal и т. д.) реализует одни и те же интерфейсы по‑своему, а ваш высокоуровневый код при этом остается неизменным.
Пример платежной системы: Несколько провайдеров юзкейсов и класс менеджера
Давайте рассмотрим реальный пример — платежную систему, которая должна поддерживать несколько провайдеров (платежных сервисов), таких как PayPal, Stripe, Klarna и т. д.
Чтобы эффективно управлять этими разнообразными реализациями, мы предлагаем использовать шаблон контейнера. Этот шаблон группирует связанные юзкейсы для каждого провайдера. Такой подход позволяет нам работать с провайдерами, которые могут поддерживать не все операции, сохраняя при этом чистый интерфейс.
Ключевым моментом здесь является то, что мы можем объединить несколько различных реализаций одних и тех же юзкейсов для разных провайдеров в один и тот же менеджер, при этом соблюдая все принципы SOLID.
Давайте посмотрим, как это работает на следующей диаграмме, которая демонстрирует полную архитектуру от слоя ViewModel до конкретных реализаций. Это позволит нам масштабировать или изменять провайдеров без необходимости переписывания больших фрагментов кода:
Вы можете увеличивать количество или менять провайдеров, не переписывая большие фрагменты кода.

На этой диаграмме представлена полная архитектура нашей платежной системы:
PaymentViewModel
зависит только от интерфейсаPaymentManager
PaymentManagerImpl
использует Map для выбора подходящего платежного провайдераКаждый провайдер (Stripe, PayPal, Square) реализует один и тот же набор юзкейсов, но делает это по‑своему, адаптируя их к особенностям своего сервиса
Архитектура позволяет легко добавлять новых платежных провайдеров без необходимости изменения существующего кода
Примечание: StripeChargeUseCase
должен называться StripeChargePaymentUseCase
и так далее, но чтобы диаграмма влезла по ширине в эту статью, мы намеренно удалили ключевое слово «Payment» из всех реализаций юзкейсов.
Давайте подробно рассмотрим каждый компонент…
7.1 Определение юзкейсов платежей
Мы определим три интерфейса юзкейсов — взимания средств, верификации, возврата средств — плюс модель результата:
interface ChargePaymentUseCase {
suspend operator fun invoke(amount: Double, currency: String): PaymentResult
}
interface VerifyPaymentUseCase {
suspend operator fun invoke(paymentId: String): Boolean
}
interface RefundPaymentUseCase {
suspend operator fun invoke(paymentId: String, amount: Double): PaymentResult
}
data class PaymentResult(
val success: Boolean,
val transactionId: String? = null,
val errorMessage: String? = null
)
SRP: У нас имеется три различных интерфейса, каждый из которых ориентирован на выполнение конкретной задачи.
Это способствует реализации Принцип единой ответственности (Single Responsibility Principle — SRP), выделяя для каждого действия отдельный юзкейс. Это также соответствует принципу разделения интерфейсов (Interface Segregation Principle — ISP): вы определяете только те функции из интерфейсов, которые необходимы для каждого действия. Например, если провайдеру не требуется верификация, он может исключить VerifyPaymentUseCase
из своей реализации.
Примечание о ISP
Некоторые провайдеры могут не выполнять все действия (например, не проводить верификацию). Благодаря разделению интерфейсов, каждый провайдер реализует только те интерфейсы, которые ему действительно нужны. Ни один клиент не будет зависеть от методов, которые он не использует.
7.2 Реализация платежных провайдеров
Теперь каждый платежный провайдер реализует эти юзкейсы в соответствии со своими уникальными правилами. Ниже представлены упрощенные примеры реализации Stripe и PayPal.
Реализация Stripe
class StripeChargePaymentUseCase : ChargePaymentUseCase {
override suspend fun invoke(amount: Double, currency: String): PaymentResult {
// Гипотетический вызов API Stripe
return PaymentResult(
success = true,
transactionId = "stripe_tx_12345"
)
}
}
class StripeVerifyPaymentUseCase : VerifyPaymentUseCase {
override suspend fun invoke(paymentId: String): Boolean {
// Верификация в Stripe
return true
}
}
class StripeRefundPaymentUseCase : RefundPaymentUseCase {
override suspend fun invoke(paymentId: String, amount: Double): PaymentResult {
// Возврат средств в Stripe
return PaymentResult(success = true, transactionId = "stripe_ref_67890")
}
}
Реализация PayPal
class PayPalChargePaymentUseCase : ChargePaymentUseCase {
override suspend fun invoke(amount: Double, currency: String): PaymentResult {
// Гипотетический вызов API PayPal
return PaymentResult(
success = true,
transactionId = "paypal_tx_ABC"
)
}
}
class PayPalVerifyPaymentUseCase : VerifyPaymentUseCase {
override suspend fun invoke(paymentId: String): Boolean {
// Верификация в PayPal
return false // Допустим, у нас сбой
}
}
class PayPalRefundPaymentUseCase : RefundPaymentUseCase {
override suspend fun invoke(paymentId: String, amount: Double): PaymentResult {
// Возврат средств в PayPal
return PaymentResult(success = false, errorMessage = "Refund failed")
}
}
Мы можем продолжить работу с другими провайдерами, например, с CreditCardChargePaymentUseCase
.
7.3 Группировка юзкейсов для конкретного провайдера в контейнер
Вместо того чтобы вводить в конструктор три отдельных юзкейса (взимание средств, проверка, возврат средств) для каждого провайдера, мы можем создать контейнер для юзкейсов каждого провайдера.
class PaymentProviderUseCases(
val charge: ChargePaymentUseCase,
val verify: VerifyPaymentUseCase?,
val refund: RefundPaymentUseCase?
)
верификация и возврат средств могут обнуляться (null), если этот провайдер их не поддерживает.
Пример:
val stripeUseCases = PaymentProviderUseCases(
charge = StripeChargePaymentUseCase(),
verify = StripeVerifyPaymentUseCase(),
refund = StripeRefundPaymentUseCase()
)
val paypalUseCases = PaymentProviderUseCases(
charge = PayPalChargePaymentUseCase(),
verify = PayPalVerifyPaymentUseCase(),
refund = PayPalRefundPaymentUseCase()
)
(Некоторые провайдеры могут не выполнять одно или несколько действий, поэтому они могут быть установлены как null
)
Подытожим: Контейнеры провайдеров
Меньше беспорядка в конструкторе: по одному контейнеру на провайдера.
Null‑safety: если провайдер не выполняет определенное действие, установите для него значение
null
.LSP: Каждый «слот» провайдера может быть заменен другим, который реализует те же интерфейсы.
7.4 PaymentManager с использованием Map
Теперь давайте определим единый интерфейс PaymentManager
, который будет использоваться в нашей ViewModel. Этот интерфейс включает методы для взимания средств (charge
), валидации (verify
) и возврата средств (refund
).
interface PaymentManager {
suspend fun charge(providerName: String, amount: Double, currency: String): PaymentResult
suspend fun verify(providerName: String, paymentId: String): Boolean
suspend fun refund(providerName: String, paymentId: String, amount: Double): PaymentResult
}
PaymentManagerImpl:
Мы передаем Map<String, PaymentProviderUseCases>
, где ключ соответствует имени провайдера, например, "stripe"
, "paypal"
, "creditcard"
. Такой подход обеспечивает несколько преимуществ:
Более чистый конструктор: У нас есть одна Map, а не несколько полей для каждого юзкейса.
Масштабируемость: Добавить нового провайдера так же просто, как добавить новую запись в Map.
class PaymentManagerImpl(
private val providers: Map<String, PaymentProviderUseCases>
) : PaymentManager {
override suspend fun charge(providerName: String, amount: Double, currency: String): PaymentResult {
// Извлечение контейнер нужного поставщика или обработка неизвестного поставщика
val useCases = providers[providerName.lowercase()]
?: return PaymentResult(false, errorMessage = "Unknown provider: $providerName")
// Вызов ChargePaymentUseCase конкретного провайдера
return useCases.charge(amount, currency)
}
override suspend fun verify(providerName: String, paymentId: String): Boolean {
val useCases = providers[providerName.lowercase()] ?: return false
// Если нет юзкейса verify, мы решаем вернуть false или обработать этот случай иначе
val verifyUC = useCases.verify ?: return false
return verifyUC(paymentId)
}
override suspend fun refund(providerName: String, paymentId: String, amount: Double): PaymentResult {
val useCases = providers[providerName.lowercase()]
?: return PaymentResult(false, errorMessage = "Unknown provider: $providerName")
// Если юзкейс не возвращен, возвращаем сообщение об ошибке
val refundUC = useCases.refund
?: return PaymentResult(false, errorMessage = "Refund not supported for $providerName")
return refundUC(paymentId, amount)
}
}
Ключевое преимущество: мы избегаем сложных операторов when
в ViewModel
, и мы можем добавлять новых провайдеров, просто заполняя карту.
Этот подход на основе ассоциативных массивов позволяет упорядочить код, избежать огромных списков параметров и, естественно, поддерживает LSP (каждый поставщик может предоставить любое подмножество юзкейсов по мере необходимости).
Преимущества:
Более чистый конструктор: вам нужно создать только одну Map, а не шесть или девять отдельных юзкейсов.
Проще добавлять провайдеров: создайте новый
PaymentProviderUseCases
для каждого провайдера и поместите его в Map.Соответствие принципам DIP:
PaymentManagerImpl
зависит от интерфейсов для юзкейсов и Map с провайдерами, а не от большой цепочки операторовwhen
илиif
.
7.5 Предоставление PaymentManager в Koin
Типичный модуль Koin может выглядеть следующим образом:
val paymentModule = module {
// PaymentProviderUseCases создает экземпляры для каждого провайдера
single {
PaymentProviderUseCases(
charge = StripeChargePaymentUseCase(),
verify = StripeVerifyPaymentUseCase(),
refund = StripeRefundPaymentUseCase()
)
} bind PaymentProviderUseCases::class named "stripe"
single {
PaymentProviderUseCases(
charge = PayPalChargePaymentUseCase(),
verify = PayPalVerifyPaymentUseCase(),
refund = PayPalRefundPaymentUseCase()
)
} bind PaymentProviderUseCases::class named "paypal"
// Создаем map ProviderName -> PaymentProviderUseCases
single<Map<String, PaymentProviderUseCases>> {
mapOf(
"stripe" to get<PaymentProviderUseCases>(qualifier = named("stripe")),
"paypal" to get<PaymentProviderUseCases>(qualifier = named("paypal"))
)
}
// Наконец, предоставляем наш PaymentManager
single<PaymentManager> { PaymentManagerImpl(get()) }
}
Здесь мы привязываем каждый
PaymentProviderUseCases
с уникальным спецификатором (именем"stripe"
,"paypal"
), чтобы различать их в контейнере.Затем мы создаем map для
PaymentManagerImpl
.Поскольку
PaymentManagerImpl
зависит отMap<String, PaymentProviderUseCases>
, Koin автоматически будет использовать эту map.
(Обратите внимание, что точный синтаксис может немного отличаться в зависимости от версии вашего Koin.)
7.6 Использование PaymentManager в ViewModel
Ниже представлена обновленная PaymentViewModel
с комментариями к коду, которые объясняют, как она получает правильный тип платежа от PaymentManager
:
class PaymentViewModel(
// DIP: Мы зависим от интерфейса PaymentManager, а не от конкретного класса
private val paymentManager: PaymentManager
) : ViewModel() {
// Текущие данные или поток состояний для хранения результатов списания
private val _chargeResult = MutableStateFlow<PaymentResult?>(null)
val chargeResult: StateFlow<PaymentResult?> = _chargeResult
/**
* Взимаем с данного провайдера определенную сумму в данной валюте.
* PaymentManager ищет правильные юзкейсы провайдера в своей map и вызывает "charge".
*/
fun charge(providerName: String, amount: Double, currency: String) {
viewModelScope.launch {
val result = paymentManager.charge(providerName, amount, currency)
// Обновляем наш поток состояний результатом
_chargeResult.value = result
}
}
/**
* Аналогично, могут быть вызваны 'verify' и 'refund' при каждом вызове PaymentManager,
* который перенапрвит нас к правильному юзкейсу для выбранного провайдера.
*/
// fun verify(...) { ... }
// fun refund(...) { ... }
}
Вызов
charge(«stripe», 50.0, «USD»)
поручает менеджеру, найти"stripe"
в его map, извлечь контейнер из юзкейсов Stripe, и вызватьcharge
.Если
"stripe"
не найден или в контейнере отсутствует данный юзкейс, менеджер возвращает соответствующую ошибку.
Теперь каждое платежное действие (списание, проверка, возврат) сводится к юзкейсу, который имеет свою собственную реализацию, зависящую от провайдера. Менеджер просто ищет подходящие юзкейсы PaymentProviderUseCases
в своей map и вызывает соответствующий метод.
Благодаря LSP вы можете подключить любого нового провайдера с его собственным набором юзкейсов. ViewModel или код более высокого уровня зависит только от PaymentManager
, усиляя DIP и провайдера.
В следующей части мы рассмотрим мультипровайдерные и многомодульные системы!
Если вы хотите освоить основы Android‑разработки, начните с нашего курса «Android Developer. Basic». Этот курс предоставит вам все необходимые знания для успешного старта в разработке мобильных приложений.
Также приглашаем вас на открытые уроки, которые помогут разобраться в ключевых аспектах разработки и понять, как курс поможет развить ваши навыки.