2012年7月28日星期六

降低代码耦合度 Service Locator

降低代码耦合度 Service Locator

原文地址: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能够减少代码之间的耦合,使代码是可测试的。

一个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

下面是定义了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类不知道也不必知道具体实现是什么。

使用Service Locator
//普通方式:HttpContext.Current.Session["user"] = "George";//使用ServiceLocator:ServiceLocator.Resolve<ISession>()["user"] = "George";

使用ServiceLocator之后,代码不再直接使用HttpContext,只依赖于ISession接口,调用者通过接口调用。

注册service

我们只需要在应用程序开始的地方注册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。

结论
  • 面向接口编程
  • 增加了一些复杂性
  • 提供了一个统一的方式来管理所有常用的依赖
  • 使测试更加容易
  • 需要一些额外的配置和注册
  • 可以通过使用factory工厂方法或者gateway模式来减少service locator的使用
  • It separates construction of dependencies from usage of same, and encourages abstraction through programming to interfaces
  • Adds some complexity by having to go through the Service Locator
  • Provides a single location to manage commonly used dependencies
  • Can make tests easier to set up by providing fake versions of dependencies, so code under test will not break when real dependencies are unavailable
  • Having to set up fake dependencies can add complexity to tests, so the testing aspect is a double-edged sword
  • Also requires some additional configuration and registration for production code
  • Want to minimize use of service locator where possible – this can be done by wrapping with factory or gateway classes
  • Can use basic implementation as shown, or implement a wrapper for one of the IoC libraries and use that for service location

总的来说,Service locator在某些场景下很有用,可以降低代码的耦合度。但是也有一些缺点,比如为了降低耦合度,引入了新的耦合。总的来说,使用Service locator可以降低代码耦合,提升可测试性。

参考
  • Inversion of Control Containers and the Dependency Injection pattern – Martin Fowler mentions the service locator and how it relates to the other two techniques
  • IServiceLocator a step toward IoC container / Service locator detente – Glenn Block writes on a recently created library that provides a common abstraction and adapters for various .NET inversion of control libraries
  • CommonServiceLocator – The project mentioned above

TAG: