This page shows the source for this entry, with WebCore formatting language tags and attributes highlighted.


Why use an IOC? (hint: testing)


<h>Inversion of Control Pattern</h> The IOC pattern is the [I] in <a href="">SOLID</a>. It stands for "Inversion of Control". In order to make good use of this pattern, an application should adhere to the following rules: <ul> Prefers composition over inheritance, exposing clear dependencies Refers to dependencies via interface or protocol types with as small a surface area as possible Obtains dependencies through injection, preferably in the constructor </ul> Components built in this manner are agnostic in their implementation. They can be composed by an application as it sees fit. <h>Containers and Injection</h> Many projects will use an "IOC Container" that offers the following features for enforcing and benefiting from the pattern. <ul> The container allows an application to register the class or object to use for a given interface The application can also indicate whether the class or object should be considered a singleton The container can create objects automatically, as long as all parameters to the object's single constructor are of types registered with the container </ul> We will first look at how composition even without a container is very powerful. Then we'll look at how a container can improve on that. <h>Step One: A limited robot simulator</h> Let's take a look at an example of an application that looks OK at first, but turns out not to be very flexible. <n>Note: The example is small, so some of the steps will feel like over­engineering. It's a good point, but the principles shown here apply just as well for larger systems.</n> The following example defines a simulator that can move a robot along a route, defined by movements. The robot starts at a given location and can travel at a fixed speed. <code> enum Direction { case north case south case east case west } struct Movement { let direction: Direction let distance: Int } struct Point { var x: Int var y: Int } class FastRobot { var speed = 2 var location: Point = Point(x: 0, y: 0) let movements: [Movement] = [Movement(direction: .north, distance: 1)] func move() { for movement in movements { let distance = speed * movement.distance switch (movement.direction) { case .north: location.y += distance case .south: location.y -= distance case .east: location.x += distance case .west: location.x -= distance } } } } class Simulator { func run() { FastRobot().move() } } </code> As mentioned above, this implementation looks well­written, but what if we wanted to verify that the robot ended up at the right location? Let's try that below. <h>Step Two: Running the limited robot</h> <code> Simulator().run() // Now what? </code> It turns out that we can't test anything in this application. We can fix this by applying the patterns outlined in the first section. <h>Step Three: Decouple the robot from the simulator</h> First, let's tackle the <c>Simulator</c>'s interface: <code> class Simulator { func run(robot: FastRobot) { robot.move() } } let robot = FastRobot() Simulator().run(robot: robot) XCTAssertEqual(robot.location.x, 0) XCTAssertEqual(robot.location.y, 2) </code> Now we can test that the robot is working as expected. Still the robot is still quite hard­coded, as is the simulator's relationship to the robot. The robot must be a <c>FastRobot</c> and it can only move along a fixed route. <h>Step Four: Reduce the robot "surface"</h> We'll first decouple the <c>Simulator</c> from a direct dependence on the <c>FastRobot</c>. <code> protocol IRobot { func move() } class Robot : IRobot { // As above } class Simulator { func run(robot: IRobot) { robot.move() } } </code> Now the simulator only knows about the protocol <h>IRobot</h>, which has a very small surface area. It's still too small to be very useful. <h>Step Five: Make the robot configurable</h> Instead of hard­coding everything, we can compose the robot out of parts. Examining the algorithm, we see three parts that could be externalized: <ul> The robot's speed is currently fixed. We could make a component that is responsible for calculating the speed of the robot. The robot's route is also fixed. We could make a component to represent the route as well. Finally, the robot's initial position is also fixed. We could make that configurable as well. </ul> Let's first externalize all of the hard­coded values out of the <c>FastRobot</c> into a generic <c>Robot</c> class. <code> class Robot : IRobot { let speed: Int var location: Point let movements: [Movement] init(speed: Int, location: Point, movements: [Movement]) { self.speed = speed self.location = location self.movements = movements } func move() { for movement in movements { let distance = speed * movement.distance switch (movement.direction) { case .north: location.y += distance case .south: location.y -= distance case .east: location.x += distance case .west: location.x -= distance } } } } </code> Now we can create a Robot, injecting all of the initial conditions. <code> let origin = Point(x: 0, y: 0) let route = [Movement(direction: .north, distance: 1)] let robot = Robot(speed: 2, location: origin, movements: route) Simulator().run(robot: robot) XCTAssertEqual(robot.location.x, 0) XCTAssertEqual(robot.location.y, 2) </code> The same assertions hold as before, but the <c>Robot</c> class is much more generalized. We can now see how we could test the robot's movement algorithm with various combinations of origin, speed and route. At this point, we've made the robot and simulator composable and testable. Now we want to have a look at how we can separate the configuration from the usage. <h>Using a container to build objects</h> We're not nearly done, though. What does this all have to do with a service provider? That's where the inversion part comes in. In the very first example, the <c>Simulator</c> was responsible for creating the <c>robot</c>. This made it impossible to test whether the robot did what it was supposed to do. So we passed the robot in as a parameter to <c>run()</c>, making the caller responsible for creating the <c>robot</c> instead of the <c>Simulator</c>. This is fine, as long as the caller is the top­level part of the program, responsible for composing the objects that will be used. However, what if the direct caller doesn't know how to do that? Or, put another way, what if the caller <i>should not</i> be doing that? What if the caller is a button handler in a UI? Would we want the button handler---or the UI that contains it---to be responsible for constructing the robot or its initial conditions? This is where the container comes in: we want to register all of the objects and classes we want to use at the beginning of the application. This configuration can be retrieved at any later point without knowing any more than the interface that's required. This takes us full circle to the original code, except instead of creating the <c>Simulator</c> directly, we want to get it from a container, called a <c>provider</c> in the following examples. <code> let simulator = provider.resolve(ISimulator.self) let robot = provider.resolve(IRobot.self) XCTAssertEqual(robot.location.x, 0) XCTAssertEqual(robot.location.y, 2) </code> <n>Note: For reasons of simplicity, we assume that all objects in the container are singletons.</n> <h>Step Six: Configure the container</h> Let's take the configurable code above and translate it to a container. Here the registrar is the configurable part and the provider is the part that can be used to retrieve objects based on that configuration. The <c>registrar</c> is sometimes called the <i>composition root</i>. <n>Note: We use the syntax for the Swift IOC, but the examples are hopefully clear enough in their intent.</n> In the example below, we register singletons for each of the objects we want the container to be able to create, <c>Point</c>, <c>Int</c>, <c>[Movement]</c>, <c>IRobot</c> and <c>Simulator</c>. <code> let registrar = ServiceRegistrar() .registerSingle(Int.class) { _ in 2 } .registerSingle(Point.class) { _ in Point(x: 0, y: 0) } .registerSingle([Movement].class) { _ in [Movement(direction: .north, distance: 1)] } .registerSingle(IRobot.class) { p in Robot( speed: p.resolve(Int.class), location: p.resolve(Point.class), movements: p.resolve([Movement].class) } .registerSingle(Simulator.class) {p in Simulator(p.resolve(IRobot.class))} </code> This is a decent start, but many of the registrations above have no semantic meaning, like <c>Int</c> and <c>Point</c> and <c>[Movement]</c>. For these, it's better to use higher­level abstractions. <h>Step Seven: using higher­level abstractions</h> We need to define three abstractions---called <c>IOrigin</c>, <c>IRoute</c> and <c>IEngine</c>---with implementations. As well, the <c>IRobot</c> interface needs to be redesigned to use them. <code> protocol IRoute { var movements: [Movement] { get } } protocol IOrigin { var point: Point { get } } protocol IEngine { var speed: Int { get } } protocol ISimulator { func run() } class Simulator : ISimulator { var robot: IRobot init (_ robot: IRobot) { self.robot = robot } func run() { robot.move() } } struct StandardRoute : IRoute { var movements: [Movement] = [Movement(direction: .north, distance: 1)] } struct StandardOrigin: IOrigin { var point: Point = Point(x: 0, y: 0) } struct FastEngine : IEngine { var speed: Int = 2 } class Robot : IRobot { var location: Point! let engine: IEngine let route: IRoute init(_ engine: IEngine, _ origin: IOrigin, _ route: IRoute) { self.engine = engine self.route = route location = origin.point } func move() { for movement in movements { let distance = speed * movement.distance switch (movement.direction) { case .north: location.y += distance case .south: location.y -= distance case .east: location.x += distance case .west: location.x -= distance } } } } </code> We've created concrete objects for our standard parameters. An added bonus of the improved semantics is that we can rewrite the <c>init</c> for <c>IRobot</c> so that it no longer expects argument labels---because the parameter are now clear without further explanation. Now we can take another crack at the configuration using these new types. This time, we'll define an <c>extension</c> of the <c>IServiceRegistrar</c> that we can use again below. <code> extension IServiceRegistrar { func useSimulator() -> IServiceRegistrar { return self .registerSingle(IEngine.class) { _ in FastEngine() } .registerSingle(IOrigin.class) { _ in StandardOrigin() } .registerSingle(IRoute.class) { _ in StandardRoute() } .registerSingle(IRobot.class) { p in Robot( engine: p.resolve(IEngine.class), p.resolve(IOrigin.class), p.resolve(IRoute.class)) } .registerSingle(ISimulator.class) {p in Simulator(p.resolve(IRobot.class))} } } </code> We've now configured a system that knows how to create our simulator along with all of its dependencies. You can see that if the <c>ISimulator</c> type is resolved from the container, it will <ul> create a <c>Simulator</c>, which resolves the <c>IRobot</c>, which resolves the <c>IEngine</c>, <c>IOrigin</c> and <c>IRoute</c> </ul> <h>Step Eight: Changing the speed</h> An application can now change the speed of the robot without knowing anything else about the simulator, simply by changing the <c>IEngine</c> that's used. <code> class SlowEngine : IEngine { var speed: Int = 1 } let provider = ServiceRegistrar() .useSimulator() .registerSingle(IEngine.class) { _ in SlowEngine() } .commit() </code> As well, any location in the application can either use the <c>IRobot</c> or the <c>ISimulator</c> without having to know anything about how either of the concrete objects are constructed. The simulator might be much more complicated than the very simple one defined above. The robot might do much more when asked to <c>move</c>. <h>Step Nine: Using a factory</h> What if we wanted to let the robot decide how fast it is, depending on what kind of robot it is? Or what if we want to separate the <c>speed</c> from being fixed in the <c>IEngine</c>? What we need is a way to create transient objects that require parameters that are not available in the provider. These are types like <c>Int</c>, <c>String</c>, etc., as we had in <i>Step Six</i> above. The example below shows a very simple usage of the factory pattern. Instead of having a single <c>IEngine</c> for the whole application, we want to provide settings that the robot uses to get its engine. The code below sketches the new types and shows how the robot would use them. <code> protocol IEngineFactory { func createEngine(speed: Int) } protocol IRobotSettings { var speed: Int } class Robot : IRobot { init(_ engineFactory: IEngineFactory, _ settings: IRobotSettings, _ origin: IOrigin, _ route: IRoute) { self.engine = _engineFactory.createEngine(settings.speed) // ... } } </code> You'll note that we didn't declare any new properties. The robot still just has an engine, but asks the factory to create it based on a <c>speed</c>, rather than having the provider inject its singleton.