|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Want a new Job?
Chapters
Services
Feature Zones
|
IntroductionIn this installment of the series, I'll show you how you can use Note: If you're wondering what the entire BackgroundWhen I discovered how to write The thought of having to clutter my code with direct references to For me, using an inversion-of-control container was the best way to meet these requirements. I therefore set out to find a library that I could easily use without having to spend more than five minutes reading its documentation or being forced to spend even more time trying to figure out its object model. More importantly, I needed a container so simple that it required little to no configuration. The container should be able to assemble itself without requiring me to write code to wire all the dependencies together. It also shouldn't force me to spend time trying to learn someone else's XML syntax. What About…?I looked at various IOC containers such as Castle's Windsor container, Spring.NET and others, but they didn't meet my needs for simplicity. They all had an impressive number of features, but I knew that I wasn't going to use all of their respective feature sets. I also knew that I didn't have the time to write that many configuration files to get my application up and running. Since none of those containers fit my needs, I decided to write my own. That's how Features and UsageIn the following sections, I'll briefly discuss the features commonly found in an IOC container and I'll show you how easy it is to implement the same features using An Example ModelNote the following example model, in pseudo-code: A vehicle has an engine.
A vehicle also has a driver, which is a person.
Vehicles can either move or park.
An engine can either start or stop.
A person has a name and an age.
Using this description, one can glean an object model that results in the following code: public interface IVehicle
{
void Move();
void Park();
IPerson Driver { get; set; }
IEngine Engine { get; set; }
}
public interface IEngine
{
void Start();
void Stop();
}
public interface IPerson
{
string Name { get; set; }
int Age { get; set; }
}
Interfaces?The first thing that you might notice here is that I'm actually using interfaces for the model rather than using either an abstract base class or a concrete class. Using interfaces allows you to easily use other parts of the Object InstantiationOne of the most important features that any IOC container can provide is the ability to separate the details of object construction from its actual usage. For the SimpleContainer container = new SimpleContainer();
// (Load the container's dependencies somewhere here..)
IVehicle vehicle = container.GetService<IVehicle>();
// Do something useful with the vehicle
Vehicle.Move();
In this example, the client code knows nothing about the concrete class that implements the Dependency InjectionIn order to ensure that there is a working implementation of the [Implements(typeof(IVehicle), LifecycleType.OncePerRequest)]
public class Car : IVehicle
{
private IEngine _engine;
private IPerson _person;
// Note: The loader can only work with default constructors
public Car()
{
}
public IEngine Engine
{
get { return _engine; }
set { _engine = value; }
}
public IPerson Driver
{
get { return _person; }
set { _person = value; }
}
public void Move()
{
if (_engine == null || _person == null)
return;
_engine.Start();
Console.WriteLine("{0} says: I'm moving!", _person.Name);
}
public void Park()
{
if (_engine == null || _person == null)
return;
_engine.Stop();
Console.WriteLine("{0} says: I'm parked!" , _person.Name);
}
}
Most inversion-of-control containers would use an external XML file to describe what should be injected into the container. That's not the case with string directory = AppDomain.CurrentDomain.BaseDirectory;
IContainer container = new SimpleContainer();
Loader loader = new Loader(container);
// Load CarLibrary.dll; If you need load
// all the libaries in a directory, use "*.dll" instead
loader.LoadDirectory(directory, "CarLibrary.dll");
The loader will scan the CarLibrary.dll assembly for any types that have Lifecycle ManagementLike other IOC containers, Once per RequestEvery time a client requests a service instance from the container, the container will create a brand new instance for your client to use. No two instances that are instantiated will ever be the same reference. In the previous example, we specified Once per ThreadIn contrast to [Implements(typeof(IVehicle), LifecycleType.OncePerThread)]
...
If you need an application-wide singleton, on the other hand, then the next option might be what you're looking for. Singleton InstancingWith the [Implements(typeof(IVehicle), LifecycleType.Singleton)]
...
Property Setter InjectionThe next thing that you might have noticed is that public interface IInitialize
{
void Initialize(IContainer container);
}
When an object is created by the container, it checks if the newly-created instance implements // Note: the differences are highlighted in bold
[Implements(typeof(IVehicle), LifecycleType.OncePerRequest)]
public class Car : IVehicle, IInitialize
{
// … Code omitted for brevity
public void Initialize(IContainer container)
{
_engine = container.GetService<IEngine>();
_person = container.GetService<IPerson>();
}
}
Since each type is responsible for injecting its own property dependencies from the container, the container doesn't have to know anything about injecting those property dependencies into each type. This makes the code for Rolling Your Own Property InjectorIf, for some reason, public interface IPropertyInjector
{
bool CanInject(object instance, IContainer sourceContainer);
void InjectProperties(object instance, IContainer sourceContainer);
}
For example, public class DefaultPropertyInjector : IPropertyInjector
{
public bool CanInject(object instance, IContainer sourceContainer)
{
return instance is IInitialize;
}
public void InjectProperties(object instance, IContainer sourceContainer)
{
if (!(instance is IInitialize))
return;
IInitialize init = instance as IInitialize;
init.Initialize(sourceContainer);
}
}
As you can see here, there's nothing special about MyCustomInjector injector = new MyCustomInjector();
container.PropertyInjectors.add(injector);
Every time the container creates a new object, it asks every Constructor InjectionUnlike other containers, Creating Your Own FactoryAs it turns out, public Car(IEngine engine, IPerson driver);
...and let's further suppose that every time there was a request for an A Layer of IndirectionIn order to create your own factory, all you have to do is implement the [Factory(typeof(IVehicle))]
public class CarFactory : IFactory<IVehicle>
{
public IVehicle CreateInstance(IContainer container)
{
// Get an instance of the engine
// and the driver
IEngine engine = container.GetService<IEngine>();
IPerson driver = container.GetService<IPerson>();
Car newCar = new Car(engine, driver);
return newCar;
}
}
The only thing left to do at this point is load your custom factory into the container. Fortunately, there's no additional code that you have to write in order to make this happen. Once you call the initial Method Call InterceptionLike other containers, public interface ITypeInjector
{
bool CanInject(Type serviceType, object instance);
object Inject(Type serviceType, object instance);
}
The ITypeInjector yourInjector = new SomeTypeInjector();
Container.TypeInjectors.Add(yourInjector);
With this approach, transparently adding additional behavior to your interface instances becomes practically trivial when combined with ExtensibilityNearly every IOC container library has at least one or two features that allow it to extend its feature set using plug-ins. In public interface IContainerPlugin
{
void BeginLoad(IContainer container);
void EndLoad(IContainer container);
}
The [ContainerPlugin]
public class SamplePlugin : IContainerPlugin
{
void BeginLoad(IContainer container)
{
// Do something useful here
Console.WriteLine("Load started");
}
void EndLoad(IContainer container)
{
// Do something useful here too
Console.WriteLine("Load completed");
}
}
…and just as it's done with the custom Points of InterestInversion of Control, Implement ThyselfProbably one of the most interesting things that you might notice about the Choosing Configuration through Reflection Instead of XMLWhile XML is (by definition) extensible and self-describing, it seemed like some developers were reinventing the wheel by trying to describe dependencies that were already contained in every .NET assembly ever compiled. For me, it made more sense to query the existing metadata embedded in an assembly rather than parse an external XML file that had more or less the same information. Thus, I chose to skip implementing an XML-based configuration. Coming Up in the Next ArticleIn Part V of this series, I'll show you how you can use [ContractFor(typeof(IDbConnection))]
public interface IConnectionContract
{
[RequireConnectionStringNotEmpty] void Open();
}
[AttributeUsage(AttributeTargets.Method)]
public class RequireConnectionStringNotEmptyAttribute : Attribute,
IPrecondition
{
public bool Check(object target, InvocationInfo info)
{
IDbConnection connection = target as IDbConnection;
// If it's a null object, return true and
// skip the check
if (connection == null)
return true;
return !string.IsNullOrEmpty(connection.ConnectionString);
}
public void ShowError(TextWriter output, object traget, InvocationInfo)
{
Output.WriteLine("The connection string cannot be null or empty!");
}
public bool AppliesTo(object target, InvocationInfo info)
{
// Perform this check only on IDbConnection objects
IDbConnection connection = target as IDbConnection;
if (connection == null)
return false;
return true;
}
public void Catch(Exception ex)
{
// If this precondition throws an error,
// ignore it
}
}
To make this even more interesting, we're going to be using the contract in VB.NET: Imports System
Imports System.Data
Imports System.Data.SqlClient
Imports LinFu.DesignByContract2.Injectors
Imports Simple.IoC
Imports Simple.IoC.Loaders
Module Module1
Sub Main()
Dim container As New SimpleContainer()
Dim loader As New Loader(container)
Dim directory As String = AppDomain.CurrentDomain.BaseDirectory
' Explicitly load the contract loader dll
loader.LoadDirectory(directory, _
"LinFu.DesignByContract2.Injectors.dll")
' Load everything else
loader.LoadDirectory(directory, "*.dll")
' Load the sample IConnection contract that was written in C#
Dim contractLoader As IContractLoader = _
container.GetService(Of IContractLoader)()
contractLoader.LoadDirectory(directory, "SampleContracts.dll")
' Manually add a service for IDbConnection
container.AddService(Of IDbConnection)(New SqlConnection())
Dim connection As IDbConnection = _
container.GetService(Of IDbConnection)()
' The code will fail at this point since there
' is no connection string defined
connection.Open()
End Sub
End Module
Believe it or not, that last call to
The error message displayed in the exception should look familiar; it's the same one that we defined in the example above. Think about that for one second. LinFu.DesignByContract allows you to pass contracts around as simple DLL files. Unlike Eiffel, the contracts can actually be swapped between languages and passed around as reusable libraries. The onus is on you to decide what to do with this feature. Suffice it to say that after using Nearly TransparentAside from the container setup code in the VB.NET example above, the client is completely oblivious to the existence of a contract. Adding new contracts is as easy as copying new contract libraries to the application directory. Once those contracts have been placed into that directory, the only thing left to do is tell the History
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||