X-Oriented Programming, Using Functions or Classes?
Working on a greenfield project is nice, but it also brings up all the uncertainties of the start: which components, modules, objects, interfaces do you need?
With Swift, you even have to decide if you want to design the system in an object-oriented or a functional way. As far as the core functionality is concerned, you do have this choice. (When it comes to UIKit/AppKit, you don’t.)
Thinking in objects, my tool of choice is OOP. But then I end up with classes that have 1 initializer with 2 dependencies injected, and 1 method that calls the dependencies to carry out some functionality. The very same method could be a free function.
class Foo {
let bar: Bar
let baz: Baz
init(bar: Bar, baz: Baz) {
self.bar = bar
self.baz = baz
}
func perform() {
baz.compute(bar.currentFizz())
}
}
// or
func perform(bar: Bar, baz: Baz) {
baz.compute(bar.currentFizz())
}
If there are side-effects, both the class and the function will need a handler, e.g. a closure like completion: (SideEffect) -> Void
. Again, this works for both cases.
With the class-based approach, the dependencies are injected upon object initialization. If Foo
is a long-lived service object, that can be very early in the app’s lifetime. Afterwards, you only need to call the perform
method. If the completion handler is injected on call time, you get a nice separation of the parts that change and the parts that stay the same.
With functions, in a naive approach, you have to inject everything at the same time: at call time. So the knowledge trickles down from app launch to the actual call site.
But this isn’t really true. You can prepare partial function applications in much the same way as you would with constuctor dependency injection:
typealias SideEffectHandler = (SideEffect) -> Void
func perform(bar: Bar, baz: Baz, completion: SideEffectHandler) {
let result = baz.compute(bar.currentFizz())
completion(result)
}
let bar: Bar = ...
let baz: Baz = ...
let preparedPerform: (SideEffectHandler) -> Void = { completion in
perform(bar: bar, baz: baz, completion: completion)
}
preparedPerform() { /* side effect processing */ }
That’s also what you can use currying for, for example.
Which path do you choose?
I tend to stick to classes and protocols because of testing. I use mocks in my unit tests as replacements for dependencies and result handlers. It works because I can test every object in about the same way.
With a purely functional approach, you can test the functional units just as fine, but the function compositions not so much. They turn into integration tests of sorts, where you validate a couple of important paths, but not all possible combinations of the underlying parts, because that would duplicate the unit tests and be a combinatorial pain. Since integration tests are a scam when used to verify the system, it’s pointless to even try to get all their combinations right; you get far more value by testing units at their seams.
I prefer similar-looking object boundaries and similar-looking tests in most parts of my application instead of attempting to implement VIPER with pure functions.
This is not a recommendation. It stems from my cluelessness, really. How could I make this different, and better? I don’t know, yet, but I sense that this is a problem that I can eventually solve.
See also:
- “Integrated tests are a scam because you continuously have to throw more of them at the problems but will never see which part of your code causes trouble”, from “1 Criterion to Determine if You Should Write Unit or UI Automation Tests”.
- Mark Sands, Mocking is Tautological, which I don’t really agree with.