Reduce. Conquer. Repeat.
| 🖤 | Support this project |
|---|---|
bc1qs6qq0fkqqhp4whwq8u8zc5egprakvqxewr5pmx |
|
0x3147bEE3179Df0f6a0852044BFe3C59086072e12 |
|
TKznmR65yhPt5qmYCML4tNSWFeeUkgYSEV |
- About
- Changelog
- Overview
- Mathematical proof
- Comparison with popular patterns
- Clean Architecture
- Proof of concept
- More examples
This repository contains a proof of concept of the Reduce & Conquer pattern built into the Clean Architecture, using the example of a cross-platform Pokédex application built using the Compose Multiplatform UI Framework.
- Command processing strategies:
Immediate,Channel,Parallel - Enhanced events with lifecycle management
- Built-in metrics collection
- Feature factory for easy creation
- Initial Reduce & Conquer pattern
- Basic
Feature,Reducer,Transition - Pokédex example app
Reduce & Conquer is an architectural pattern leveraging functional programming principles and pure functions to create predictable and testable functional components.
classDiagram
class Feature~Command, State~ {
<<interface>>
+StateFlow~State~ state
+Flow~Event~ events
+((suspend () -> Unit))? invokeOnClose
+execute(command: Command)*
+collect(event, joinCancellation, action)*
+stopCollecting(key, joinCancellation)*
+stopCollectingAll(joinCancellation)*
+cancel()*
+cancelAndJoin()*
+close()*
}
class BaseFeature~Command, State~ {
-Reducer~Command, State~ reducer
-CommandProcessor~Command~ commandProcessor
-MutableStateFlow~State~ _state
-Channel~Event~ _events
-perform(command: Command)
-dispatchFailure(throwable: Throwable)
}
class Reducer~Command, State~ {
<<interface>>
+reduce(state: State, command: Command): Transition~State~*
+transition(state: State, vararg event: Event): Transition~State~
}
class Transition~State~ {
+State state
+List~Event~ events
+withEvents(block): Transition~State~
}
class CommandProcessor~Command~ {
<<interface>>
+Int activeOperations
+((suspend (Throwable) -> Unit))? onFailure
+process(action: CommandProcessorAction~Command~)*
+close()*
}
class Event {
<<interface>>
+Any? payload
+Instant timestamp
}
class Event_Collectable~T~ {
<<abstract>>
+Any key
+Flow~T~ flow
}
class MetricsFeature~Command, State~ {
-Feature~Command, State~ feature
-MetricsCollector~Command~ metricsCollector
+execute(command)*
}
class FeatureFactory {
+create(initialState, reducer, strategy, ...): Feature~Command, State~
}
class CommandProcessorAction~Command~ {
+Command command
+((suspend (Command) -> Unit)) block
}
class MetricsCollector~Command~ {
<<interface>>
+recordSuccess(command, duration)*
+recordFailure(command, duration, throwable)*
}
Feature <|.. BaseFeature
BaseFeature --> Reducer
BaseFeature --> CommandProcessor
BaseFeature --> Event
Feature <|.. MetricsFeature
MetricsFeature --> Feature : delegates to
MetricsFeature --> MetricsCollector
FeatureFactory --> BaseFeature
FeatureFactory --> CommandProcessor
CommandProcessor --> CommandProcessorAction
Event <|-- Event_Collectable
Tip
The idempotent nature of deterministic state allows you to implement functionality such as rolling back the state to a previous version.
A class or object that describes the current state of the presentation.
A class or object that describes an action that entails updating state and/or raising events.
Note
It's not a side effect because reduce is a pure function that returns the same result for the same arguments.
A class or object that describes the "Fire and forget" event caused by the execution of a command and the reduction
of the presentation state.
May contain a payload.
Collectable- for reactive data streams with lifecycle managementTimeout- when command processing times outCancellation- when command processing is cancelledFailure- when command processing fails with an exception
An interface that takes two type parameters: Command and State.
A functional unit or aggregate of presentation logic within isolated functionality.
state: A read-only state flow that exposes the current state.events: A flow that exposes the events emitted by the feature.invokeOnClose: Optional cleanup callback.
-
suspend execute(command: Command): Suspending command submission with configurable processing strategy. -
suspend <T> collect(event: Event.Collectable<T>, joinCancellation: Boolean, action: suspend (T) -> Unit): Collects reactive data streams with automatic lifecycle management. -
suspend <T> stopCollecting(key: T, joinCancellation: Boolean): Stops collecting specific stream. -
suspend stopCollectingAll(joinCancellation: Boolean): Stops all active collections. -
suspend cancel(): Cancels current transition. -
suspend cancelAndJoin(): Cancels current transition and waits for completion. -
suspend close(): Terminates all operations and cleans up resources.
val feature = FeatureFactory().create(
initialState = MyState(),
reducer = MyReducer(),
strategy = CommandStrategy.Parallel(limit = 4),
debounceMillis = 300L,
timeoutMillis = 5_000L,
coroutineContext = Dispatchers.IO
)Version 2.0.0 introduces flexible command processing strategies.
Commands are processed immediately in a mutually exclusive manner.
-
Unlimited: Unlimited buffer for commands. -
Rendezvous: No buffer, sender waits for receiver. -
Conflated: Only the latest command is kept. -
Fixed: Fixed capacity buffer.
Process multiple commands in parallel with configurable concurrency limit.
-
Debouncing: Prevent rapid successive commands (useful for frequent inputs) -
Timeout: Set maximum processing time per command (prevents hanging operations) -
Coroutine Context: Custom execution context for command processing
A functional interface that takes two generic type parameters: Command and State.
A stateless component responsible for reducing the input command to a new state and generating events.
reduce(state: State, command: Command): Reduces theStatewith the givenCommandand returns aTransition.transition(state: State, vararg event: Event): Constructs aTransitionwith the givenStateand variadicEvent.
A data class that represents a state transition.
state: The newState.events: A list ofEvents emitted during the transition, which can be empty.
withEvents(block: (List<Event>) -> List<Event>): Transforms events using the provided block.
Enhanced event system with built-in error handling and lifecycle management.
// Collect flows in feature
events.collect { event ->
when (event) {
is UserEvent.ObserveUsers -> collect(
event = event, joinCancellation = false, action = { users ->
execute(UserCommand.UpdateUsers(users = users))
})
}
}
// Handle different event types in Composable
val event by feature.events.collectAsState(null)
LaunchedEffect(event) {
when (event) {
is PokedexEvent.ScrollToStart -> gridState.animateScrollToItem(0)
is PokedexEvent.ResetScroll -> gridState.scrollToItem(0)
else -> Unit
}
}Built-in metrics collection for monitoring feature performance:
class MyMetricsCollector : MetricsCollector<Command> {
override fun recordSuccess(command: Command, duration: Duration) {
// Log successful command execution
}
override fun recordFailure(command: Command, duration: Duration, throwable: Throwable) {
// Log failed command execution
}
}
val feature = MetricsFeature(
feature = baseFeature,
metricsCollector = MyMetricsCollector()
)Let
We define a function
The function
-
Associativity: For all
$s \in S$ ,$c_1, c_2 \in C$ , we have:$$R(R(s, c_1), c_2) = R(s, [c_1, c_2])$$ where$[c_1, c_2]$ denotes the composition of commands$c_1$ and$c_2$ . -
Commutativity (under specific conditions): For all
$s \in S$ ,$c_1, c_2 \in C$ such that$c_1 \circ c_2 = c_2 \circ c_1$ , we have:$$R(s, c_1) = R(s, c_2)$$
Let
-
Apply Command
$c_1$ :$$R(s, c_1) = (s_1, e_1)$$ where$s_1$ is the new state and$e_1$ is the event generated by applying$c_1$ to state$s$ . -
Apply Command
$c_2$ to the New State$s_1$ :$$R(s_1, c_2) = (s_2, e_2)$$ where$s_2$ is the new state after applying$c_2$ to$s_1$ and$e_2$ is the event generated. -
Sequential Application of Commands
$c_1$ and$c_2$ :$$R(s, c_1 \circ c_2) = (s_2, e_1 \cup e_2)$$ where$c_1 \circ c_2$ denotes applying$c_1$ first, resulting in$s_1$ and$e_1$ , and then applying$c_2$ to$s_1$ , resulting in$s_2$ and$e_2$ .
Since both
This shows that the reduction function satisfies associativity in the context of command composition.
For commutativity under specific conditions where commands are commutative:
Let
-
Apply Command
$c_1$ and then$c_2$ :$$R(s, c_1) = (s_1, e_1)$$ $$R(s_1, c_2) = (s_2, e_2)$$ where$s_2$ is the state resulting from applying$c_2$ to$s_1$ and$e_2$ is the event generated. -
Apply Command
$c_2$ and then$c_1$ :$$R(s, c_2) = (s_1', e_1')$$ $$R(s_1', c_1) = (s_2', e_2')$$ where$s_2'$ is the state resulting from applying$c_1$ to$s_1'$ and$e_2'$ is the event generated.
Since
Thus, we have:
This demonstrates the commutativity of the reduction function under the specific condition of commutative commands.
We have successfully proved that the reduction function
The associativity property ensures that the order in which commands are applied does not affect the final state and events, while the commutativity property ensures that commands can be applied in any order without affecting the result under specific conditions. These properties provide a solid foundation for ensuring the correctness and reliability of the system, influencing its design and maintenance.
The MVC pattern separates concerns into three parts: Model, View, and Controller.
The Model represents the data, the View represents the UI,
and the Controller handles user input and updates the Model.
In contrast, the Reduce & Conquer combines the Model and Controller into a single unit.
The MVP pattern is similar to MVC,
but it separates concerns into three parts: Model, View, andPresenter.
The Presenter acts as an intermediary between the Model and View, handling user input and updating
the Model.
The Reduce & Conquer is more lightweight than MVP, as it does not require a separate Presenter layer.
The MVVM pattern is similar to MVP,
but it uses a ViewModel as an intermediary between the Modeland View.
The ViewModel exposes data and commands to the View, which can then bind to them.
The Reduce & Conquer is more flexible than MVVM, as it does not require a separate ViewModel layer.
The MVI pattern is similar to MVVM,
but it uses an Intent as an intermediary between the Model andView.
The Intent represents user input and intent, which is then used to update the Model.
The Reduce & Conquer is more simple than MVI, as it does not require an Intent layer.
The Redux pattern uses a global store to manage application state.
Actions are dispatched to update the store, which then triggers updates to connected components.
The Reduce & Conquer uses a local state flow instead of a global store,
which makes it more scalable for large applications.
The TEA pattern uses a functional programming approach to manage application state.
The architecture consists of four parts: Model, Update, View, and Input.
The Model represents application state,
Update functions update the Model based on user input and commands,
Viewfunctions render the Model to the UI, and Input functions handle user input.
The Reduce & Conquer uses a similar approach to TEA, but with a focus on reactive programming and
coroutines.
The EDA pattern involves processing events as they occur.
In this pattern, components are decoupled from each other, and events are used to communicate between components.
The Reduce & Conquer uses events to communicate between components,
but it also provides a more structured approach to managing state transitions.
The Reactive Architecture pattern involves using reactive programming to manage complex systems.
In this pattern, components are designed to react to changes in their inputs.
The Reduce & Conquer uses reactive programming to manage state transitions and emit events.
Clean Architecture is a software design pattern that separates the application's business logic into layers, each with its own responsibilities.
The main idea is to create a clear separation of concerns, making it easier to maintain, test, and scale the system.
graph LR
subgraph "Presentation Layer"
View["View"] --> Feature["Feature"]
Feature["Feature"] --> Reducer["Reducer"]
end
subgraph "Domain Layer"
UseCase["Use Case"] --> Repository["Repository"]
UseCase["Use Case"] --> Entity["Entity"]
end
subgraph "Infrastructure Layer"
direction TB
Dao["DAO"] --> Database["Database"]
Service["Service"] --> FileSystem["File System"]
Service["Service"] --> NetworkClient["Network Client"]
end
Reducer --> UseCase
Repository --> Dao
Repository --> Service
Clean Architecture can be represented as follows:
View(
Feature(
Reducer(
UseCase(
Repository(
Service
)
)
)
)
)Tip
Organize your package structure by overall model or functionality rather than by purpose. This type of architecture is called "screaming".
Representing the business domain, such as users, products, or orders.
Defining the actions that can be performed on the entities, such as logging in, creating an order, or updating a user.
Handling communication between the application and external systems, such as databases, networks, or file systems.
Providing the necessary infrastructure for the application to run, such as web servers, databases, or operating systems.
Reduce & Conquer is a part of Frameworks and Drivers, as it is an architectural pattern that provides an
implementation of presentation.
Tip
Follow the Feature per View principle and achieve decomposition by dividing reducers into sub-reducers.
The Feature class contains methods that implement the flow mechanism, but you can also implement your own
using the principles described below.
Let's say there is a command that calls a use case, which returns a flow with data that needs to be stored in
the state.
As a container, flow is only useful as long as it is collected, which means it can be classified as a one-time
payload.
As should be done with this kind of data, flow must be processed using the appropriate mechanism - events,
which must begin to be collected before executing the command that returns the event containing flow.
Thus, we can set an arbitrary flow processing strategy, as well as manage the lifecycle of the collector using
coroutines, without going beyond the functional paradigm.
Here is an example implementation of flow collection:
data class User(val id: String)
interface UserRepository {
suspend fun observeUsers(): Result<Flow<List<User>>>
}
class ObserveUsers(private val userRepository: UserRepository) {
suspend fun execute() = userRepository.observeUsers()
}
data class UserState(
val users: List<User> = emptyList(),
)
sealed interface UserCommand {
data object ObserveUsers : UserCommand
data class UpdateUsers(val users: List<User>) : UserCommand
}
enum class UserEventKey {
OBSERVE_USERS
}
sealed interface UserEvent {
data class ObserveUsers(
override val flow: Flow<List<User>>
) : UserEvent, Event.Collectable<List<User>>() {
override val key = UserEventKey.OBSERVE_USERS
}
}
class UserReducer(
private val observeUsers: ObserveUsers,
) : Reducer<UserCommand, UserState> {
override suspend fun reduce(state: UserState, command: UserCommand) = when (command) {
is UserCommand.ObserveUsers -> observeUsers.execute().fold(
onSuccess = { flow: Flow<User> ->
transition(state, UserEvent.ObserveUsers(flow = flow))
},
onFailure = { throwable ->
transition(state, Event.Failure(throwable = throwable))
}
)
is UserCommand.UpdateUsers -> transition(state.copy(users = command.users))
}
}
class UserFeature(
private val feature: Feature<UserCommand, UserState>
) : Feature<UserCommand, UserState> by feature {
private val coroutineScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
init {
coroutineScope.launch {
events.collect { event ->
when (event) {
is UserEvent.ObserveUsers -> collect(
event = event, joinCancellation = false, action = { users ->
execute(UserCommand.UpdateUsers(users = users))
})
}
}
}
}
override val invokeOnClose: (suspend () -> Unit)? get() = { coroutineScope.cancel() }
}
single {
UserFeature(
feature = FeatureFactory().create(
initialState = UserState(),
reducer = UserReducer(observeUsers = get()),
strategy = CommandStrategy.Immediate
)
)
}The new collect method automatically manages the lifecycle of flow collections, eliminating the need for manual coroutine management.
It is assumed that all the important logic is contained in the Reducer, which means that the testing pipeline can be
roughly represented as follows:
val (actualState, actualEvents) = feature.execute(command)
assertEquals(expectedState, actualState)
assertEquals(expectedEvents, actualEvents)With version 2.0.0, you can also test different command processing strategies and error scenarios.
A cross-platform Pokédex application built using the Compose Multiplatform UI Framework.
graph TD
subgraph "Use Case"
GetMaxAttributeValue["Get Max Attribute Value"]
GetDailyPokemon["Get Daily Pokemon"]
GetPokemons["Get Pokemons"]
InitializeFilters["Initialize Filters"]
GetFilters["Get Filters"]
SelectFilter["Select Filter"]
UpdateFilter["Update Filter"]
ResetFilter["Reset Filter"]
ResetFilters["Reset Filters"]
CardsReducer["Cards Reducer"]
ChangeSort["Change Sort"]
end
subgraph "Navigation"
NavigationView["Navigation View"] --> NavigationFeature["Navigation Feature"]
NavigationFeature["Navigation Feature"] --> NavigationReducer["Navigation Reducer"]
end
NavigationReducer["Navigation Reducer"] --> NavigationCommand["Navigation Command"]
NavigationCommand["Navigation Command"] --> DailyView["Daily View"]
NavigationCommand["Navigation Command"] --> PokedexView["Pokedex View"]
subgraph "Daily"
DailyView["Daily View"] --> DailyFeature["Daily Feature"]
DailyFeature["Daily Feature"] --> DailyReducer["Daily Reducer"]
end
DailyReducer["Daily Reducer"] --> GetMaxAttributeValue["Get Max Attribute Value"]
DailyReducer["Daily Reducer"] --> GetDailyPokemon["Get Daily Pokemon"]
subgraph "Pokedex"
PokedexView["Pokedex View"] --> PokedexFeature["Pokedex Feature"]
PokedexFeature["Pokedex Feature"] --> PokedexReducer["Pokedex Reducer"]
PokedexReducer["Pokedex Reducer"] --> CardsReducer["Cards Reducer"]
PokedexReducer["Pokedex Reducer"] --> FilterReducer["Filter Reducer"]
PokedexReducer["Pokedex Reducer"] --> SortReducer["Sort Reducer"]
end
PokedexReducer["Pokedex Reducer"] --> CardsReducer["Cards Reducer"]
CardsReducer["Cards Reducer"] --> GetMaxAttributeValue["Get Max Attribute Value"]
CardsReducer["Cards Reducer"] --> GetPokemons["Get Pokemons"]
PokedexReducer["Pokedex Reducer"] --> FilterReducer["Filter Reducer"]
FilterReducer["Filter Reducer"] --> InitializeFilters["Initialize Filters"]
FilterReducer["Filter Reducer"] --> GetFilters["Get Filters"]
FilterReducer["Filter Reducer"] --> SelectFilter["Select Filter"]
FilterReducer["Filter Reducer"] --> UpdateFilter["Update Filter"]
FilterReducer["Filter Reducer"] --> ResetFilter["Reset Filter"]
FilterReducer["Filter Reducer"] --> ResetFilters["Reset Filters"]
PokedexReducer["Pokedex Reducer"] --> SortReducer["Sort Reducer"]
SortReducer["Sort Reducer"] --> CardsReducer["Cards Reducer"]
SortReducer["Sort Reducer"] --> ChangeSort["Change Sort"]
- Switching between Daily and Pokedex screens (functionality).
- Get a Pokemon of the Day card based on the current day's timestamp
- Getting a grid of Pokemon cards
- Search by name
- Multiple filtering by criteria
- Reset filtering
- Sorting by criteria
Note
The Pokemon card is a double-sided rotating card where
- front side contains name, image and type affiliation
- back side contains name and hexagonal skill graph
- Jetpack Compose Multiplatform
- Kotlin Coroutines
- Kotlin Flow
- Kotlin AtomicFU
- Kotlin Datetime
- Kotlin Serialization Json
- Koin Dependency Injection
- Kotlin Multiplatform UUID
- Kotlin Coroutines Test
- Mockk
- Haskcore - A modern, lightweight standalone desktop IDE with LSP support, built with Kotlin & Compose Desktop for Haskell development
- StarsNoMore - An application for getting a summary of statistics and traffic of a user's GitHub repositories
- Klarity - Jetpack Compose Desktop media player library demonstration (example) project
- camera-capture - Part of a project (mobile application) that provides the ability to take pictures with a smartphone camera and use them in the ComfyUI workflow
- compose-desktop-media-player - Examples of implementing a media (audio/video) player for Jetpack Compose Desktop using various libraries
