Leveraging Composable Architecture at Scale by Krzysztof Zabłocki. Swift Heroes 2023 Talk
Related: Krzysztof Zablocki, iOS App Development, Software Engineering
Testability
TCA has been pushing Exhaustive testing from the very beginning. Exhaustivity makes a lot of sense in the development of isolated libraries.
However, that's not always the case. It often leads to test duplication and their fragility which makes them hard to maintain.
It's also overwhelming. Devs tend to copy broken tests' "expectations" without figuring out what really happened.
What we want is to have a test that is:
- Small and easy to understand
- Verifies only one piece of functionality at a time
- Doesn't break due to internal implementation changes
- Verifies only public interface/behavior
This allows devs to be confident when changing implementation details.
We would prefer to have 10 tests with a single assert each other than 1 test with 10 asserts. This way we can easily reason about what exactly got broken.
Maintainability of Codebase
Boundaries. Actions implemented as enum does not provide enough isolation of details of features implementation.
We can categorize them and cover them with a protocol:
protocol TCAFeatureAction {
associatedtype ViewAction
associatedtype DelegateAction
associatedtype LocalAction
static func view(_: ViewAction) -> Self
static func delegate(_: DelegateAction) -> Self
static func local(_: LocalAction) -> Self
}
Then implement actions like this:
public enum MyFeatureAction: TCAFeatureAction {
enum ViewAction: Equatable {
case disappear
case dismissed
}
enum LocalAction: Equatable {
case listResult(Result<[Todo], TodoError>)
}
enum DelegateAction: Equatable {
case userLoggedin(User)
}
case view (ViewAction)
case local(LocalAction)
case delegate(DelegateAction)
}
This categorization allows us to write our own linter rules to ensure certain actions are not used where they shouldn't.
Performance
If the app has too many actions sent to the system TCA does not perform well.
The whole set of optimizations that comes to mind:
- Stores updates deduplication
- Multiples stores instead of a single one (also good from an arhitecture decomposition standpoint)
- Throttle/Debounce on actions producing and state consumption levels
What didn't work out:
- Scoping Stores deduplication with explicitly defined paths of the state. It's kinda wastes the whole UDF idea
- Caching/memoization turned out to be too much fragile. Cache missing, etc.
Tooling
The biggest promise of UDF/MVI architectures like TCA is to make it clear what's happening in the application. In large applications built with TCA, it doesn't happen by itself. But the tooling may help.
One of the big benefits of TCA as a consistent architecture is the ability to build tooling around it.
- Performance dashboard with all actions and state processing measurements
- State tree visualization
- Dashboard with actions logging and filtering
- Object lifetime tracker to spot memory leaks
References
Krzysztof Zabłocki - Leveraging Composable Architecture at Scale | Swift Heroes 2023 Talk - YouTube
Comments