Dynamic component binding made easier - An easy to use Microkernel to help reap Contract-First-Design benefits in .NET programs
If you like, you can download Ralf´s Microkernel here. It´s written in C# and comes with a couple of unit tests (NUnit style). Feel free to use the binary in your projects and play around with the source. Although it´s not much code, I hope you will be able to gain quite a bit from its essential Microkernel functions. Here is how it works...
Contract-First-Design
I don´t want to explain CFD at length here, but to set the scene for the Microkernel some hints as to what it means are in order.
CFD views software as consisting of components. To avoid any heated discussion on what this term could possibly mean, let me try a very practical explanation: Think of a component as an assembly or a set of related assemblies. This assembly (or set of assemblies) you want to use together with other components to build a software solution.
That´s it.
Don´t think of "components are made for reuse" or "components require a market" or "components as units of independent versioning". That´s all good and well, but can get in your way when starting out with components. So just think: Components encapsulate some functionality I want to separate from other functionality in other components.
Now, when using components there are two basic kinds of them: client components (or consumers) and service components (or producers). A client component uses one or more service components to do its work.
When using VS2005 you´d set up two projects, one for the client component and one for the service component and then the client component would refrence the service component. That´s necessary to instanciate service component classes.
Although this is how most of the developers do it most of the time, it´s not how you should do it. There are at least two reasons, why this is hampering your work:
- For to code the client component you need any service component to already exist. Only existing service components can be referenced in VS2005 projects. That´s ok for the grid component you use - but what about the components you develop yourself? You´d need to develop them strictly bottom-up. But that´s bad for your productivity.
- When testing the client component it always needs to use the real service component. That might be bad for your testing speed, though, because the service component might need a long time for its operations. Testing would be easier if you could exchange the service component for some stand-in or mock-up to ease testing of the client component. But usage of the service component is hard wired into the client which instanciates classes from it.
So, what can you do to overcome these hurdles in component oriented software development?
Use Contract-First-Design!
CFD changes the above picture like this:
A client component no longer references its service components, but only so called contracts. Each contract describes the services of a service component in terms of interfaces and other data types independent of any service implementation. The contracts of one or more components can be assembled in a contract assembly. However I prefer a one-to-one mapping: each contract gets its own assembly and each assembly is contains only one contract.
Contract-First-Design then means, you define all contracts before you implement them. You create contract assemblies before their service components which implement the interfaces described in their contracts.
Then - and this is crucial to CFD! - client components only reference contracts. Client components don´t reference service components anymore! During design time client components don´t know which service components will fulfill their requests during runtime. There is no more static binding between client and service components.
That´s why you can´t instanciate classes from a service component anymore in a client component. You simply don´t know the class implementing a service contract´s interface.
This might sound grim to you - and it is considering support of this style of programming in VS2005.
But don´t despair! Help´s on its way.
But first: How does CFD solve the above two problems?
- Since you no longer reference a service component in your client but just its contract assembly, you are free to develop components in any order you like or in parallel. Only the contract assemblies need to be present - that´s why it´s called Contract First (!) Design.
- Since service components are no longer referenced there are no dependencies on their types in a client component´s code anymore. As long as some component is available during testing which adheres to the contract required by the client, the client can be tested. There needs just a way to map the contract to a concrete implementation during runtime. If this implementation then is just a mock-up or "the real thing" is of no concern for the client component´s code.
A Microkernel to the rescue
So if there is no static binding between client and service anymore, how and when is a client component then bound to a service component? This binding happens dynamically during runtime - and is done by a Microkernel. (I prefer the term Microkernel (or the simpler Service Locator) instead of Dependency Injecttion or Inversion of Control framework, since I want to keep the concept of dynamic binding separate from the initialization of objects and am skeptical about automatically weaving large dependency networks.)
A Microkernel helps a client component to overcome its ignorance regarding the service implementation. A Microkernel makes the impossible possible: it lets you instanciate an interface! Instead of
... = new MyServiceClass();
you write something doing the equivalent of
.. .= new IMyService();
Or to be more concrete, here´s how you´d do it using my little Microkernel:
using ralfw.Microkernel;
...
... = DynamicBinder.GetInstance(typeof(IMyService));
Of course a Microkernel is not smarter than VS2005 or the .NET Framework. So in order to accomplish its feat, the Microkernel needs some help. Before it can instanciate an interface it needs to know to the implementing class. Please note: The Microkernel needs to know this mapping between interface (contract) and class (service implementation), not the client component. This might sound like a small difference compared to how you usually work, but in fact it is not. It´s a major difference separating would be component orientation from real and true component orientation.
To help the Microkernel you need to provide it with the required mapping information: For each interface of a contract a client should be able to instanciate you need to state the class to instanciate behind the scene. You need to bind interfaces to implementations, e.g. "Interface ServiceContract.IMyService is implemented by class AllMyServices.MyServiceClass in assembly AllMyServices.dll."
This mapping can be done either imperatively:
DynamicBinder.AddBinding(typeof(IMyService),
"AllMyServices.MyServiceClass, AllMyServices",
false);
or in a XML mapping file (which can be the app.config of your program):
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<sectionGroup name="microkernel">
<section name="objects"
type="ralfw.Microkernel.ConfigSectionMappingAdapter, ralfw.Microkernel" />
</sectionGroup>
</configSections>
<microkernel>
<objects>
<object name="ServiceContract.IMyService"
type="AllMyServices.MyServiceClass, AllMyServices" />
</objects>
</microkernel>
</configuration>
That´s it! More you don´t need to do.
- Design your contracts and formalize them as assemblies in their own right.
- Bind client components to contract assemblies instead of service components.
- Define the mapping between contract interfaces and service component classes implementing them.
- Use a Microkernel to instanciate service component classes through their contract interfaces.
Easy, isn´t it?
The downside of using a Microkernel
Well, you could stop reading here and start using my little Microkernel or any other like dynamic binding framework. They all work alike.
However, sooner or later you´ll find a couple of things disturbing the rosy picture of just outlined:
- Setting up the mapping - even a declarative mapping like the XML above - is tedious and error prone. If you truely subscribe to component oriented programming and decompose your software solution into a larger number of components and also keep these components in different (!) VS2005 solutions to enforce component oriented thinking and ease of parallel development, then you´ll end up defining a lot of mappings. And each one, even the most straightforward ones, require some setup effort.
- If your client components no longer reference service components directly then VS2005 cannot help gather all assemblies in the execution directory of a solution´s startup project. You´ll have to somehow copy the relevant files manually before running a project.
- Components often read some parameters for their work from an external file. The .NET Framework provides an easy means for this via its System.Configuration.ConfigurationManager object. Just put any parameters into the app.config of your program. This might be just fine as long as you develop your software within a single large VS2005 solution. But as soon as you set up separate VS2005 solutions for each component you´ll need to merge settings from several app.config files whenever you integrate several components.
This is the downside of using a Microkernel: If you subscribe to CFD and want to let the organization of your code reflect your component oriented design, then you loose quite some convenience provided by VS2005.
Or to put it the other way around: Microkernels are a great way to reach higher productivity and better testability - but this comes at quite some price. It´s less convient than using VS2005 like Microsoft is promoting it.
Since I deem the Microkernel concept core to true component orientation and found it unnerving to deal with the above problems, I´m trying to overcome them with my own little Microkernel and some guidance.
Solution 1: Semi-automatic gathering of assemblies
Once you no longer reference service components but just contracts, VS2005 does not help you anymore gathering all necessary assemblies in your EXE-project´s bin directory. Fortunately it´s quite easy to "simulate" the VS behavior. Here´s my guideline:
In the VS2005 project supposed to integrate several componets, add the components´ assemblies manually using the "Add existing item...". Be sure to link (!) them and set the "Copy to output directory" flag to true!
Here´s an example: Consider the following simple component oriented architecture.
Three components. The frontend is the client of the businesslogic, which in turn is the client of the DB adapter. Two contract isolate the clients from any concrete implementations of the services they depend on.
Following CFD the contracts are defined first and then implemented as distinct VS2005 projects:
Since the DB adapter does not rely on any further service component, it´s straightforward to implement:
Put it in a VS2005 solution of its own and add a test project which could contain unit tests for the component. The whole solution I call a component test bed, because it not only contains the component itself but also any tests and possible mock-up (as stand-ins for service components required by the component under development).
Enter the Microkernel: The client of the DB adapter is the businesslogic component. It too is put into a test bed solution of its own:
Since the businesslogic needs the service of the DB adapter, it refrences the respective contract as well as the Microkernel. In code it then instanciates the DB adapter using the DynamicBinder class:
namespace microkernel.demo.logic
{
public class Businesslogic : contract.IBusinesslogic
{
private microkernel.demo.dbadapter.contract.IDBAdapter db;
public Businesslogic()
{
db = (microkernel.demo.dbadapter.contract.IDBAdapter)
ralfw.Microkernel.DynamicBinder.GetInstance(
typeof(microkernel.demo.dbadapter.contract.IDBAdapter));
}
...
Using the Microkernel in the businesslogic means, any integrator of components including the businesslogic needs to provide a IDBAdapter implementation. In the above sample that´s the task of the test businesslogic project.
But since neither it nor the businesslogic can or should reference the DBAdapter component in the regular way, how can this be accomplished? How can the test project provide a IDBAdapter implementation during runtime? By linking the DBAdapter component into the project (see highlighted project item above) and setting its "Copy to Output Directory" property to true. VS2005 will then see to that the bin\debug or bin\release directory will always contain the latest copy of this file. And that´s like VS2005 behaves for ordinarily referenced assemblies.
If you want to be able to step into a component linked into a project like this be sure to also link in its .pdb file. The same is true for any supporting files the component needs (e.g. other assemblies it depends on or configuration files).
By following this guideline you can get the same level of convenience you´re used to from using VS2005 and regular assembly references. It requires slightly more effort - but it ensures you can reap the benefits of using a Microkernel.
To sum up:
- A project implementing a contract references the contract assembly as usual. It´s exporting this contract. See the DBAdapter project above.
- A project using a contract references the contract assembly as usual. It´s importing this contract. In addition it references the Microkernel and uses it to instanciate contract interfaces. See the Businesslogic project above.
- A project hosting components which use the Microkernel at least needs to link in the assemblies of any service component required in the described way. See test project for businesslogic above.
Solution 2: Automatic mapping
Once all required service components have been gathered in a host´s runtime directory a mapping has to be built. Any contract interfaces to be instanciated by client components need to be bound to classes in service components implementing them.
This is usually done using a declarative mapping in the app.config of the hosting project (e.g. the test project for businesslogic above) or in a separate XML mapping file. Here´s a sample app.config:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<sectionGroup name="microkernel">
<section name="objects"
type="ralfw.Microkernel.ConfigSectionMappingAdapter, ralfw.Microkernel" />
</sectionGroup>
</configSections>
<microkernel>
<objects>
<object name="microkernel.demo.dbadapter.contract.IDBAdapter"
type="microkernel.demo.dbadapter.DBAdapter, microkernel.demo.dbadapter" />
</objects>
</microkernel>
</configuration>
Please note the config section handler defined at the top. It´s needed for the Microkernel to access the <microkernel> section. This mapping section can be either named <microkernel> or <spring>, because its <objects> element looks like the one from Spring.Net, although it´s stripped down to the essential mapping information.
Each <object> binding consists of a name for the binding and the type to be instanciated for this name. The type information is given as the fully qualified class name followed by the assembly name containing the class. It´s the usual .NET Framework format for such kind of information.
The name of the binding is arbitrary - however I prefer to set it to the fully qualified name of the interface type to be instanciated and which is implemented by the type referenced. The Microkernel supports this convention by providing a GetInstance() overload which takes an interface type and uses its full name as the name for the binding to look up in the mapping.
In order to load mappings from the app.config call
ralfw.Microkernel.DynamicBinder.LoadBindings();
right after start of the hosting code.
If you want to keep the mapping info in a separate XML file like this:
<?xml version="1.0" encoding="utf-8" ?>
<microkernel>
<objects>
<object name="microkernel.demo.dbadapter.contract.IDBAdapter"
type="microkernel.demo.dbadapter.DBAdapter, microkernel.demo.dbadapter" />
</objects>
</microkernel>
then call
ralfw.Microkernel.DynamicBinder.LoadBindings("mymappings.xml");
instead.
LoadBindings() will read the bindings, build a map and load the referenced assemblies. When you later call DynamicBinder.GetInstance() the Microkernel will look up the name and instanciate the type referenced implementation type. Either a new instance is created each time or a singleton is created once. This you can determine by setting the singleton attribute of the mapping.
<object name="microkernel.demo.dbadapter.contract.IDBAdapter"
type="microkernel.demo.dbadapter.DBAdapter, microkernel.demo.dbadapter"
singleton="true" />
The default is false.
That´s all nice and well - but it´s quite cumbersome. Especially when you start using a Microkernel you´ll hate to get the mapping right first before you can see your program running. That´s why my little Microkernel provides a third way of setting up the mapping.
When you call
ralfw.Microkernel.DynamicBinder.CompileBindings();
the Microkernel will determine the mappings all by itself. It will scan all assemblies in the runtime directory of the host and check if they contain contract interfaces or contract interface implementations. Interfaces will automatically bound to implementations. No explicit mapping is needed on your side. Just ensure all service component assemblies are there.
In addition only one small hint you need to give to the Microkernel: You need to annotate contract interface which client components should be able to instanciate with an attribute:
namespace microkernel.demo.dbadapter.contract
{
[ralfw.Microkernel.Bindable()]
public interface IDBAdapter
{
...
That´s all. The Microkernel will now be able to spot relevant contract interfaces and find any class implementing them in the assemblies present in the runtime directory.
One word of caution: This is a feature to help you enter the realm of true component oriented development. It´s targeted at pretty small projects with maybe 10, 20, 30 assemblies in the runtime directory. So please don´t blame it on the Microkernel, if CompileBindings() takes too long for your taste, if you use it with hundreds of components. It´s not targeted for such large scenarios - at least not performance wise.
Solution 3: Distributed settings
Once you start developing components in a way like I described above, where you set up test bed solutions for each component, you´ll soon realize, it´s hard to merge all the different app.config settings required by the components. In many test beds you´ll define special settings for just this component which later will have to be copied to the app.config of the integrating host project. That´s tedious and error prone work.
To make the handling of settings (or external component parameters) easier, I propose component local config files. Here´s an example:
Let´s assume the DBAdapter requires a connection string it wants to load from an external setting file. Instead of putting it into the app.config create a XML setting file in the DBAdapter´s project with a name of your choice, e.g. microkernel.demo.dbadapter.config. It could look like this:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<add key="connectionstring" value="server=..."/>
</appSettings>
</configuration>
Then add a reference to the Microkernel to the DBAdapter even though it does not want to instanciate any service components. Rather the Microkernel is used to load the setting (instead of the usual System.Configuration.ConfigurationManager):
namespace microkernel.demo.dbadapter
{
public class DBAdapter : contract.IDBAdapter
{
public int[] LoadValues(string filename)
{
string connectionstring;
connectionstring = ralfw.Microkernel.ConfigurationManager.Settings("microkernel.demo.dbadapter.config")["connectionstring"];
...
The Settings() method returns a ConfigFile object which - for now - allows access to <appSettings> settings in the file specified.
In order to have your own config file available where it´s needed during runtime, link it into the integrating host project like a component assembly (see highlighted file in the above picture) and set its "Copy to Output Directory" flag to true.
This way you can set up individual settings for each component in several XML files and avoid merging them into a single file for integration. Instead you just link in all necessary settings files and use a single API to access them, the Microkernel´s ConfigurationManager.
This is close to how the .NET Framework handles such settings. So it´s easy to understand and use. Currently the ConfigFile class is limited to accessing <appSettings> only, but if distributing settings in this way appears practical, then it´s easy to add support for config section handlers.
Summary
Even though my little Microkernel does not sport an array of features like Spring.Net etc. it does a decent job of dynamically binding contract implementations. But what´s most important, it´s much easier to start with than other service locator frameworks, since it can build the interface-implementation mapping automatically and let´s you keep component settings separate.
The guideline for linking in components manually helps you to enforce a true distributed development of components in their own test beds and still benefit from some VS2005 help during integration. That´s ok for a start, I´d say. But in the long run, I want to provide some tool support for this too. But that´s a different story. Stay tuned!
For now, have fun using my little Microkernel and feel free to let me know what you think of it.