原文地址:http://zhangz.wordpress.com/2010/01/17/reducing-code-coupling-service-locator/
我们在代码中经常使用new关键字来实例化对象。这样做使得变量和具体的类型耦合在了一起。在大多数情况下,这样做并没有什么问题。但是在某些情况下,会导致我们希望避免的紧耦合。
SqlConnection conn = new SqlConnection();
上面的代码实例化了一个SqlConnection对象,任何使用了conn对象的代码都与SqlConnection类型发生了耦合。
IDbConnection conn = new SqlConnection();
这段代码与之前的区别在于,conn的类型是一个接口IDbConnection。同时,我们将一个SqlConnection类型的object
赋给了变量conn,SqlConnection类型实现了IDbConnection接口。尽管conn是抽象类型IDbConnection,但是整行代码仍然与SqlConnection类型耦合在了一起。于是我们将代码修改为:
IDbConnection conn = CreateConnection();IDbConnection CreateConnection() { return new SqlConnection(); }
conn仅仅依赖于IDbConnection,通过调用CreateConnection得到一个IDbConnection的实现。conn变量
并不知道具体实现是什么,只知道其实现了IDbConnection接口。这就是面向接口编程。通过将实例化对象的
工作交给了一个单独的方法,上面的代码展示了一个很简单的service location的例子。在这里,从其他地方接
受依赖比直接创建依赖更好,可以帮助我们降低耦合。
Service locator提供了一个统一的位置保存所有用到的依赖,和按需获得特定依赖的方法。作为对面向接口编程实践的支持,使用service locator能够减少代码之间的耦合,使代码是可测试的。
我们经常用session来保存数据,并在之后使用。这可以通过直接引用Session对象来实现,但是考虑到我们以后可能会把session数据保存在数据库中。Session默认实现HttpContext.Current.Session是把数据存放在web server的内存中的。同时也考虑到单元测试的需要,Session对象在web server上才有效,因此在单元测试时无法使用Session对象。
下面的接口定义了session存储和访问的方式:
public interface ISession { object this[string key] { get; set; } void Remove(string key); bool Contains(string key); void Clear(); } //下面是接口实现: public sealed class Session : ISession { public object this[string key] { set { HttpContext.Current.Session[key] = value; } get { return HttpContext.Current.Session[key]; } } public void Remove(string key) { HttpContext.Current.Session.Remove(key); } public bool Contains(string key) { return HttpContext.Current.Session[key] == null; } public void Clear() { HttpContext.Current.Session.Clear(); } }
到目前为止,我们已经拥有了一个ISession接口定义及其实现,接下来将实现一个service locator用来定位这个服务,让service的使用者可以在需要时使用服务。
下面是定义了Service locator的接口:
public interface IServiceLocator { T Resolve<T>(); }
下面是一个简单实现,除了实现接口之外,还包含了一些其他的函数,比如将service注册到registry里,这与IServiceLocator接口无关,使用该接口的类只需要知道怎样得到service。这个registry的方法是定义在具体实现里的,因为不同的实现有不同的注册service的方式,但是获取service是方式是标准的。
public sealed class ServiceLocatorBasic : IServiceLocator { private IDictionary<Type, Type> items; public ServiceLocatorBasic() { items = new Dictionary<Type, Type>(); } public void Register<Serv, Impl>() { items.Add(typeof(Serv), typeof(Impl)); } public T Resolve<T>() { if (!items.ContainsKey(typeof(T))) { return default(T); } return (T)Activator.CreateInstance(items[typeof(T)]); } }
这是一个很简单的实现,一个需要考虑的问题是如果请求一个没有注册的service时,是否需要抛异常要根据使用场景变化。Also, this implementation does not handle the case where a registered implementation may have construction dependencies of it’s own. There are other points to consider, but they are not strictly relevant here and now.
剩下的问题是调用方如何与service locator交互,有很多方法可以保存一个service locator,并使用它的功能。一个是singleton,另一个是application variable。
public sealed class ServiceLocator { private static IServiceLocator _locator; public static void Initialize(IServiceLocator serviceLocator) { _locator = serviceLocator; } public static T Resolve<T>() { return _locator.Resolve<T>(); } }
上面代码中包含一个IServiceLocator类型
的成员,和一个Resolve方法将IServiceLocator
与ServiceLocatorBasic映射起来。还有一个initialize方法,接受和保存了一个IServiceLocator
的具体实现。ServiceLocator类不知道也不必知道具体实现是什么。
//普通方式: HttpContext.Current.Session["user"] = "George"; //使用ServiceLocator: ServiceLocator.Resolve<ISession>()["user"] = "George";
使用ServiceLocator
之后,代码不再直接使用HttpContext,只依赖于ISession接口,调用者通过接口调用。
我们只需要在应用程序开始的地方注册service,这也是唯一需要知道使用IServiceLocator接口的哪个具体实现的地方。如果是一个web application,就是在Global.asax文件的Application_Start里注册:
ServiceLocatorBasic locator = new ServiceLocatorBasic(); locator.Register<ISession, Session>(); ServiceLocator.Initialize(locator);
之后代码里就可以使用service locator,调用service了。
单元测试的一个原则是不能使用外部的服务,如数据库,文件,网络。这其中就包含了web server,session是默认保存在其内存中的。所以单元测试要使用一个假(fake)的session store。
public class SessionDummy : ISession { private IDictionary<string, object> _store; public SessionDummy() { _store = new Dictionary<string, object>(); } public object this[string key] { get { return _store[key]; } set { _store[key] = value; } } public void Remove(string key) { _store.Remove(key); } public void Clear() { _store.Clear(); } public bool Contains(string key) { return _store.ContainsKey(key); } } [TestFixture()] public class AUnitTestFixture { [SetUp()] public void TestSetUp() { ServiceLocatorBasic locator = new ServiceLocatorBasic(); locator.Register<ISession, SessionDummy>(); //register the dummy version of the session for use by code under test //register any other dummy services ServiceLocator.Initialize(locator); } [Test()] public void AUnitTestCase() { //code under test will get an ISession, the dummy implementation } }
这样我们就有一个“session”对象可以使用了,这其中并不需要web server。
总的来说,Service locator在某些场景下很有用,可以降低代码的耦合度。但是也有一些缺点,比如为了降低耦合度,引入了新的耦合。总的来说,使用Service locator可以降低代码耦合,提升可测试性。