When A depends on B (A → B), a change in B may force A to change as well. Dependencies are both good and bad: they let objects work together, but they make software harder to change. We can use lambda functions to reduce dependencies between objects.
An Example
Suppose we have a Persistence object with a number of methods:
It doesn’t matter if this is a protocol (interface) or a class (although a protocol can also help reduce dependencies).
Somewhere in the middle of a bunch of work, we call persistence.save()
Work.doit(persistence: Persistence) {
...
persistence.save(entity)
...
}
Work depends on Persistence. That is, if Persistence changes, we’ll have to consider how those changes affect Work.
Testing
When we’re testing Work, we’d rather not write to a real database – it requires setup (outside our program) and it’s slow to test.
We might decide to create some sort of test double (e.g., a mock object).
The setup is like this:
MyTest sets up the results and exceptions for MockPersistence. It passes the mock to Work instead of passing the production Persistence.
If Persistence changes, our mock must change, and our test may change too.
Lambda to Reduce Dependencies
Now suppose that the Work.doit() doesn’t take an instance of Persistence, but a function instead:
Work.doit(saveFunction: (Entity) → Void) {
…
saveFunction(entity)
…
}
Before, our method depended directly on both Persistence and Entity. Now, it only depends on Entity.
Refactoring Mechanics
This is a way to remove the dependency on Persistence from the method doit()
:
1. For each unique method that is called on Persistence:
Extract To Variable on the call (not including the parameters), yielding a function reference.
persistence.save(entity)
=>
let saveFunction = persistence.save
saveFunction(entity)
2. Repeat for each function reference
a. If your tool supports it:
a.1. Introduce Parameter for the saveFunction
a.2. Delete Parameter on the now-unreferenced Persistence.
-or-
b. Otherwise:
b.1. Move the variable declaration to be the first line of the function – not a problem since it’s independent of the existing code.
b.2. Extract Method on the method for everything past the new first line, naming it the same as the original doit()
. Make sure the new method has the same visibility as the original doit()
.
b.3. Inline the function variable in the original method.
b.4. Inline (and delete) the original doit()
– this has the effect of replacing each call site passing in the saveFunction.
This lets us test differently. Rather than setting up a test double for the Database, the test can pass a lambda. That can capture the entity it’s called with so the test can examine it.
Code Example
Here’s Entity:
class Entity: Identifiable {
let id = UUID()
var contents: String
init(_ contents: String) {
self.contents = contents
}
}
and Persistence:
class Persistence {
func save(_ entity: Entity) { /*…*/ }
func load(_ id: UUID) -> Entity { /*…*/ }
func searchByName(_ searchText: String) -> [Entity] { /*…*/ }
func searchByTag(_ tag: String) -> [Entity] { /*…*/ }
// …
}
Class Work:
class Work {
func doit(_ db: Persistence) {
let newEntity = Entity("new entity")
db.save(newEntity)
}
}
Now, refactor
Extract To Variable =>
func doit(_ persistence: Persistence) {
let newEntity = Entity("new entity")
let saveFunction: (Entity) -> () = persistence.save
saveFunction(newEntity)
}
Move the variable definition up:
func doit(_ persistence: Persistence) {
let saveFunction: (Entity) -> () = persistence.save
let newEntity = Entity("new entity")
saveFunction(newEntity)
}
Extract Method on everything past the first line and adjust its visibility:
func doit(_ saveFunction: (Entity) -> ()) {
let newEntity = Entity("new entity")
saveFunction(newEntity)
}
func doit(_ persistence: Persistence) {
let saveFunction: (Entity) -> () = persistence.save
doit(saveFunction)
}
Inline the function variable in the original call:
func doit(_ persistence: Persistence) {
doit(persistence.save)
}
Inline the original call, leaving just our new method:
func doit(_ saveFunction: (Entity) -> ()) {
let newEntity = Entity("new entity")
saveFunction(newEntity)
}
Its callers used to look like this:
doit(persistence)
And now look like this:
doit(persistence.save)
Here’s a sample test, passing in a lambda that captures the entity and checks it:
func test_doit() throws {
var savedEntity: Entity?
let work = Work()
work.doit({ savedEntity = $0 })
XCTAssertEqual(savedEntity!.contents, "new entity")
}
Notice that our test has no need to use Persistence at all.
Conclusion
When one class calls another, the original class depends on both the called class and the classes of its arguments.
By passing in a function, instead of the instance, we no longer depend on the second class, just on the classes of the parameters.
We looked at a persistence example, but it can help with any case where we’re trying to reduce dependencies.