Replace RxSwift View Model Types with Free Functions
Instead of writing view model types that expose the input and/or output ports as properties, consider writing free functions that are the transformation.
This reminds me of Ken Scambler’s proposal to get rid of test doubles through side-effect free function composition. One of his examples, simplified, goes like this: Instead of testing what customer.pay(amount, wallet)
does in your tests by mocking the wallet
object, you rewrite Wallet
as a value object and change pay
to return the effect.
Before:
class Wallet {
private var cash: Int
func takeOut(amount: Int) {
self.cash -= amount
}
}
extension Customer {
func pay(amount: Int, wallet: Wallet) {
// Need to mock `wallet` reference type in your tests
wallet.takeOut(amount: amount)
}
}
After:
struct Wallet {
var cash: Int
}
func pay(amount: Int, wallet: Wallet) -> Wallet {
var result = wallet
result.cash -= amount
return result
}
This way, the original Wallet
isn’t affected and you can test the payment action in isolation. Ken’s examples are more sophisticated than that, but it should get the idea across.
Now DJ Mitchell proposes a similar approach to RxSwift view models: make them mere transformations of one set of observables into another, e.g. of this form: (Observable<T1>, Observable<T2>, ...) -> (Observable<U1>, Observable<U2> ...)
Usually, in your view code, you end up with PublishSubject
/PublishRelay
as to generate output events that you expose as Observable
. This dance is a well-established pattern to bridge between the non-reactive and reactive world. It’s useful to expose an OOP message input (e.g. accept changeUserName(_:)
messages) and expose a reactive observable sequence output (e.g. userName: Observable<String>
).
But when you write reactive code, you can end up with input events as observable event sequences anyway. There’s no need to use non-reactive messages in some cases.
This is where direct transformations kick in.
Look at the example by DJ:
func loginViewModel(
usernameChanged: Observable<String>,
passwordChanged: Observable<String>,
loginTapped: Observable<Void>
) -> (
loginButtonEnabled: Observable<Bool>,
showSuccessMessage: Observable<String>
) {
let loginButtonEnabled = Observable.combineLatest(
usernameChanged,
passwordChanged
) { username, password in
!username.isEmpty && !password.isEmpty
}
let showSuccessMessage = loginTapped
.map { "Login Successful!" }
return (
loginButtonEnabled,
showSuccessMessage
)
}
You have 3 input streams and return a tuple of 2 output streams. That pretty easy to test thorougly thanks to RxTest and its timed event recording. And it reduces the actual amount of ugly RxSwift binding code.
You can get from a lot of input/output bindings like this:
disposeBag.insert(
// Inputs
loginButton.rx.tap.asObservable()
.subscribe(onNext: viewModel.inputs.loginTapped),
usernameTextField.rx.text.orEmpty
.subscribe(onNext: viewModel.inputs.usernameChanged),
passwordTextField.rx.text.orEmpty
.subscribe(onNext: viewModel.inputs.passwordChanged),
// Outputs
viewModel.outputs.loginButtonEnabled
.bind(to: loginButton.rx.isEnabled),
viewModel.outputs.showSuccessMessage
.subscribe(onNext: { message in
print(message)
// Show some alert here
})
)
To a transformation call and a lot less bindings here:
let (
loginButtonEnabled,
showSuccessMessage
) = loginViewModel(
usernameChanged: usernameTextField.rx.text.orEmpty.asObservable(),
passwordChanged: passwordTextField.rx.text.orEmpty.asObservable(),
loginTapped: loginButton.rx.tap.asObservable()
)
disposeBag.insert(
loginButtonEnabled
.bind(to: loginButton.rx.isEnabled),
showSuccessMessage
.subscribe(onNext: { message in
print(message)
// Show some alert here
})
)
I’m constantly refactoring my RxSwift-based code as I learn new things. I found view model types to be pretty useless in the long run. Writing tests for them is cumbersome because they are the transformations and expose outputs as properties, accepting inputs in the initializer. I end up with lots of object setup boilerplate. They don’t need to be actual objects since they expose to actual behavior. They are already mere value transformations, only wrapped in a type.
I guess this is a natural step when you start with OOP in mind and think about sensible encapsulations in types. But sometimes, like in DJ’s example, free functions work just as well to express your value transformation intent.
Update 2019-03-08: I continued this line of thought and refactoring in Replacing More Reference Objects with Value Types.