Создание собственной версии UseCase в 2023 году: гибкий и функциональный подход

Зачем создавать собственную версию 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 представляет собой процесс размещения клиентом заказа на товар в приложении электронной коммерции.

Требования:

  1. Клиент должен быть авторизован.
  2. Корзина не должна быть пуста, чтобы можно было оформить заказ.
  3. Сумма денег в кошельке клиента не должна быть меньше суммы заказанного товара.
  4. Если все три условия соблюдены, необходимо обновить запас продукции.
  5. После завершения оформления заказа корзина в приложении должна быть очищена.

Чтобы реализовать это все в рамках чистой архитектуры, вы, как правило, следуете структуре кода, подобной приведенной ниже.

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:

  1. Создайте класс с именем, состоящим из глагола в настоящем времени + существительное/(что) (опционально) + UseCase.
  2. Определите необходимые параметры, которые могут быть либо Repositories, либо другими UseCase, в зависимости от требований приложения.
  3. В Kotlin можно сделать экземпляры класса UseCase вызываемыми в качестве функций, определив функцию invoke() с модификатором operator.

Для получения дополнительной информации обратитесь к документации разработчика Android, где показано, как определяются доменный слой и UseCase, что похоже на подход, описанный выше.

В контексте гибкой и функционально чистой архитектуры цель состоит в том, чтобы достичь реализации, аналогичной описанной ниже.

OrderUseCase.kt

fun OrderRepository.placeOrder(order: Order) {
whenUserLoggedIn {
whenCartNotEmpty {
withSufficientFunds {
updateProductStock(order)
clearCart()
}
}
}
}

Основная цель состоит в том, чтобы предоставить четкое и сжатое описание необходимых шагов при оформлении заказа. В следующем фрагменте кода вы заметите, что он читается как естественный язык, поэтому UseCase сразу можно понять. Перед оформлением заказа необходимо выполнить несколько проверок.

Разбивка реализации:

  1. Вместо использования класса, создан файл с именем “OrderUseCase”.
  2. Имя файла соответствует условию “существительное/что (опционально) + UseCase”.
  3. Здесь не задействован класс. Вместо этого используется функция расширения высшего уровня или обычная функция высшего уровня.
  4. Сама функция представляет собой UseCase.
  5. Имя функции служит первичным UseCase, который вызывается из уровня представления.

В чем преимущества такого UseCase?

  1. Удобочитаемость и выразительность. Используется цепочка вызовов функций со значимыми именами (whenUserLoggedIn, whenCartNotEmpty, withSufficientFunds), поэтому код становится более читабельным и выразительным. Каждый шаг четко описан, что облегчает понимание процесса использования.
  2. Упрощенный поток управления. Цепочка вызовов функций следует естественному потоку условий, которые необходимо выполнить для размещения заказа. Таким образом, упрощается процесс управления, а логика становится более понятной.
  3. Модульность. Код разделяет задачи на различные функции (whenUserLoggedIn, whenCartNotEmpty, withSufficientFunds), заставляя каждую функцию обрабатывать определенную проверку или условие. Это способствует модульности и упрощает изменение или добавление новых проверок, не затрагивая весь UseCase.
  4. Лаконичность. Код позволяет избежать необходимости в отдельном классе UseCase с шаблонным кодом. Он сводит логику к лаконичной и целенаправленной функции расширения.
  5. Текучий интерфейс. Использование ключевого слова when в именах функций (whenUserLoggedIn, whenCartNotEmpty) придает коду стиль текучего интерфейса, улучшая читаемость кода и делая его более похожим на естественный язык.
  6. Ясность намерений. Разбивка логики 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, которые включают управление состоянием и несколько экземпляров, классы все еще могут оказаться ценным выбором.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Stephen Siapno: Transforming UseCase: Embracing Fluent and Functional Approach (2023)

Предыдущая статьяКак автоматизировать создание контента для YouTube и блога с помощью LangChain и OpenAI
Следующая статьяЧто возвращать в Go: структуры или интерфейсы?