Зачем создавать собственную версию UseCase? Причина очевидна: у каждого из нас уникальный опыт и свои подходы к решению проблем. Делясь собственной версией UseCase, я не опровергаю идеи других разработчиков. Просто предлагаю свою точку зрения на то, как вижу проблему, и решение, которое подходит мне наилучшим образом. Возможно, мое решение покажется вам интересным или вы почерпнете из него новые идеи. В этом и заключается цель сотрудничества — вносить свой вклад в копилку общих знаний и способствовать появлению различных точек зрения в сообществе разработчиков программного обеспечения.
Что такое UseCase?
В разработке программного обеспечения UseCase — это описание конкретного действия или взаимодействия, которое система или приложение выполняет для достижения определенной цели субъекта (пользователя или другой системы). В нем описываются “шаги” и взаимодействия между различными компонентами, чтобы продемонстрировать, как конкретная функциональность используется в системе. UseCase часто используется для фиксирования “функциональных требований” и определения поведения системы с точки зрения ее пользователей.
В контексте чистой архитектуры термин “UseCase” относится к конкретному архитектурному компоненту, представляющему бизнес-операцию или задачу, которую необходимо выполнить приложению. UseCase является частью доменного слоя и действует как посредник между слоем представления (UI) и доменным слоем. UseCase отвечает за организацию потока данных и бизнес-логики для достижения желаемого результата.
Таким образом, UseCase в разработке программного обеспечения описывает функциональность с точки зрения пользователя. А в контексте чистой архитектуры UseCase представляет собой конкретную бизнес-операцию в доменном слое, придерживающуюся принципов разделения задач и модульности.
Что такое UseCase в гибкой и функционально чистой архитектуре?
В контексте Fluent and Fun Clean Architecture (гибкой и функционально чистой архитектуры) UseCase служит более всеобъемлющей цели по сравнению с традиционным сценариями использования. Вместо того чтобы описывать функциональность исключительно с точки зрения пользователя, мы подходим к ней с точки зрения приложения при выполнении события или действия.
Это означает, что UseCase не только описывает действия пользователя, но и представляет шаги, необходимые для достижения определенного результата при инициации события. Объединяя эти подходы, мы создаем выразительные и удобочитаемые UseCase, соответствующие требованиям приложения.
Расширенные возможности позволяют разрабатывать гибкую систему с инкапсулированной бизнес-логикой независимо от UI и доступа к данным. Это оптимизирует модульность и поддерживаемость, облегчая адаптацию и улучшение программного обеспечения по мере необходимости. Сосредоточившись на том, как процесс выглядит в контексте приложения, и детализируя шаги по обработке событий, мы получаем более глубокое понимание поведения системы, что приводит к более эффективной и адаптируемой разработке программного обеспечения.
Как создать UseCase в рамках гибкой и функционально чистой архитектуры?
Прежде чем мы углубимся в особенности UseCase в контексте гибкой и функционально чистой архитектуры, разберемся с тем, как реализовать UseCase в рамках чистой архитектуры.
Для лучшего понимания рассмотрим простой пример UseCase в приложении электронной коммерции.
UseCase: Place Order (размещение заказа).
Действие: Клиент.
Описание: Этот UseCase представляет собой процесс размещения клиентом заказа на товар в приложении электронной коммерции.
Требования:
- Клиент должен быть авторизован.
- Корзина не должна быть пуста, чтобы можно было оформить заказ.
- Сумма денег в кошельке клиента не должна быть меньше суммы заказанного товара.
- Если все три условия соблюдены, необходимо обновить запас продукции.
- После завершения оформления заказа корзина в приложении должна быть очищена.
Чтобы реализовать это все в рамках чистой архитектуры, вы, как правило, следуете структуре кода, подобной приведенной ниже.
class PlaceOrderUseCase(
private val userRepository: UserRepository,
private val productRepository: ProductRepository
) {
operator fun invoke(order: Order) {
if (userRepository.isLoggedIn()) {
val cart = userRepository.getCart()
if (cart.isNotEmpty()) {
if (userRepository.hasEnoughFunds(order.getTotalPrice())) {
productRepository.updateProductStock(order)
userRepository.clearCart()
} else {
throw InsufficientFundsException(
"Not enough funds in the wallet."
)
}
} else {
throw EmptyCartException("The cart is empty.")
}
} else {
throw NotLoggedInException("User is not logged in.")
}
}
}
Теперь разберем ее шаг за шагом, чтобы создать UseCase:
- Создайте класс с именем, состоящим из глагола в настоящем времени + существительное/(что) (опционально) + UseCase.
- Определите необходимые параметры, которые могут быть либо Repositories, либо другими UseCase, в зависимости от требований приложения.
- В Kotlin можно сделать экземпляры класса UseCase вызываемыми в качестве функций, определив функцию
invoke()
с модификаторомoperator
.
Для получения дополнительной информации обратитесь к документации разработчика Android, где показано, как определяются доменный слой и UseCase, что похоже на подход, описанный выше.
В контексте гибкой и функционально чистой архитектуры цель состоит в том, чтобы достичь реализации, аналогичной описанной ниже.
OrderUseCase.kt
fun OrderRepository.placeOrder(order: Order) {
whenUserLoggedIn {
whenCartNotEmpty {
withSufficientFunds {
updateProductStock(order)
clearCart()
}
}
}
}
Основная цель состоит в том, чтобы предоставить четкое и сжатое описание необходимых шагов при оформлении заказа. В следующем фрагменте кода вы заметите, что он читается как естественный язык, поэтому UseCase сразу можно понять. Перед оформлением заказа необходимо выполнить несколько проверок.
Разбивка реализации:
- Вместо использования класса, создан файл с именем “OrderUseCase”.
- Имя файла соответствует условию “существительное/что (опционально) + UseCase”.
- Здесь не задействован класс. Вместо этого используется функция расширения высшего уровня или обычная функция высшего уровня.
- Сама функция представляет собой UseCase.
- Имя функции служит первичным UseCase, который вызывается из уровня представления.
В чем преимущества такого UseCase?
- Удобочитаемость и выразительность. Используется цепочка вызовов функций со значимыми именами (
whenUserLoggedIn
,whenCartNotEmpty
,withSufficientFunds
), поэтому код становится более читабельным и выразительным. Каждый шаг четко описан, что облегчает понимание процесса использования. - Упрощенный поток управления. Цепочка вызовов функций следует естественному потоку условий, которые необходимо выполнить для размещения заказа. Таким образом, упрощается процесс управления, а логика становится более понятной.
- Модульность. Код разделяет задачи на различные функции (
whenUserLoggedIn
,whenCartNotEmpty
,withSufficientFunds
), заставляя каждую функцию обрабатывать определенную проверку или условие. Это способствует модульности и упрощает изменение или добавление новых проверок, не затрагивая весь UseCase. - Лаконичность. Код позволяет избежать необходимости в отдельном классе UseCase с шаблонным кодом. Он сводит логику к лаконичной и целенаправленной функции расширения.
- Текучий интерфейс. Использование ключевого слова
when
в именах функций (whenUserLoggedIn
,whenCartNotEmpty
) придает коду стиль текучего интерфейса, улучшая читаемость кода и делая его более похожим на естественный язык. - Ясность намерений. Разбивка логики UseCase на отдельные функции позволяет разработчику четко выражать свои намерения на каждом этапе процесса. Становится очевидным, что представляет собой каждое условие в контексте размещения заказа.
В целом, мы предпочитаем этот подход, поскольку он предлагает более краткий, выразительный и модульный способ определения логики UseCase. Это сокращает количество шаблонного кода и помогает разработчикам сосредоточиться на конкретных шагах и условиях, необходимых для размещения заказа. Кроме того, здесь мы имеем дело с примером применения концепций функционального программирования, таких как цепочки и текучие интерфейсы, для разработки более читаемого и выразительного кода.
Почему бы не использовать класс для реализации UseCase?
Это вполне резонный вопрос. Прежде чем дать на него ответ, приведем определение класса в программировании.
- Классы используются для определения схем создания объектов. Можно создать несколько экземпляров (объектов) класса с различным состоянием и поведением.
- Классы могут иметь свойства, функции-члены и конструкторы, что позволяет создавать экземпляры с различными инициализациями и состояниями.
- Если нужно создать несколько экземпляров концепта (например, несколько пользователей, продуктов и т. д.), обычно используется класс.
Что такое состояние и поведение в классе?
- Состояние представляет текущие данные или свойства каждого объекта. Оно выражено переменными экземпляра (полями) внутри класса. Каждый объект, созданный из класса, имеет свой набор этих переменных. Например, в классе Car (автомобиль) атрибуты состояния могут включать цвет, марку, модель, год выпуска и уровень топлива, причем каждый экземпляр имеет свои конкретные значения.
class Car(private val color: String,
private val make: String,
private val model: String,
private val year: Int) {
private var fuelLevel: Double = 0.0
}
- Поведение в классе определяет действия или операции, которые могут выполнять объекты. Оно представлено методами внутри класса. Эти методы определяют, как одни объекты взаимодействуют с другими объектами и внешним миром. Например, в классе Car могут быть такие методы, как start(), accelerate(), brake() и refuel(), для представления различных поведений, которые может демонстрировать объект Car в зависимости от его состояния.
class Car {
private var fuelLevel: Double = 0.0
// Поведение (методы)
fun start() {
println("Car has started.")
}
fun accelerate() {
println("Car is accelerating.")
}
fun brake() {
println("Car is braking.")
}
fun refuel(amount: Double) {
fuelLevel += amount
println("Car has been refueled with $amount gallons.")
}
А теперь ответим на поставленный выше вопрос
Классы действительно являются мощной концепцией в программировании. Однако в случае определенных UseCase в Kotlin мы не раскрываем полностью функционал класса, если используем его только для вызова одного поведения или функции.
В таких сценариях выбор функции вместо класса обеспечивает более простой и лаконичный подход. Функция позволяет напрямую определить логику для UseCase, не вводя ненужный синтаксис и структуру, связанные с классом. Это также согласуется с принципами функционального программирования, побуждая нас разрабатывать UseCase как чистые функции, которые предсказуемы, тестируемы и менее подвержены побочным эффектам.
Чтобы проиллюстрировать этот момент, рассмотрим простую аналогию. Представим, что автомобиль — это класс, а прогулка — функция. Если нужно добраться до места, которое находится в двух-трех минутах езды, использование мощного автомобиля с автомагнитолой, GPS и кондиционером может оказаться излишним. В этом случае у вас есть возможность пройтись пешком, что является более простым и непосредственным подходом.
Аналогично, представим, что телефон — это класс, а разговор — функция. Если нужно что-то сказать человеку, находящемуся с вами в одной комнате, нет необходимости использовать мощный телефон с различными функциями. Непосредственный разговор с этим человеком будет более простым способом достижения цели.
Ключевой вывод заключается в том, что существуют различные способы выполнения задач, и иногда функциональный путь может быть более подходящим и эффективным. Рассматривая в первую очередь функциональный подход, мы можем сосредоточиться на простоте, удобочитаемости и выразительности кода. Важно выбрать правильный инструмент для работы и использовать соответствующие концепции при разработке программного обеспечения. Внедрение концепций функционального программирования в Kotlin может привести к созданию более чистого и удобного в обслуживании кода, что в конечном итоге повысит качество разработки.
Заключение
В контексте гибкой и функционально чистой архитектуры мы исследовали подход к реализации UseCase, который предлагает преимущества с точки зрения удобочитаемости, выразительности и модульности. Используя функции расширения высшего уровня или обычные функции высшего уровня, мы можем создавать UseCase, которые читаются как на естественном языке, делая логику мгновенно понятной и легко выполняемой.
Отдавая предпочтение функциям перед классами в UseCase, мы упрощаем поток управления и избегаем ненужного шаблонного кода, при этом кодовая база становится более лаконичной и целенаправленной. Функциональный подход соответствует принципам гибкой и функциональной чистой архитектуры, способствуя созданию более выразительной и адаптируемой кодовой базы.
Однако важно отметить, что выбор между использованием классов или функций для UseCase зависит от конкретных требований и сложности приложения. Для более обширных UseCase, которые включают управление состоянием и несколько экземпляров, классы все еще могут оказаться ценным выбором.
Читайте также:
- 10 вопросов, которые помогут нанять лучшего Android-разработчика
- Последовательное объединение адаптеров с помощью MergeAdapter
- Чистая архитектура с MVVM
Читайте нас в Telegram, VK и Дзен
Перевод статьи Stephen Siapno: Transforming UseCase: Embracing Fluent and Functional Approach (2023)