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?

  1. The retrieval operation is automatically added as a dependency of the processing operation.
  2. 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 the ResultOperationType.Result type must match the AutomaticInjectionOperationType.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 both ResultOperationType and AutomaticInjectionOperationType, 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.
}