前言:也看过一些国内介绍SM的文章,但还是老外这篇更详尽通俗。这是偶翻译的第一篇文章,错误不当之处请不吝赐教。总觉得一个第三方框架,要想成功一定要有个 响亮的名字。四年前刚接触JQuery时,就认为它一定能流行起来,因为名字叫起来明显比其它框架(Prototype/Moo Tool/Ext JS)印象深刻。StructureMap也是如此,妙得是缩写SM。IoC以前,我们常被纷繁的业务逻辑SM得痛不欲生,用StructureMap, 底层的码农看到了翻身的希望,可以SM这些业务逻辑了。
我在30岁生日之际,对斯德哥尔摩EPiServer(1)组织(2),作过一场关于IOC容器-StructureMap(简称SM)的演讲。该演讲以及这篇文章的重心,不是深入IoC是什么以及它试图解决什么(已经被写过无数次了,你有兴趣的话可以看维基百科,Martin Fowler, Joel Abrahamsson, 还有StackOverflow上的文章)。我在这里要关注的是,StructureMap能为你做什么,你怎么着手做这些。
TLDR(3): 这篇文章相当长,所以如果你更像是那种编译一下就运行的人,就直接把页面滚动到底下载示例解决方案吧。
一个简单服务类:
假设你最近开始看SOLID(4)原则,特别是依赖反转,它可能产生如下的一个代码结构:
这样的话,我们的服务对验证和存储的抽象产生了一个构造函数依赖。存储的具体实现本身,依赖于一些配置和日志记录器。日志记录器的具体实现又依赖于其它一些抽象。当我们说具体实现时,比如说,存储的依赖于配置和日志记录意味着代码是这样:
public class Repository : IRepository { public Repository (IConfiguration configuration, ILogger logger) { } }
创建一个我们服务的实例要像这样:
var service = new Service(new Validator(), new Repository(new Configuration(), new Logger(new AmAnotherNeccessaryButAtThisPointAnnoyingToTypeDependency())));
这就是手动依赖注入,意思正如它听上去的,我们正在手动地把依赖注入的我们的服务中。如果你有兴趣了解更多使用IoC容器的利弊,而不是如何使用的话,我想此篇文章作了一个很好的总结。
IoC容器
已经出现了许多IoC容器,我头脑中首先想到的是StructureMap, Unity, Ninject 和Castle Windsor。说实话我没有对不同容器间作透彻地比较。我就从StructureMap开始的,我还没有找到理由要换一个。我的理解是,所有这些框架的特性都相似,只是语法等不同。
StructureMap和语法
说到StructureMap的语法,知道两件事会不错:官方文档(链接在上面)覆盖面很广,可它基于旧一点的版本。这意味着如果你用一个较新的版本(我想2.5以上,本文中我在用2.6.1),你会发现许多方法找不到或过时了。另一件事是,这个框架大量地使用了多种Lambda表达式,所以如果你不太熟悉那些,当你刚开始用时,也许乍看上去它有些怪异。
基本配置和自动装配
撇开语法,我们要做的基本的事是,给SturctureMap一些抽象,还有一些说明以使它能决定在实际中我们想用什么具体实现。因此,还是用同样用上面的服务做例子,当我们谈到IConfiguration抽象时,我们实际想用Configuration这个具体的类。
添加一个SturctureMap.dll的引用后,最简单地配置它的形式是像这样:
ObjectFactory.Configure(x => x.For<IConfiguration>().Use<Configuration>());
当我们想创建具体实现时,只要向容器发请求:
var config = ObjectFactory.GetInstance<IConfiguration>();
如果我们在配置如何由抽象创建类型的具体实现时失败了,SM(作者终于用缩写了)会抛出一个很友好的异常:
为了调试目的,想要详细了解容器内部发生了什么,也有很棒的方式,可以像这样调用WhatDoIHave(顾名思义)方法:
var what = ObjectFactory.WhatDoIHave();
很容易想到,要获取我们的服务(配置我们需要的依赖项后),我们会写这样的代码:
var service = new Service( ObjectFactory.GetInstance<IValidator>(), ObjectFactory.GetInstance<IRepository>());
使用(IoC)容器并不推荐这种方式。我们这里做的(指上面的代码)多少有些像我一开始展示的代码,而我们已经把我们的服务绑定到我们的容器了。这样不好,我们已经开始用依赖反转摆脱我们代码中对其它部分具体实现的依赖。我们想用自动装配的概念来让SM自己配置所有的依赖。
假设当我们向SM要我们的服务(类实例)时,它会发现最贪婪(其有最多的参数)的构造函数。在这个构造函数中,发现需要一个IValidator和IRespository(的参数)。它会查看配置,看到我们已经告诉它使用具体的类Validator和Repository. 当它尝试创建Repository时,看到又需要一个IConfiguration和一个ILogger,它又查看与之相关的配置,如此下去,直到所有的依赖被解析。所以,要得到一个Service实例我们只要这么做:
var service = ObjectFactory.GetInstance<Service>();
自动装配和Web框架
不幸的是,对我们EPiServer开发者来说,WebForms在IoC方面没有得到很好的支持,采用MVC的话会让使用IoC容易得多。
原因是我们可以控制Controller的创建(5). 既然逻辑是在Controller中,这打下个很棒的基础,从而调用你的GetInstance方法,并调整自动装配而不必在你的代码中引用SM。即使你不能控制WebForm程序的Page创建,你也有办法克服-用Setter注入(参考我的文章),只是不是那么优雅。
程序集扫描和约定
如果看下我们的服务配置:
ObjectFactory.Configure(x => { x.For<IValidator>() .Use<Validator>(); x.For<IAmAnotherNeccessaryButAtThisPointAnnoyingToTypeDependency>() .Use<AmAnotherNeccessaryButAtThisPointAnnoyingToTypeDependency>(); x.For<IRepository>() .Use<Repository>(); x.For<IConfiguration>() .Use<Configuration>(); x.For<ILogger>() .Use<Logger>(); });
可以看到一个新兴的模式。我们有某种形式的抽象,叫做ISomething,还有一个具体实现叫做Something。这种(约定)非常流行,围绕它形成了SM的默认约定。这样的话,为了让它更容易以跳过许多枯燥打字工作,我们可以让SM扫描程序集,并添加所有遵循这种约定的的抽象/实现。
ObjectFactory.Configure(x =>
{
x.Scan(a =>
{
a.AssembliesFromApplicationBaseDirectory();
a.WithDefaultConventions();
});
});
我们肯定可以控制想让哪些程序集被扫描。在上面的示例中,我们扫描所有在程序根目录(在Asp.Net程序中应该是bin目录)下的所有程序集。我们也可以针对某一个程序集,或正在调用程序集,或筛选以某个字符串开始的程序集……没错,无所不能。
要是你有某种命名方面的编码规范,也可以创建自己的约定。
注册DSL和引导
目前为止我们看到的示例中已经使用了SM注册DSL(6),推荐这种方式进行配置。通常通过一个引导器和继承注册类实现。
大约在你的程序入口点(比如在Asp.Net程序中的Application_Start),调用你的引导器,用来依次查看所有的注册并添加任何在它们中配置过的东西。
创建一个注册和从Registry类继承一样简单。我们已经看到上面的例子中,在类构造函数中能访问Registry的API。这(方式)有利于将相关联的依赖分组在一个注册中,并赋注册相应的命名。
假如我们想创建一个处理日志记录的注册:
public class LogRegistry : Registry { public LogRegistry() { For<ILogger>() .Use<Logger>(); For<IAmAnotherNeccessaryButAtThisPointAnnoyingToTypeDependency>().Use <AmAnotherNeccessaryButAtThisPointAnnoyingToTypeDependency>(); } }
我们的引导器就像这样:
public class StructureMapBootrstrapper { public static void Bootstrap(IContainer container) { container.Configure(x => { x.Scan(a => { a.AssembliesFromApplicationBaseDirectory(); a.LookForRegistries(); }); }); } }
要注意我们传递了一个IContainer实例(到Bootstrap方法)。我们本可以直接使用ObjectFactory(展示主要容器的一个简单静态门面),但我们想令对具体类的依赖最少。
在程序的入口点,只要调用我们的引导器并传递给它我们用的容器。
StructureMapBootrstrapper.Bootstrap(ObjectFactory.Container);
在这个例子中,既然那些注册(和我们之前见到其他的一样)本可以通过默认约定进行注册,我们可能还不需要一个LogRegistry。通常你会尽可能地使用默认约定,而对于不符约定的情况进行手动注册(使用注册类)。
实现转换
对我来说,关于IoC容器最棒的一件事是,很容易改变你的程序行为而不用直接修改代码。那只是一个配置的问题。让我们看下面的例子:我们的程序用下面的设置通过SMTP发送邮件。
public interface ISendMailMessage { void SendMessage(MailMessage message); }
public class SendMailMessageViaSmtp : ISendMailMessage { public void SendMessage(MailMessage message) { using (var client = new SmtpClient("mail.somecompany.com")) { client.Send(message); } } }
public class MailRegistry : Registry { public MailRegistry() { For<IMailService>().Use<MailService>(); For<ISendMailMessage>().Use<SendMailMessageViaSmtp>(); } }
当我们在本机开发时,可能没有对服务器的访问权,或因某个其他原因不想发邮件。我们当然可以用多种技巧(hacks),比如在SendMailMessageViaSmtp类中注释/取消注释代码,但这最后很少能产生一个坚实的解决方案。
使用剖面
有几种更好的方式,其中一个是使用SM中剖面(Profile)。这样可以做到为不同的剖面创建不同的配置。对于我们的开发机器,我们已经创建另一个实现(7),它会将邮件记录到一个文本文件中。
public class LogMailMessage : ISendMailMessage { public void SendMessage(MailMessage message) { using(var writer = new StreamWriter("MailLog.txt", true)) { writer.WriteLine( "Sending mailmessage from {0} to {1} with subject {2}", message.From.Address, message.To, message.Subject ); } } }
在我们的MailRegistry中,我们创建一个叫作DEBUG的剖面,指引SM在这个剖面下使用上面这个类:
For<ISendMailMessage>().Use<SendMailMessageViaSmtp>(); Profile("DEBUG", profile => { profile.For<ISendMailMessage>().Use<LogMailMessage>(); });
接着再任意用一种我们已知的逻辑,在合适时切换剖面:
#if DEBUG ObjectFactory.Profile = "DEBUG"; #endif
使用条件逻辑
另一种做到切换实现的方式是在配置中使用条件。因此,先不提是否要在debug模式下构建程序,假设我们想基于机器名字切换Mail实现,使用条件语句,我们可以在下面的配置中做到:
ObjectFactory.Configure(x => { x.For<ISendMailMessage>() .ConditionallyUse(c => { c.If(context => Environment.MachineName.Equals("CNNOTE41")) .ThenIt .Is .ConstructedBy(by => new SendMailMessageViaSmtp()); c.TheDefault .Is .ConstructedBy(by => new LogMailMessage()); }); });
原生构造函数参数
在一些构造函数中,你可能对某些原生类型有依赖(string, int等)。其中一个经典的例子是某种存储类会带一个连接字符串作为构造函数参数。如果我们暂时忽略,在架构观点上看这可能不是个好主意,那你使用SM的话该怎么做?
其实还是很简单,就像回答SM的问题,说我们想用什么参数。来给出这个存储类:
public class Repository : IRepository { private string connectionString; public Repository(string connectionString) { this.connectionString = connectionString; } public string GetConnectionString() { return connectionString; } }
我们可以把使用连接字符串的选择,通过Ctor方法注入。
ObjectFactory.Configure(x => { x.For<IRepository>() .Use<Repository>() .Ctor<string>() .Is("Stefans connectionstring"); });
因为我们的类只有一个构造函数包含string类型参数,SM能够推算出匹配的那个。要是有两个,我们将不得不指定哪个值属于哪个参数。要这样你只须在Ctor方法中指定一个名字。下面代码产生的会和上面一样,只是显示地指定了该设置哪个构造函数参数。
ObjectFactory.Configure(x => { x.For<IRepository>() .Use<Repository>() .Ctor<string>("connectionString") .Is("Stefans connectionstring"); });
列表和添加所有类型实现
设想我们在构建我们框架的一个插件部分,这里第三方程序集想让我们处理它们与一个特定接口相关的代码。这个例子多少有些不自然,是一个HelloService,它获取ISayHello(实现)的列表,并简单地循环以执行其中每个的hello方法。
public class SayHelloService { private List<ISayHello> helloList; public SayHelloService(List<ISayHello> helloList) { this.helloList = helloList; } public void SayHello() { helloList.ForEach(x => Console.Out.WriteLine(x.Hello())); } }
public interface ISayHello { string Hello(); }
我们可以指引SM扫描程序集,通过某种逻辑,来添加它找到的实现某个接口的全部类型。
public class SayHelloRegistry : Registry { public SayHelloRegistry() { Scan(a => { a.AssembliesFromApplicationBaseDirectory(); a.AddAllTypesOf<ISayHello>(); }); } }
因此要是我们有两个ISayHello的具体实现(8):
public class SayHelloInEnglish : ISayHello { public string Hello() { return "Hello"; } } public class SayHelloInChinese : ISayHello { public string Hello() { return "你好"; } }
SM聪明到可以推算出,既然我们对ISayHello列表(9)有依赖,那就传递所有它能在配置找到的实现。所以,只要第三方想用你的插件系统来实现一个协商好的接口,你扫描他们的程序集,(这些实现)就会被自动发送给你。
就这多了吧?
SM肯定有比我写的更多的内容,不过这些部分是我目前在自己项目中最常用的。
示例解决方案
我做了一个VS2010的解决方案,包含上面列出的多种SM特性的例子。你可以在这里下载。
原文:http://world.episerver.com/Blogs/Stefan-Forsberg/Dates/2010/8/An-introduction-to-StructureMap/
备注:
1) EPiServer: 是一个公司,提供强壮、灵活和可高度定制的解决方案,支持和管理公司在四个重要领域的在线展示–内容管理、社区和社会媒体、商业和通信(官方网站)。
2) 这里翻译成组织,原词是Meetup,指帮助全球各地进行线下小组会议的一个在线社交网络入口(参考)。
3) Too long, didn’t read的缩写(太长没有看)。
4) 面向对象设计五原则的缩写,参考http://en.wikipedia.org/wiki/SOLID。
5) 原文参考链接。园子里也有几篇:麒麟的Asp.net MVC2中你必须知道的扩展点(一):Controller Factory,杜总的当ASP.NET MVC爱上IoC,老赵的支持Area的ControllerFactory。
6) DSL是Domain Specific Language的缩写。
7) 同样继承ISendMailMessage。好像接口名字里的Message有些累赘,叫ISendMail就好。