Architecture
10.03.2021 | https://habr.com/ru/company/itelma/blog/546372 |
23.07.2024 | https://apptractor.ru/develop/mvi-v-eventbrite.html |
13.08.2023 | https://apptractor.ru/info/articles/chto-takoe-mvvm-arhitektura.html |
Структура типов
class
Шаблон или схема для создания объектов. Класс определяет атрибуты (состояние) и методы (поведение), которые объекты этого класса будут иметь.
class Car(val make: String, val model: String) {
fun drive() {
println("Driving $make $model")
}
}
abstract class
Класс, который не может быть создан напрямую и предназначен для того, чтобы служить базой для других классов. Он может содержать как абстрактные методы (без реализации), так и методы с реализацией. Абстрактные методы должны быть реализованы в производных классах. Абстрактные классы используются для создания общего интерфейса для группы связанных классов.
// Абстрактный класс
abstract class Animal {
// Абстрактный метод
abstract fun makeSound()
// Метод с реализацией
fun sleep() {
println("This animal is sleeping")
}
}
// Конкретный класс, который наследует абстрактный класс
class Dog: Animal() {
override fun makeSound() {
println("Woof")
}
}
// Использование
fun main() {
val myDog = Dog()
myDog.makeSound() // Выведет: Woof
myDog.sleep() // Выведет: This animal is sleeping
}
interface
Контракт или соглашение, который определяет набор методов, которые класс должен реализовать. Интерфейсы описывают поведение, но не содержат реализации методов. Они позволяют классу реализовать несколько различных интерфейсов и служат для обеспечения гибкости и многоразового использования кода.
interface Drivable {
fun start()
fun stop()
}
class Car : Drivable {
override fun start() {
println("Car is starting")
}
override fun stop() {
println("Car is stopping")
}
}
Aggregation
Агрегация - концепция в ООП, которая обозначает отношение “часть-целое”, где одна сущность содержит или управляет другой, но при этом обе могут существовать независимо друг от друга. В агрегации связь между объектами слабее по сравнению с композицией. Объекты могут существовать независимо друг от друга, и удаление одного объекта не обязательно приводит к удалению связанного объекта. Объекты могут быть частью нескольких агрегатов.
// Пример: класс Library может содержать несколько объектов класса Book, но книги могут существовать и без библиотеки.
class Book(val title: String)
class Library(val name: String) {
private val books = mutableListOf<Book>()
fun addBook(book: Book) {
books.add(book)
}
fun getBooks(): List<Book> {
return books
}
}
fun main() {
val book1 = Book("1984")
val book2 = Book("Brave New World")
val library = Library("City Library")
library.addBook(book1)
library.addBook(book2)
println(library.getBooks().map { it.title }) // Output: [1984, Brave New World]
}
method signature
Сигнатура метода - это часть определения метода, которая включает его имя и список параметров (их типы и порядок).
Не включает возвращаемый тип, модификаторы доступа и бросаемые исключения.
fun calculate(a: Int, b: Double) // Сигнатура метода — это calculate(Int, Double).
Clean Architecture
Архитектура, написанная с заботой. Код разделен на слои, по структуре похоже на лук, с одним правилом зависимости: внутренний уровень не должен зависеть от каких-либо внешних уровней. Зависимости должны указываться внутри каждого уровня, чтобы не было зависимостей между уровнями (слоями).
• облегчить дальнейшую модификацию.
• высокий уровень абстракции.
• слабая связанность между частями кода.
• легкая тестируемость.
Data | Данные приложения. |
Domain | Бизнес-логика. Дополнительный слой маппинга данных. |
Presentation | Пользовательский интерфейс. |
Repository | Объект предоставляющий доступ к данным с возможностью выбора источника данных в зависимости от условий. |
Interactor | Реализует сценарии использования бизнес-объектов. |
UseCase | Действие, которое может совершить пользователь. |
Entities | Бизнес-объекты приложения. |
MVVM
Model-View-ViewModel. Архитектурный шаблон, используемый в разработке программного обеспечения для разделения пользовательского интерфейса (UI) от бизнес-логики и данных. ViewModel ничего не знает про View. Unidirectional data flow.
Model отвечает за представление данных и бизнес-логику приложения. Модель может включать в себя операции с данными, хранение информации и управление состоянием приложения.
View отвечает за отображение данных и взаимодействие с пользователем. Это компонент, с которым пользователь взаимодействует, и он визуализирует данные, предоставляемые ViewModel.
ViewModel служит посредником между Model и View. ViewModel преобразует данные из Model в формат, который может быть легко отображен в View, и обрабатывает пользовательские действия, перенаправляя их в Model. ViewModel также позволяет реализовать биндинг (связывание) данных между Model и View.
• способствует улучшению читаемости кода, облегчает тестирование и делает приложения более масштабируемыми, так как разделение бизнес-логики и представления делает каждый компонент более независимым.
Преимущества:
• разделение ответственности: MVVM четко разделяет бизнес-логику (Model) от представления (View) и управления представлением (ViewModel). Это способствует улучшению читаемости кода и упрощает поддержку и развитие приложения.
• тестирование: MVVM упрощает тестирование, так как ViewModel может быть отдельно протестирована, и ее можно тестировать независимо от View. Это делает юнит-тестирование более эффективным и позволяет легче обнаруживать и исправлять ошибки.
• биндинг данных: MVVM позволяет использовать data binding между Model и View через ViewModel. Это позволяет автоматически обновлять пользовательский интерфейс при изменении данных в Model, что упрощает синхронизацию данных и представления.
• масштабируемость: Разделение компонентов MVVM делает приложение более масштабируемым. Разработчики могут работать над разными частями приложения независимо, что позволяет параллельное развитие.
• архитектурная гибкость: MVVM дает возможность легко изменять пользовательский интерфейс без необходимости переписывать всю бизнес-логику. Также легче адаптировать приложение под разные платформы, так как ViewModel может оставаться практически неизменной.
• улучшенный UX-дизайн: MVVM позволяет разделить дизайн пользовательского интерфейса и логику взаимодействия с данными. Это улучшает процесс совместной работы дизайнеров и разработчиков, позволяя им работать над интерфейсом независимо.
• использование реактивных подходов: MVVM хорошо сочетается с реактивным программированием, позволяя более эффективно реагировать на изменения данных и событий в приложении.
Недостатки:
• комплексность: Внедрение этого шаблона может потребовать больше времени и усилий на начальном этапе разработки из-за необходимости создания ViewModel для каждого View. Это может добавить сложности, особенно для простых приложений.
• увеличение количества классов: Использование шаблона может привести к увеличению количества классов в проекте, особенно если приложение имеет много экранов и компонентов. Это может сделать проект более громоздким и усложнить его обслуживание.
• сложности при обработке сложной бизнес-логики: Если бизнес-логика приложения довольно сложная и требует прямого взаимодействия между Model и View, MVVM может создать дополнительный уровень абстракции (ViewModel), что может усложнить обработку таких сценариев.
MVP
Model-View-Presenter. Взаимодействие Presenter и View построено через интерфейс. View знает про Presenter и наоборот. Two-way binding.
Model представляет данные и бизнес-логику приложения.
View отвечает за отображение данных и взаимодействие с пользователем.
Presenter извлекает данные из модели и уведомляет View через интерфейс, презентер управляет состоянием представления и выполняет действия в соответствии с уведомлениями пользователя из View.
• взаимодействие между View-Presenter и Presenter-Model происходит через интерфейс.
• один класс презентера управляет одним представлением
• модель и view ничего не знают о существовании друг друга
MVI
Model-View-Intent. Intent обрабатывает события от View и передает их в Model, она обрабатывает события и возвращает новую, готовую для отображения модель, которую отобразит View.
Model представляет данные и бизнес-логику приложения. В MVI модель неизменяема и представляет собой текущее состояние приложения.
View отвечает за отрисовку пользовательского интерфейса и реакцию на ввод данных пользователем. Однако, в отличие от MVVM и MVC, представление в MVI является пассивным компонентом. Он не взаимодействует с моделью напрямую и не принимает решений на основе данных. Вместо этого он получает обновления состояния и пользовательские намерения от ViewModel.
Intent представляет собой действия пользователя или события, происходящие в пользовательском интерфейсе, такие как нажатие кнопки или ввод текста. В MVI эти намерения перехватываются представлением и отправляются во ViewModel для обработки.
ViewModel в MVI она отвечает за управление состоянием приложения и бизнес-логикой. Она получает пользовательские намерения от представления, обрабатывает их и соответствующим образом обновляет модель. Затем ViewModel выдает новое состояние, которое View наблюдает и отображает.
• однонаправленность - с данными работает только одна сущность, и мы знаем, кто изменяет данные, зачем и почему.
• неизменяемость состояния - новое состояние складывается из предыдущего и какого-то действия. Изменить данные мы не можем, можем только получить новые.
• удобство логирования и отладки - легко воспроизвести, где была ошибка, и собрать все условия (отследить в crashlytics текущий state и тот, что был до краша.
• удобно работать с jetpack compose.
Преимущества:
• хорошо интегрируется с декларативными UI фреймворками такими как Compose и SwiftUI.
• один единственный стейт, который рисуется на вьюшке.
• стейт меняется в одном месте. Благодаря этому легко дебажить, и на экране невозможно увидеть не консистентное состояние. То есть мы всегда видим один конкретный стейт.
Недостатки:
• бойлерплейт. стейт может быть большим на сложном экране, например, может быть 15 полей в стейте.
• нужно считать дифф (разницу, изменения) состояния и перерисовывать только изменения. Если не считать дифф, а каждый раз перерисовывать весь экран при обновлении стейта, то UI может подлагивать... Для этого нужно считать дифф состояния и перерисовывать только изменения.
Reducer
Обновляет состояние в зависимости от действия. Определяет, как состояние приложения должно измениться в ответ на действия пользователя (интенты). Принимает текущее состояние и действие, а затем возвращает новое состояние.
Middleware
Обрабатывает побочные эффекты, такие как асинхронные операции или взаимодействие с внешними системами, и передает результат обратно в Reducer. Middleware может перехватывать действия и выполнять дополнительную логику до или после их обработки.
UDF
Unidirectional Data Flow (однонаправленный поток данных) — это концепция архитектурного паттерна, который используется для управления состоянием в приложениях. Основная идея заключается в том, что данные в приложении изменяются и передаются в одном направлении. Этот паттерн помогает в управлении сложностью и предсказуемостью приложения, а также улучшает масштабируемость и тестируемость кода.
• Один источник истины. Все состояние приложения хранится в одном месте, обычно в модели или хранилище состояния.
• Изменение состояния. Для изменения состояния приложения используются только действия (actions) или события, которые обрабатываются функциями или reducers, изменяющими состояние.
• Обновление пользовательского интерфейса. Изменения состояния передаются в пользовательский интерфейс через связывание данных. Это обеспечивает синхронизацию UI с текущим состоянием.
• Однонаправленный поток. Поток данных идет от источника (например, модели) через действия и обработчики к пользовательскому интерфейсу, где он отображается. Обратная связь с UI осуществляется через действия, которые инициируют изменение состояния.
// Модель (Model)
data class User(val name: String, val age: Int)
// ViewModel
class UserViewModel: ViewModel() {
private val _user = MutableLiveData<User>()
val user: LiveData<User> get() = _user
fun updateUser(name: String, age: Int) {
_user.value = User(name, age)
}
}
// Activity/Fragment
class UserActivity: AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_user)
viewModel.user.observe(this, Observer { user ->
// Обновление UI на основе новых данных
userNameTextView.text = user.name
userAgeTextView.text = user.age.toString()
})
updateButton.setOnClickListener {
viewModel.updateUser("John Doe", 30)
}
}
}
ООП
Object-Oriented Programming (OOP). Объектно-ориентированное программирование (ООП) - методология программирования основанная на представлении программы в виде совокупности объектов каждый из которых является экземпляром определенного класса а классы образуют иерархию наследования.
Encapsulation
Инкапсуляция позволяет скрывать внутренние детали реализации объекта и предоставлять доступ к данным через методы. Это означает, что объект контролирует доступ к своим данным и предотвращает их неконтролируемое изменение.
// В данном примере name скрыт от прямого доступа извне и доступен только через методы setName и getName.
class Person {
private var name: String = ""
fun setName(name: String) {
this.name = name
}
fun getName(): String {
return name
}
}
Polymorphism
Полиморфизм позволяет объектам разного типа обрабатывать вызовы методов, имеющих одно имя, но различающихся поведением. Это позволяет использовать единый интерфейс для работы с различными типами объектов.
open class Animal {
open fun makeSound() {
println("Какой-то звук")
}
}
class Dog: Animal() {
override fun makeSound() {
println("Гав")
}
}
class Cat: Animal() {
override fun makeSound() {
println("Мяу")
}
}
fun main() {
val animals: List<Animal> = listOf(Dog(), Cat())
animals.forEach { it.makeSound() }
}
Inheritance
Наследование позволяет создавать новые классы на основе существующих. Новый класс (производный класс) наследует свойства и методы базового класса, что позволяет повторно использовать код и создавать иерархию классов.
open class Animal {
fun eat() {
println("Жрет")
}
}
class Dog: Animal() {
fun bark() {
println("Гавкает")
}
}
Abstraction
Абстракция позволяет выделить общие черты объектов и скрыть детали реализации. Абстрактные классы и интерфейсы используются для описания общих характеристик и поведения, которые должны быть реализованы в конкретных классах.
// В данном примере Shape – абстрактный класс с абстрактным методом draw, который реализуется в конкретных классах Circle и Square.
abstract class Shape {
abstract fun draw()
}
class Circle: Shape() {
override fun draw() {
println("Drawing a Circle")
}
}
class Square: Shape() {
override fun draw() {
println("Drawing a Square")
}
}
Development Principles
SOLID
Пять основополагающих принципов ООП, разработанных для повышения гибкости, расширяемости и удобства поддержки кода.
Single Responsibility | Принцип Единственной Ответственности | Каждый класс или метод должны иметь одну ответственность. Не стоит нагружать классы большой логикой. Пример: RecyclerView: ItemAnimator LayoutManager Adapter DiffUtil . |
Open-Closed | Принцип Открытости/Закрытости | Классы должны быть открыты для расширения, но закрыты для модификации. Поведение класса можно расширить без изменения существующего кода. |
Liskov Substitution | Принцип Подстановки Лисков | Объекты базового класса должны быть заменимы объектами его подклассов без изменения корректности программы. Подклассы должны поддерживать контракт базового класса. |
Interface Segregation | Принцип Разделения Интерфейсов | Интерфейсы должны быть специфичными и не содержать методов, которые не нужны реализациям. Это предотвращает ситуацию, когда класс вынужден реализовывать методы, которые ему не нужны. |
Dependency Inversion | Принцип Инверсии Зависимостей | Модули высокого уровня не должны зависеть от модулей низкого уровня. Оба типа модулей должны зависеть от абстракций (интерфейса). Абстракции не должны зависеть от деталей, детали должны зависеть от абстракций. |
DRY
Don't Repeat Yourself. Не повторяйся. Дублирование кода – пустая трата времени и ресурсов. Вам придется поддерживать одну и ту же логику и тестировать код сразу в двух местах, причем если вы измените код в одном месте его нужно будет изменить и в другом. В большинстве случаев дублирование кода происходит из-за незнания системы. Прежде чем что-либо писать, проявите прагматизм: осмотритесь. Возможно, эта функция где-то реализована. Возможно эта бизнес-логика существует в другом месте. Повторное использование кода – всегда разумное решение.
KISS
Keep It Simple, Stupid. Будь проще. Простые системы будут работать лучше и надежнее сложных. Не придумывайте к задаче более сложного решения, чем ей требуется. Иногда самое разумное решение оказывается и самым простым. Написание производительного, эффективного и простого кода – это прекрасно. Одна из самых распространенных ошибок нашего времени – использование новых инструментов исключительно из-за того, что они блестят. Разработчиков следует мотивировать использовать новейшие технологии не потому, что они новые, а потому что они подходят для работы.
YAGNI
You Aren’t Gonna Need It. Вам это не понадобится. Если пишете код, будьте уверены, что он понадобится. Не пишите код, если думаете, что он пригодится позже. Принцип применим при рефакторинге. Если вы занимаетесь рефакторингом метода, класса или файла, не бойтесь удалять лишние методы. Даже если раньше они были полезны – теперь они не нужны. Может наступить день, когда они снова понадобятся – тогда вы сможете воспользоваться git-репозиторием, чтобы воскресить их из мертвых.
BDUF
Big Design Up Front. Глобальное проектирование прежде всего. Прежде чем переходить к реализации, убедитесь, что все хорошо продумано. Составив план, вы избавите себя от необходимости раз за разом начинать с нуля.
APO
Avoid Premature Optimization. Избегайте преждевременной оптимизации. Эта практика побуждает разработчиков оптимизировать код до того, как необходимость этой оптимизации будет доказана. Прежде чем вы погрузитесь в детали реализации, убедитесь, что эти оптимизации действительно полезны. Пример – масштабирование. Вы не станете покупать 40 серверов из предположения, что ваше новое приложение станет очень популярным. Вы будете добавлять серверы по мере необходимости. Преждевременная оптимизация может привести к задержкам в коде и, следовательно, увеличит затраты времени на вывод функций на рынок.
Occam's razor
Бритва Оккама. Не создавайте ненужных сущностей без необходимости. Будьте прагматичны — подумайте, нужны ли они, поскольку они могут в конечном итоге усложнить вашу кодовую базу.
Сlasses and objects relationships
association
Ассоциация. Отношение, при котором объекты одного типа неким образом связаны с объектами другого типа. Например объект одного типа содержит или использует объект другого типа. Например, игрок играет в определенной команде:
class Team
class Player {
val team: Team
}
composition
Композиция определяет отношение HAS A, то есть отношение "имеет". Например, в класс автомобиля содержит объект класса электрического двигателя. При этом класс автомобиля полностью управляет жизненным циклом объекта двигателя. При уничтожении объекта автомобиля в области памяти вместе с ним будет уничтожен и объект двигателя. И в этом плане объект автомобиля является главным, а объект двигателя - зависимой.
class ElectricEngine
class Car {
val engine: ElectricEngine = ElectricEngine()
}
aggregation
Агрегация. Следует отличать от композиции. Предполагает то же отношение, но другую реализацию. При агрегации реализуется слабая связь, то есть в данном случае объекты Car и Engine будут равноправны. В конструктор Car передается ссылка на уже имеющийся объект Engine. И, как правило, определяется ссылка не на конкретный класс, а на абстрактный класс или интерфейс, что увеличивает гибкость программы.
abstract class Engine
class Car(
var engine: Engine
)
Design Patterns
Паттерны проектирования необходимы для быстрого решения типовых задач в программировании. Паттерны бывают 3 разновидностей:
• архитектурные паттерны. Шаблоны высшего уровня. описывают структурную схему программной системы в целом. В этой схеме располагаются отдельные подсистемы и определяются отношения между ними. Пример: MVP.
• паттерны проектирования. Они описывают схемы детализации программных подсистем и их отношений между собой. Такие паттерны никак не влияют на структуру программной системы в целом и не зависят от использования языка программирования. Пример: Adapter, Singleton.
• идиомы. Паттерны низкого уровня. Реализация той или иной проблемы с учётом специфики соответствующего языка программирования.
Паттерны проектирования:
• порождающие. Пример: Factory, Singleton.
• структурные. Пример: Decarator, Adapter.
• поведенческие. Пример: Observer, Strategy.
Adapter
Позволяет объектам с несовместимыми интерфейсами работать вместе. Адаптер предусматривает создание класса-оболочки с требуемым интерфейсом.
/**
* Совместимый интерфейс и его реализация
*/
interface NickName {
fun getNickName(person: Person): String
}
class NickNameImpl: NickName {
override fun getNickName(person: Person): String = person.name
}
/**
* Несовместимый интерфейс и его реализация
*/
interface FullName {
fun getFullName(person: Person): String
}
class FullNameImp: FullName {
override fun getFullName(person: Person): String = "${person.name} ${person.family}"
}
class Client(
private val nickName: NickName
) {
private val person = Person("John", "Marston")
val name: String
get() = nickName.getNickName(person)
}
class Adapter(
private val fullName: FullName
): NickName {
override fun getNickName(person: Person): String {
return fullName.getFullName(person)
}
}
val nickName: NickName = NickNameImpl()
val fullName: FullName = FullNameImp()
val adapter: NickName = Adapter(fullName)
val clientTarget = Client(nickName)
println(clientTarget.name) // output: John
val clientAdapter = Client(adapter)
println(clientAdapter.name) // output: John Marston
Decorator
Динамически добавляет новую функциональность объекту, используя класс-обертку. Пример: при добавлении нового ключа в HashMap, сообщать об этом.
interface Name {
fun getName(firstName: String): String
}
class SimpleName: Name {
override fun getName(firstName: String): String {
return firstName
}
}
open class NameDecorator(
protected var name: Name
): Name {
override fun getName(firstName: String): String {
return name.getName(firstName)
}
}
class FullName(
name: Name
): NameDecorator(name) {
val lastName: String = "Marston"
override fun getName(firstName: String): String {
return name.getName(firstName) + " " + lastName
}
}
val firstName: String = "John"val name: Name = SimpleName()
println(name.getName(firstName)) // output: John
val nameDecorator: Name = NameDecorator(name)
println(nameDecorator.getName(firstName)) // output: John
val fullName: Name = FullName(name)
println(fullName.getName(firstName)) // output: John Marston
Facade
Оборачивает сложную подсистему более простым интерфейсом. Предоставляет унифицированный интерфейс вместо набора интерфейсов некоторой подсистемы. Определяет интерфейс более высокого уровня, упрощающий использование подсистемы.
interface Name {
fun getName(): String
}
class NameImp: Name {
override fun getName(): String = "John"
}
interface FullName {
fun getFullName(): String
}
class FullNameImp: FullName {
override fun getFullName(): String = "John Marston"
}
class FacadeName(
private val name: Name,
private val fullName: FullName
) {
fun getName(): String = name.getName()
fun getFullName(): String = fullName.getFullName()
}
val name: Name = NameImp()
val fullName: FullName = FullNameImp()
val facade = FacadeName(name, fullName)
println(facade.getName()) // output: John
println(facade.getFullName()) // output: John Marston
Composite
Компоновщик. Позволяет обращаться к отдельным объектам и к группам объектов одинаково. Объединяет объекты в древовидную структуру для представления иерархии от частного к целому.
open class Person(
private val name: String
) {
open fun getName(): String = name
}
class John: Person("John")
class Arthur: Person("Arthur")
open class Composite(
private val name: String
): Person(name) {
private val persons: MutableList<Person> = mutableListOf()
fun add(person: Person) {
persons.add(person)
}
override fun getName(): String {
return name + ", " + persons.joinToString(", ") { it.getName() }
}
}
val composite = Composite("Sadie")
composite.add(John())
composite.add(Arthur())
println(composite.getName()) // output: Sadie, John, Arthur
Singleton
Когда нам необходим один экземппляр объекта и глобальная точка доступа к нему. Пример: класс Application.
Android Architecture. Вопросы на собесе
- Перечисли слои чистой архитектуры в контексте Android?
- Различие Interactor и UseCase?
- Что входит в сигнатуру метода?
- Расскажи про паттерны банды четырех?
- Преимущества MVI?
Преимущества MVI включают единый источник истины, который упрощает управление состоянием и отладку, а также четкое разделение на Model, View и Intent с односторонним потоком данных.
- Где в clean-архитектуре используется принцип Dependency Inversion из SOLID?
В Clean Architecture принцип Dependency Inversion помогает отделить бизнес-логику от деталей реализации, определяя интерфейсы в бизнес-логике и внедряя их реализации через Dependency Injection. Это улучшает гибкость и тестируемость, позволяя легко заменять реализации без изменения бизнес-логики.
- Что такое UDF?
Unidirectional Data Flow (UDF) — это архитектурный паттерн, при котором данные в приложении передаются и изменяются в одном направлении, обеспечивая предсказуемость и упрощая управление состоянием.
- Что такое агрегация?
Это отношение между объектами, где один объект содержит другой, но оба могут существовать независимо друг от друга.
- Разница между наследованием и композицией? abstract class vs interface?
Наследование создает иерархию классов с повторным использованием кода через базовый класс, тогда как композиция строит объекты из других объектов, делегируя выполнение задач, что обеспечивает большую гибкость и меньшую связанность.
- Как MVVM соотносится с Clean Architecture?
Model ↔ Data. View ↔ Presentation. ViewModel ↔ Domain. Model будет в Presentation-слое.
- Где в Android нарушается 1 принцип SOLID?
Бизнес-логика в Activity/Fragment. Класс ListView
- Какой компонент MVI отвечает за определение нового состояния на основе текущего состояния и действия?
• ViewModel
• Middleware
• Reducer
• Controller
- Какой компонент MVI отвечает за обработку побочных эффектов, таких как сетевые запросы?
• SideEffectHandler
• Interactor
• Reducer
• Middleware
- Какой из следующих компонентов используется для отображения данных в MVVM архитектуре?
• Controller
• Model
• View
• ViewModel
- Какой компонент отвечает за обеспечение реактивного обновления данных в MVVM?
• DataSource
• Repository
• ViewModel
• LiveData