Injecting Results
Up until now operations have been discussed as being isolated units of work. However, in practice this is rarely possible. More than likely if an operation has a dependency, the result of that operation is needed in the next one.
In software engineering this is known as Dependency Injection but we wish to avoid this term to avoid confusion with operational dependencies.
Synchronous & literal requirements
If the requirement for an operation is available synchronously, or perhaps is a literal value, then it can be injected into the initializer of the operation. This is following best practices for object orientated programming.
Asynchronous Requirements
However, if the requirements needed for an operations are not available when the instance is created, it must be injected sometime later, but before the operation executes.
Consider a DataProcessing
operation class. It's job is to process NSData
, which we refer to as the requirement. However, the data in question must be retrieved from storage, or the network, by a DataRetrieval
operation. In this context, the data is the result.
class DataRetrieval: Operation, ResultOperationType {
private(set) var result: NSData?
override func execute() {
fetchDataFromNetwork { data in
result = data
finish()
}
}
}
Above is an example DataRetrieval
class. It conforms to ResultOperationType
which defines the result
property. This is a generic property with no constraints. When the operation executes, before finishing it sets the result.
class DataProcessing: Operation, AutomaticInjectionOperationType, ResultOperationType {
var requirement: NSData?
private(set) var result: NSData?
override func execute() {
processData(requirement) { processed in
result = processed
finish()
}
}
}
The above example shows a DataProcessing
class. Here it conforms to AutomaticInjectionOperationType
in addition to ResultOperationType
. AutomaticInjectionOperationType
defines the requirement
property, which is also a generic property with no constraints.
The next step is to chain the operations together:
let retrieval = DataRetrieval()
let processing = DataProcessing()
processing.injectResultFromDependency(retrieval)
queue.addOperations(retrieval, processing)
What does this do?
- The retrieval operation is automatically added as a dependency of the processing operation.
- We add an observer to the retrieval operation to automatically set its result as the requirement of the processing operation.
Type constraint between Result and Requirement
There is a constraint on
injectResultFromDependency
where theResultOperationType.Result
type must match theAutomaticInjectionOperationType.Requirement
.
If the data retrieval operation finishes with errors, the processing operation will cancel with an AutomaticInjectionError
which contains the errors from the dependency.
Create transformation queues
By constructing
Operation
classes with conformance to bothResultOperationType
andAutomaticInjectionOperationType
, data can be transformed through a queue of chained operations.
Use of optionals
The ResultOperationType
only defines result
to get a generic typealias. Therefore the type of result
can be NSData?
or NSData
. However, bear in mind that standard Swift rules apply here. If the property is not an optional, it must be set during initialization. This can be useful if there are default values which would be suitable. Clearly it must be a var
if the operation will set it during the execute
function.
The same considerations apply for the requirement
property, except that the type of both properties must be the same. We recommend usage of optionals for these types, because even if a default value can be set on a property, which would allow for non-optional types, it might couple the two operations together as both would need the default value set.
Manual Result Injection
In some cases, representing results and requirements as single properties is not possible. In some cases an operation might have multiple requirements, or produce multiple results. For these cases, the framework provides a less automatic injection approach.
let retrieval = DataRetrieval()
let processing = DataProcessing()
processing.injectResultFromDependency(retrieval) { op, dep, errors in
// Here we have access to both operations, plus any errors
// from the dependency.
}
Updated less than a minute ago