Saturday, September 27, 2008

Using Ninject(one DI container)

在设计中, 经常会遇到这样的情况, 在一个类中要创建或使用另一个类的实例, 暂且将前一个类称为大类, 后一个类称为小类, 也许小类有很多变种, 我们会根据不同的情况, 让大类使用不同的小类变种. 可以看出这里有一个耦合关系, 一个好的办法是使用依赖注入模式(Dependency Inject), Martin Fowler 有个非常精彩的文章专门讲述DI).
DI原理其实很简单, 我们设计的大类要依赖于小类的接口或基类, 而不是一个具体的小类, 同时不要在大类中创建小类实例, 而是通过大类的构造子或property将小类的实例传进来即可.
目前.Net领域有不少DI容器框架, (这里是DI容器框架列表), 这些框架主要有两个功能, (1)帮助完成依赖的注册与依赖实例的创建, 即绑定小类接口和某个具体小类, 绑定的地方不是在大类中, 所以实现了解耦. (2)更高级的功能是, DI 容器框架可以帮助我们创建大类实例, 就像是一个Factory工厂方法一样, 当然大类所依赖的小类, 也不同我们手动创建, 容器自动按照我们定义的小类绑定关系来创建小类实例.
Ninject是一个新的DI框架, 它的特点是:
(1)不需要使用Xml来定义绑定关系, 使用一个自定义的Module类(基类为StandardModule)来定义绑定关系;
(2)不仅仅支持constructor注入, 而且支持property注册;
(3)对于constrctor注入, 构造子还可以包含其他类型参数, 不仅仅是所依赖的类型参数.
(4)对于非constructor注入, 不仅仅支持无参构造子, 而且支持带参构造子.
(5)支持基于类型的条件绑定, 可以使用Bind().To().ForMembersOf();
(6)部分支持基于variable的Context的条件绑定, 示例代码中对此有说明.(Ninject支持另一种条件绑定, 但它需要在大类的代码加上Parameter attribute或Method attribute, 增加了大类和小类的耦合度, 所以我不喜欢, 好在基于context variable的绑定基本够用了)
(7)对于利用DI容器创建的对象, 可以在定义绑定时, 指定对象的生命周期, 比如Singleton以及SingleCall的生命周期模式.

下面是一个使用Ninject的简单示例, 需要添加Ninject.core和Ninject.condition两个assembly.
主程序类和自定义的Module类:



using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;

using Ninject.Core;
using Ninject.Conditions;
using Ninject.Core.Behavior;
using Ninject.Core.Parameters;
using Ninject.Core.Activation;

namespace NinjectDemo
{
class Program
{
static void Main(string[] args)
{
#region DI: Old way
IEditor editor = new NotePad();
var csProgrammer = new WindowsProgrammer(editor);
csProgrammer.WriteCode();
#endregion

#region DI container
#region Initialization for using Ninject
var module = new ProgrammerModule();
IKernel kernel = new StandardKernel(module);
#endregion

#region Simple inject
var vbProgrammer = kernel.Get<WindowsProgrammer>();
vbProgrammer.WriteCode();

//Get correct concrete class according to binding condition
var linuxProgrammer = kernel.Get<LinuxProgrammer>();
linuxProgrammer.WriteCode();
#endregion

#region Including other parameters rather than inject interface
Dictionary<string, object> param = new Dictionary<string, object>();
//there is another parameter named firstName in constructor
param.Add("firstName", "Harry-Mac OS");
var macProgrammer = kernel.Get<MacProgrammer>(With.Parameters.ConstructorArguments(param));
macProgrammer.WriteCode();
#endregion

#region Control binding by context variable
IParameterCollection pc = With.Parameters.ContextVariable("OS", "Mac").ConstructorArguments(param);
var programmer = kernel.Get<Programmer>(pc);
macProgrammer.WriteCode();
#endregion

#endregion DI container

Console.WriteLine("Enter any key to exit...");
Console.ReadKey();
}
}


class ProgrammerModule : StandardModule
{
/// <summary>
/// 注册接口和对象的绑定
/// </summary>
public override void Load()
{
//This is a default bind for IEditor inteface
Bind<IEditor>().To<NotePad>();
//Bind<IEditor>().To<NotePad>().Always();// same to the above line

//This is a condtional bind for IEditor injection
Bind<IEditor>().To<Vi>().ForMembersOf<LinuxProgrammer>();
//or the following code
//Bind<IEditor>().To<Vi>().OnlyIf(c => c.Member.DeclaringType == typeof(LinuxProgrammer));
//or the following code
//Bind<IEditor>().To<Vi>().OnlyIf(delegate(IContext c) { return c.Member.DeclaringType == typeof(LinuxProgrammer); });

Bind<IEditor>().To<Vi>().ForMembersOf<MacProgrammer>();

//This is another kind of conditional bind.
//我们获取对象要用kernel.Get<T1>()方法, 绑定用Bind<T2>()方法,
// 这种context绑定仅仅适合于T1和T2同一类型或基于同一类型
Bind<Programmer>().To<MacProgrammer>().Only(When.Context.Variable("OS") == "Mac");
}

/// <summary>
/// Load2 方法在本项目中其实没用, 只是为了列出Ninject在绑定时可指定对象的生命周期
/// </summary>
public void Load2()
{
//每次调用kernel.Get<>(), 总是返回一个新的对象, 如果Remoting中讲的SingleCall对象
Bind<IEditor>().To<NotePad>();
//Bind<IEditor>().To<NotePad>().Using<TransientBehavior>(); //same as the above

//获取的对象是Singleton对象, 即使是不同线程, 获得的对象总是同一个
Bind<IEditor>().To<NotePad>().Using<SingletonBehavior>();

//One instance of the type will be created per web request, and will be destroyed when the request ends.
Bind<IEditor>().To<NotePad>().Using<OnePerRequestBehavior>();

//在同一个线程中, 每次调用kernel.Get<>()获取的对象总是同一个
Bind<IEditor>().To<NotePad>().Using<OnePerThreadBehavior>();
}
}

}


下面是IEditor接口和实现类:



using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace NinjectDemo
{

interface IEditor
{
string Code();
}

public class Vi : IEditor
{
public string Code()
{
return "This is Vi editor";
}
}

public class NotePad : IEditor
{
public string Code()
{
return "This is NotePad editor";
}
}
}

下面是Programmer基类和派生类:



using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

#region Ninject namespace
using Ninject.Conditions;
using Ninject.Core.Behavior;
using Ninject.Core;
#endregion

namespace NinjectDemo
{
abstract class Programmer
{
IEditor m_Editor;
public Programmer(IEditor editor)
{
this.m_Editor = editor;
}

public virtual void WriteCode()
{
Console.WriteLine(m_Editor.Code());
}

}

class WindowsProgrammer:Programmer
{
public WindowsProgrammer(IEditor editor)
: base(editor)
{
}
}

class MacProgrammer : Programmer
{
string m_FirstName;

[Inject]
public MacProgrammer(IEditor editor, string firstName)
: base(editor)
{
m_FirstName = firstName;
}

public override void WriteCode()
{
Console.Write( m_FirstName+" , ");
base.WriteCode();
}
}

class LinuxProgrammer : Programmer
{
[Inject]
public LinuxProgrammer(IEditor editor)
: base(editor)
{
}
}
}