My focus in this article will be designing code using the SOLID principle of Dependency Inversion, and the use of Inversion of Control (IoC) frameworks for wiring applications up at runtime.
Determined to be Deterministic
As I discussed last week, the objective is to make our components testable using the Dependency Inversion principle, by binding our logic to abstractions (interfaces) rather than concretions. The third example I built showed how we pass our dependency instances (drinksMenu & clock) to the higher level class (Checkout) in the constructor. The reason we created this pattern was so that we can make the CalculateCost method deterministic.
A deterministic function is a function that produces a specific result from a particular set of supplied inputs. Each combination of inputs will produce the same output every time the function is executed. In the first two examples from last week, the code was not deterministic because the results of the function were being calculated from using state that was managed internally within the class which was not part of the inputs supplied to the function. The third example was deterministic because the state being used in the calculation was supplied as constructor parameters. This means that we can verify the output of the function because we can supply all the inputs.
You may already be familiar with deterministic functions if you have written User Defined Functions (UDF) in SQL Server. SQL Server will prevent a UDF from being created if it contains a call to getdate() within it, for the same reason that we decoupled the dependency the the DateTime variable in the example last week, a UDF requires a date to be supplied as an input to the function. The reason why UDFs must be deterministic is for memoisation so that SQL Server can cache results, but this constraint also forces developers to create testable UDFs.
Guided Tour of Castle Windsor
In my post from last week I showed some example mocks using a library called Moq which I typically use for mocking in unit tests, and similarly I prefer to use Castle Windsor as an IoC container. There are other libraries out there such as Autofac and Ninject, but I tend to stick to Castle Windsor, probably because I am familiar with it. Feel free to choose the library you prefer, as the concepts I am discussing are not specific to the library I am using, even though the examples themselves will use syntax that is library specific.
Finally, an ArcObjects Example
The next example will illustrate building ArcObjects code that uses an Inversion of Control (IoC) pattern. The example is relatively basic, it is a Server Object Extention which is supplied a "land use" code value in the request, which results in the service selecting the features that are defined with that land use. This is a simplistic example that illustrates the use of an IoC framework in a Server Object Extension, but in reality you would never create an extension to do this because it is achievable with a service straight out of the box.
The architecture of my example server object extension is a typical SOE class that has been autogenerated by the Visual Studio 2010 template for an SOE which comes with the ArcObjects SDK. I am building a 10.1 SOE which has class attributes that define settings used for registering the component when it is installed in the ArcGIS Server Site.
I have defined an ExampleService class, with matching interface which will contain my business logic, but there isn't much business logic, just the call to the repository class, and then the packaging up of the result. The repository class, with matching interface deals with the access to the geodatabase. My call to retrieve the affected land use parcels utilises a QueryFilter. So my dependency chain is - ExampleService, which depends on an ExampleRepository, which depends on a Workspace and a QueryFilter.
Our dependencies will be constructed by the IoC container, and supplied to the constructor of each class, except for the Workspace which is being acquired from the layer info in the service itself. This is done to simplify the definition of connection details, i.e. a connection can be acquired from an existing feature class represented as a layer in the service.
The Castle Windsor Approach
To use the Castle Windsor IoC container, simply install the Castle Core and Castle Windsor packages using NuGet. Once installed the libraries should be referenced in your project and available to be called.
There are two main aspects to using an IoC framework -
- Defining the mapping between interfaces and concrete types so that the IoC container can create the dependencies at run time.
- Using the IoC container to resolve the dependencies of the root object in the chain to execute the program.
In my example I map the interfaces/concrete types in code using an installer class. These classes implement the IWindsorInstaller interface which allow the IoC container to find them and derive the dependency mapping at runtime. Dependencies can also be defined in configuration files. When a developer writes a class that has dependencies they also alter the constructor of their class to supply the dependencies defined as an interface, and then add the interface/concrete type mapping to the installer file.
The second aspect of using the IoC framework is to create the instance of the root object of the program. In Castle Windsor this is explained as the three calls pattern -
- Creating the Container
- Resolving the root Interface/Concretion
- Disposing the Container
Lets see some code....
In the main SOE class ExampleSoe you can see that in the Construct method I call a method called bootstrapContainer to create an instance of the IWindsorContainer. In the Shutdown method I dispose of the container. Notice that I don't create an instance of the service until the operation handler is called. The ExampleService instance is create in the createService method, which executes the Resolve method for the IExampleService interface. When called this method will use the Installer classes to derive the concrete types from the interfaces in the constructors of each of the dependency classes up the chain. In addition to calling Resolve, the createService method also supplies the IWorkspace dependency which is held by the server object extension in one of the layer infos. The workspace dependency is passed by setting a property only, not in the constructors of the dependency chain.
The ExampleService class has a dependency on the IExampleRepository which it acquires in its constructor using the IoC container. The GetLandUseSelection method gets the landUse code passed into it in json, deserialises it and supplies it to the IExampleRepository.SelectParcelsWithLandUse method. Once executed it constructs a json array of the resulting parcels. Note the Parcel model which contains a Deserialise and Serialise method to convert to and from a json fragment.
The ExampleRepository class has a dependency on the IQueryFilter which it acquires in its constructor using the IoC container. The SelectParcelsWithLandUse validates the land use code, gets the desired feature class, executes a query using the IQueryFilter and converts the result cursor to a list of Parcel models which are returned.
The installer classes map the interfaces to the desired concrete types. I have separated my installers into two classes, one to handle ArcObjects dependencies and one to handle dependencies in the project itself.
As you can see, using Castle Windsor allows us to execute the program as we typically would, without having to supply the dependencies in our constructors manually, Castle Windsor takes care of that for us, and manages caching of the types etc to improve performance.
Conclusion
Hopefully this gave a more concrete example of using ArcObjects with an IoC container and showed how and where the container and the root instances should be created in a server object extension project.
There are many more features available in Castle Windsor for creating dependency instances which goes beyond my example, such as singletons and typed factories, but feel free to explore the framework further yourself.
In the coming posts I will focus more on Unit Testing and Mocking.
No comments:
Post a Comment
Note: Only a member of this blog may post a comment.