Groups
Encapsulate operations into a group
Operations makes it easy to decompose significant work into smaller chunks of work which can be combined together. This is always a good architectural practice, as it will make each component have reduced impact and increased testability. However, it can be unwieldy and diminish code re-use opportunities.
Therefore we can create more abstract notions of work with GroupOperation
. Earlier in the guide we showed how to use the class directly; whereas here we will focus on subclassing GroupOperation
.
The initializer
If all the child operations are known at compile time they can be configured during initialization. Configuration would be things such as setting dependencies, observers, conditions.
class LoginOperation: GroupOperation {
class PersistLoginInfo: Operation { /* etc */ }
class LoginSessionTask: NSURLSessionTask { /* etc */ }
init(credentials: Credentials /* etc, inject known dependencies */) {
// Create the child operations
let persist = PersistLoginInfo(credentials)
let login = URLSessionTaskOperation(task: LoginSessionTask(credentials))
// Do any configuration or setup
persist.addDependency(login)
// Call the super initializer with the operations
super.init(operations: [persist, login])
// Configure any properties, such as name.
name = "Login"
// Add observers, conditions etc to the group
addObserver(NetworkObserver())
addCondition(MutualExclusive<LoginOperation>())
}
}
The initialization strategy shown above is relatively simple, but shows some good practices. Creating and configuring child operations before calling the GroupOperation
initializer reduces the complexity and increases the testability and readability of the class. Adding observers and conditions to the group inside its initializer sets the default and expected behavior which makes using the class easier. Remember that these can always be nullified by using ComposedOperation
.
Adding child operations later
In some cases, the results of one operation are needed by a subsequent operation. We covered techniques to achieve this in Injecting Results which still apply here, but don't cover a critical scenario which is branching. A common usage might be to perform either operation Bar
or Baz
depending on Foo
, which cannot be setup during initialization.
class FooBarBazOperation: GroupOperation {
/* etc */
override func operationDidFinish(operation: NSOperation, withErrors errors: [ErrorType]) {
if errors.isEmpty && !cancelled, let foo = operation as? FooOperation {
if foo.doBaz {
addOperation(BazOperation())
}
else {
addOperation(BarOperation())
}
}
}
}
The above function does nothing in GroupOperation
and exists purely to allow subclasses to perform actions when each the child operation finishes. Therefore a standard operation should
- Inspect and handle any errors
- Test the received operation to check that it is the expected instance. For example, optionally cast it to the expected type, and possible check if it equal to a stored property of the group.
- Call
addOperation
oraddOperations
to add more operations to the queue.
Using this technique the group will keep executing and only finish until all children, including ones added after the group started, have finished.
Cancelling
GroupOperation
itself will already handle being cancelled correctly. However, in some cases, such as if an NSOperation
is injected into the group at initialization, it is necessary to cancel the group if one of the children is cancelled. We can do this via a CancelledObserver
class LoginSessionTask: NSURLSessionTask { /* etc */ }
class LoginOperation: GroupOperation {
class PersistLoginInfo: Operation { /* etc */ }
let task: URLSessionTaskOperation
init(credentials: Credentials, task: URLSessionTaskOperation) {
self.task = task
// Create the child operations
let persist = PersistLoginInfo(credentials)
// Do any configuration or setup
persist.addDependency(task)
// Call the super initializer with the operations
super.init(operations: [persist, login])
// Configure any properties, such as name.
name = "Login"
// Add observers, conditions etc to the group
addObserver(NetworkObserver())
addCondition(MutualExclusive<LoginOperation>())
}
override func execute() {
task.addObserver(CancelledObserver { [weak self] _ in
self?.cancel()
})
super.execute()
}
}
The above example is a modified from the original LoginOperation
example, in that the URLSessionTask
is injected to the group. This might be preferably to decouple the networking task, possibly to increase testability.
In this situation, it could be possible for the injected operation to be cancelled from outside the group, but logically that should cause the entire group to be cancelled. Therefore the group must observe this event. Although this could be configured after super.init
is called, sometimes it is necessary to perform such configuration (which references self
) after the initializer has finished.
For these situations, override execute
but always call super.execute()
. This is because the GroupOperation
has critical functionality in its execute
implementation (such as starting the queue).
Updated less than a minute ago