October 18, 2017 Why Are Operations Essential to Scalable Software? Every development team should create small, maintainable units of code. Leveraging operations helps to build quality code that scales with business needs. Gabriela Zagarova Quality code is an essential element of successful software. Code that is written poorly not only makes for bad software, but it also makes a development team’s job harder to do. Complex lines of code diminish API functionality, slow down overall performance, and reduce scalability of the application. How Operations Improve Software Quality Every development team should strive to create small, maintainable units of code. Leveraging operations helps them achieve this goal. Product owners and development teams alike will realize the following benefits when operations are utilized: Modularity of the code: easy to remove or add new functionalities as needed Documentation: well-structured code enables easier documentation and better collaboration with new team members Multithreading: operations support better user experience Maintainability of the code: operations organize code in a way that is easier to update and debug Testability: operations simplify the writing of unit tests, which help developers find and resolve issues within their work, decreasing the burden on QAs later on. It is also easier to investigate specific issues that arise. Why It’s Not Too Late to Improve Software Quality With Operations Whether developers are beginning to write a new application or want to refactor an existing one, operations can be added to improve software quality at any point. Regardless of project size, development teams can leverage operations to improve software maintainability, scalability, testability, and the opportunity to write clear code. Operations Organize One — Or Many — Features and Improve Software Quality Operations can be conceptualized like a person’s daily tasks, like cleaning, shopping, cooking, eating, and reading. For some tasks, like cleaning or reading, it doesn’t matter which comes first. Some are best done at the same time — like cooking and listening to music. But for others, order matters. A person needs to buy the proper ingredients before they can cook the meal. These same concepts apply to the operations developers use when building code. An operation is a small unit of code that encapsulates logic that usually generates asynchronous tasks. Coding asynchronous tasks are essential to high-performing applications. For example, as a user to reads an article in an application, the application will simultaneously fetch other suggested articles to read afterward. In iOS, the operation logic is represented by Operation class. It is a high level wrapper for dispatch_block_t. This gives developers access to documentation of the lifecycle events of an operation. When developers have access to lifecycle events, they are able to cancel, start, and suspend operations. They can also gather information about what happens during an operation, arming them with better data to improve software quality. Operation is an abstract class and should be inherited. An operation may be in one of the following states: Pending: the operation is not ready to be executed Ready: this status indicates that operation is ready to be executed Executing: operation carries out its task Finished: the operation successfully executed its task Cancelled: the operation does not actively stop the receiver’s code from executing, but when the operation is cancelled property returns true. One cannot cancel execution once code processing has finished. Beware that when a developer changes properties like “Cancelled” or “Ready,” only their boolean values are changed. While the property may be set to true or false, no action has actually taken place. The developer must take care to specify and write the action after changing the boolean value. Operation states How Queues Organize Tasks Executed by Operations Operations can be executed from a queue. The main class for an operation queue in the iOS framework is OperationQueue, which is a high level wrapper of dispatch_queue_t. An operation executed from a queue creates its own thread. Otherwise, an operation can be executed on any thread. A developer cannot directly remove an operation from a queue after it has been added. An operation remains in its queue until it reports that it is finished with its task. OperationQueue has a property for adjusting the number of concurrency objects, which is known as maxConcurrentOperationCount. If set to one, it runs only one operation per time, If default settings are chosen, as many operations are run as the system allows. How a Single Operation is Executed A single concurrent operation The diagram above shows the process initiated when maxConcurrentOperationCount is set to one. Even if two or more operations become ready in the same time, only one will be started. In this case the queue follows a First-In-First-Out methodology. The operation that enters the queue first will be executed before all that come after it. How Multiple Operations Are Executed Multiple concurrent operations When max concurrency operations allow more than one task to be executed at a time, those tasks are executed in parallel. The diagram above illustrates how when three operations become ready at the same time, they all start simultaneously. Running Dependent Operations in iOS Applications What happens if an operation can be executed only after another operation has finished? This is where dependency in operations comes into play. Imagine the following tasks: Login to user profile Access user profile The accessing profile task cannot be executed if the user has not yet logged into the profile. The case is illustrated on the diagram below. Dependent operations An operation provides the dependency functionality via the addDependency() method. Dependencies are not limited by the operation queues in which they are written. But developers should avoid writing two operations that depend on each other. Instead the dependence should go one way only. Either operation 1 depends on operation 2, or vice versa. How Do Operations Improve the MVC Design Pattern? Design patterns make the programmer’s life easier and improve software quality. One of the most popular design patterns for developing web and mobile applications is Model-View-Controller (MVC). It contains three groups of objects: Model – represent the data layer of the application, View – is responsible for the UI components Controller – controls the interactions between the model and the view. Apple recommends using MVC for developing iOS applications. While MVC has been around for a long time, it has been updated very infrequently. Many people love the pattern MVC provides, but just as many find it ineffective. Following MVC can be problematic, resulting in a Massive View Controller. If this happens, It can be more difficult to write code in the ViewController properly. Massive View controllers are hard to maintain and slow down development velocity and the project adoption. We think that operations can be used to prevent the Massive View Controller, extend the MVC, and improve software quality. How Agile Distributed Teams Build Better Software Why businesses need remote Agile teams & questions to ask before starting. Implementing Operations to Improve Software Quality in iOS In the following example, we demonstrate how operations can be integrated in a project that supports users as they log into their GitHub account, view their profile, and explore repositories. Our example is based on the popular programing language, Swift, which Apple developed. Objective-C is the other language used for developing iOS applications. It also supports Operations, and while it uses a different syntax, the same rules and techniques for writing operations are valid. Operations in action in iOS architecture In iOS a controller is called ViewController. Each view controller manages a part of the user interface and interactions between this interface and underlying data. The diagram above shows connection between view controller and operations. Each view controller knows what operations to create for the page it manages. After the operations are created, the view controller sends a message to the OperationManager to add them to the queue and execute the tasks. We create our own subclasses of Operation and OperationQueue. We’ve called them base classes. The name refers to artefacts that contain generic, shared code between different objects — BaseOperation, BaseOperationQueue, and BaseGroupOperation. OperationManager We used this class, OperationManager, to build the operation queues. It starts, cancels, and tracks execution of the operations. Developers can go with one or more queues. We decided to use two types of queues: fileprivate var mainOperationQueue: BaseOperationQueue! or fileprivate var authenticationOperationQueue: BaseOperationQueue! Why is it functional to have a different operations queue to authorize user into the application? Suppose the authentication token expires. If you have a separate queue for renewing it, you can suspend the queue currently running and start it again after a new token has been acquired. The OperationManager provides an interface to execute one or more operations with or without dependencies: final func startOperation(queueType: OperationQueueType, operation: BaseOperation!, completion: (OperationCompletionBlock)?) Without: final func startOperationSequence(queueType: OperationQueueType, operations: [BaseOperation], withDependency: Bool, completion: (OperationCompletionBlock)?) -> [BaseOperation] The first method uses parameters like queue type, operation, and a completion block. The second method takes as parameters the queue type, collection of operations, whether they have any dependencies or not, and a completion block. Note that if the dependency parameter is true, the оperations will depend on each other in the same order they are added to the collection. In the startOperationSequence method, the block will be executed once all of the operations are done. This class has some other main functions – to manage all of the queues and if it’s necessary to cancel, pause, or restart particular operations: final func cancelOperation(operation: BaseOperation) final func cancelOperationQueue(queueType: OperationQueueType) final func pauseOperationQueue(queueType: OperationQueueType) final func restartOperationQueue(queueType: OperationQueueType) BaseOperationQueue OperationQueue is not an abstract class. Usually the developer does not need to subclass it. In our case we made a subclass for the sake of convenience. We also define an enum for different operation queue types that we are going to use. We give an example of two different operation queue types: main and authentication. enum OperationQueueType: Int { case main case authentication } BaseOperationQueue will keep track of errors and results returned by the operations in the queue. This is necessary in the case of starting a sequence of operations with one completion block. var aggregatedQueueResults: [String: AnyObject] = [:] var aggregatedQueueErrors: [NSError] = [] BaseOperation We created a BaseOperation subclass. It is designed as non-concurrent, but notice that оperations in the queue are always executed on a separate thread, regardless of whether they are designated as asynchronous or synchronous operations. The base class is responsible for the readiness of the operation. It holds the logic that dictates when an operation is ready to be executed. For example, the logic might dictate whether an internet connection is necessary for the operation to complete. override var isReady: Bool { get { return super.isReady && evaluateConditions() } } This base class also takes care of the correct finishing of a task. Even for asynchronous tasks, like a service request, are Finished and will be set properly. fileprivate var internalFinished: Bool = false override var isFinished: Bool { get { return internalFinished } set (newAnswer) { willChangeValue(forKey: "isFinished") internalFinished = newAnswer didChangeValue(forKey: "isFinished") if newAnswer { debugPrint("\(self.name ?? "Operation") operation is finished.") } } } Some of the operations can implement the NetworkRequestable protocol to describe a service operation. This protocol describes all required parameters for a service request to be created. public protocol NetworkRequestable { // MARK: Properties var requestURL: URL { get } var httpMethod: String { get } var allHTTPHeaderFields: [String: String]? { get } var bodyJSON: AnyObject? { get } } Another specific type of operations are database connected operations. For our purposes, we created DatabaseOperation as a subclass of BaseOperation. It holds the logic for common actions and conditions associated with database usage. BaseGroupOperation This is a different type of operation, which holds and executes logically related operations. BaseGroupOperation is special because this class has an internal queue and can execute multiple operations. In the init method of the group operation you can define the logic for handling the group of operations. This class is also a subclass of BaseOperation. Examples of Group Operations Login With a Group Operation Assuming that the developer uses CoreData, the following steps are typical to execute a login operation: Authentication with the given credentials. Fetching user info Parsing user info into database Saving database context Each of these steps leverage different operations within one larger task. Each time the user logs in, these operations will be executed in this order. Grouping operations such as this one is convenient to developers and can improve software quality by providing the end user with a smooth and reliable experience in the software. class LoginUserGroupOperation: BaseGroupOperation { // MARK: Initialization init(username: String, password: String) { // All of the operations should depend on the first one or the each one of them to the previous one let authorizationOperation = AuthorizationOperation(username: username, password: password) let fetchUserInfoOperation = FetchUserInfoOperation() let parseUserInfoOperation = ParseUserInfoOperation() let saveContextOperation = SaveContextOperation() // Define dependencies fetchUserInfoOperation.addDependency(authorizationOperation) parseUserInfoOperation.addDependency(fetchUserInfoOperation) saveContextOperation.addDependency(parseUserInfoOperation) let listOfOperations = [authorizationOperation, fetchUserInfoOperation, parseUserInfoOperation, saveContextOperation] super.init(operations: listOfOperations) } } Dependent Operations to Access the User Repos List The following operations reveal user repositories through the view controller: let fetchUserReposOperation = FetchUserReposOperation() let parseUserReposOperation = ParseUserReposOperation() let saveContextOperation = SaveContextOperation() operationsManager?.startOperationSequence(operations: [fetchUserReposOperation, parseUserReposOperation, saveContextOperation], withDependency: true, completion: { (success, result, errors) in if success { self.loadRepos() } }) Different operations can have different responsibilities, from sending server request to parsing data or saving the database context. While the logic for each piece of work is in a separate operation, they have relations between them that are critical to overall software functionality. How Do Operations Improve Software Quality Improve user experience. If developers want to guarantee that the user only encounters one alert in an application page at a time, they can leverage a dependency mechanism so that any additional alert operations that show up do so only after the one prior has been resolved by the user. Streamline documentation. Operations relieves the burden on developers to document their steps when writing and refining their code. If every operation call has only one endpoint, the developer can document all service calls by listing the operations. Define priorities. Operations help software developers to define priorities, which improve user experience. Functionalities for the users such as sending a text message will have higher priority than downloading or uploading. Cancel operations easily. If the currently executing operation finishes with a connectivity error, the developer can stop all following tasks from execution. You Built an App. Now What? Plan for maintenance with these 13 tactics. Operations effectively break code into maintainable pieces that are more easily refined, tested, and built upon over time. Operations allow you to abstract the underlying implementation. For instance, if there is an operation that should parse a server’s response and save it into a database, you can effortlessly change frameworks for connection with a different database. When beginning work on new software products, developers can easily write operations to support the writing of quality code and improve QA testing outcomes later on. Adding operations to legacy technology is more time consuming, but product owners and technical teams alike will improve software quality and build more effective experiences for end users. Image Source: Unsplash, Samuel Zeller Tags MobileDevelopment Share Share on Facebook Share on LinkedIn DevOps Strategy We break DevOps into five main areas: Automation, Cloud-Native, Culture, Security, and Observability. Download Share Share on Facebook Share on LinkedIn Sign up for our monthly newsletter. Sign up for our monthly newsletter.