Ioc容器Autofac系列(1)-- 初窥

前言

第一次接触Autofac是因为CMS系统--Orchard,后来在一个开源爬虫系统--NCrawler中也碰到过,随着深入了解,我越发觉得Ioc容器是Web开发中必不可少的利器。那么,Ioc容器是用来做什么的?用了有什么好处?我相信如果不明白这两点就很难敞开心扉接受Ioc容器。

传统解耦设计的弊端

为方便描述,举个日志的栗子。我简化实现,一个Log类,一个SaveLog方法。如果其他类想拥有记日志功能,那么只需在内部包含一个Log类型的变量:

   public class Log
  {
        public void SaveLog(string message) {
          // save log here.
      }
  }

  public class ProductService {
      private Log _log;
      public ProductService() {
          _log = newLog();
      }

      public void SaveProduct() {
          // save product here.
          //...
          _log.SaveLog("save 1 product");
      }
  }

有经验的程序员可能会告诉你,这样做会导致其他类与Log耦合。于是,为了解耦,我们将Log类的功能抽象出来,ILog接口就产生了。如此一来,当其他类需要日志功能时,内含变量就从Log变成了ILog

public interface ILog {
        void SaveLog(string message);
    }
    public class Log:ILog
    {
        public void SaveLog(string message)
        {
            // save log here.
        }
    }

    public class ProductService
    {
        private ILog _log;
        public ProductService()
        {
            _log = newLog();
        }
        // .......
    }

由于ILog被抽象出来,它的实现类可多样化,保存为txt、xml、数据库,甚至可扩展出邮件通知功能等。基本上,我看到的国内项目里,所谓的解耦就只能走到这一步了,但这种设计真的是所谓的“灵活,易扩展,高内聚,低耦合”吗?

现在,我们来模拟需求变更。假设已有TxtLog类把日志保存成txt文件,但使用一段时间后发现:这种日志难以查询。项目经理决定将日志保存到数据库,DbLog类应运而生。但是由于整个系统充斥着new TxtLog(),转换过程实质上就是逐个查找TxtLog替换成DbLog的过程。此时,项目大小决定出错率,出错导致日志记录不全,记录不全导致系统故障后查不到日志,查不到日志导致找不到原因,找不到原因导致加班,后果太严重了。

忽然有天,高潮降临,经理或老板决定换回txt或换另外一种日志形式,原因不明,或节省成本,或体验不好,或佞臣谗言,或成心玩你,或与数据库有世仇,总之--TMD就是要换。于是,悲剧的查找替换再次上演,几番折腾,千疮百孔。

而此时此刻,你还会称赞这种设计“灵活,易扩展”吗?

迈进IoC大门--改变实例化的方式

现在我们使用Ioc容器--Autofac改进上面的代码,目标是消除代码中的new语句,把实例化类的控制权转移到别的地方,这个地方通常会在一个程序加载时只执行一次的全局方法中。

public class Global {
    public static IContainer container;
    public void Application_Start() {
        ContainerBuilder builder=newContainerBuilder();
        builder.RegisterType<Log>().As<ILog>();
        builder.RegisterType<ProductService>();
        container = builder.Build();
        var productService = container.Resolve<ProductService>();
    }
}

public class ProductService
{
    private ILog _log;
    public ProductService(ILog log)
    {
        _log = log;
    }
    // .......
}

上面代码中,ContainerBuilderIContainer是Autofac中的核心类(之后的文章中会介绍,本文不赘述)。当我们要实例化一个ProductService时,需要写如下代码:

var productService = container.Resolve<ProductService>();

没有任何跟Log有关的操作,但productService中的_log变量确已被赋值了一个Log的实例。Ioc容器会在已注册的组件(类或接口)中匹配实例化参数的类型,一旦发现该类型注册过,则自动将对应的实例赋值给该类型,这个过程叫做--构造函数注入。

回头看看那个曾经折磨过我们的TxtLogDbLog的问题,托Ioc的福,只要在那个全局方法中改一下类型就解决了。

Ioc不仅仅是控制翻转

也许你会说这个栗子有些极端,实际开发中查找替换的地方并不多,而Ioc只是给实例化换了个地方而已,为了这么一点收益却要付出巨大的学习成本,是否值得?

实际上,Ioc除了控制反转外,还提供了很多对实例生命周期的控制,本文使用的Autofac针对流行的框架(如MVC,WCF)提供了简易整合模块,以及动态代理功能。在不修改原代码的前提下,如何为类中方法添加逻辑?Orchard框架通过Autofac和DynamicProxy库设计出一种很有意思的架构让两个不相干的类的方法逻辑能合并在一起。更多细节,我会在之后的系列文章中向大家展示。

结语

5月,天降陨星,三斤大菠萝来袭,我与基友拔剑而起向邪恶宣战。如今战罢收心,准备踏实写一些文章与大家分享,希望这次能坚持久一些^_^。

 

经过上篇蜻蜓点水的介绍后,本篇通过实例快速上手autofac,展示当asp.net mvc引入了autofac之后会带来什么。

创建Asp.net MVC并引入Autofac

首先,创建一个MVC站点,为方便起见,选初始带HomeControllerAccountController的那种。然后通过NuGet或到Autofac官网下载来引入类库。个人推荐前者,因为从VS2010开始,已内集可视化的NuGet功能,使用起来非常方便。如下图所示:

 

这是vs2012的界面,点击“Manage NuGet Packages…”,弹出窗体如下,在右上角搜索框中输入“Autofac”,找到对应的库点击安装即可。这里需要应用的库有两个“Autofac”和“Autofac ASP.Net MVC3 Integration”。

 

autofac实现对Controller的自动注入

现在HomeController需要用到日志记录功能。沿用上篇ILog接口,假设目前已有一种ILog的实现类--TxtLog,放在根目录下Services文件夹中。

public class TxtLog:ILog
{
    public void Save(string message)
    {
        //save as txt
    }
}

HomeController需添加一个ILog类型变量,为了直观看到类名,我把_log的类名赋给ViewBag.LogTypeName并显示出来。代码如下:

public class HomeController : Controller
{
    private ILog _log;
    
    public HomeController(ILog log)
    {
        _log = log;
    }

    public ActionResult Index()
    {
        ViewBag.Message = "Welcome to ASP.NET MVC!";
        ViewBag.LogTypeName = _log.GetType().Name;
       
        return View();
    }

    //....

接着,在对应页面加入以下代码(抱歉这里用英文描述,因为我的代码高亮插件不能显示中文):

<h2>ILog's type name is @ViewBag.LogTypeName</h2>

此时运行程序会报错,因为HomeController找不到无参构造函数,而我们也没有给_log指定任何类型的实例。

现在我们让Autofac发挥作用,在Global文件的Application_Start方法中添加配置代码,如下:

protected void Application_Start()
{
    ......

    ContainerBuilder builder = new ContainerBuilder();
    builder.RegisterControllers(Assembly.GetExecutingAssembly());
    builder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly())
        .AsImplementedInterfaces();
    
    var container = builder.Build();
    DependencyResolver.SetResolver(new AutofacDependencyResolver(container));
}

先不管含义,加了这些代码以后,再运行程序,会发现HomeControllerILog变量被赋予了TxtLog类型的实例,而我们并没有做任何new操作,结果如下图:

 

 

autofac应对需求变更

回头再看Application_Start中配置文件的代码:

  1. builder.RegisterControllers注册了当前程序集内所有的Controller类。
  2. builder.RegisterAssemblyTypes注册了当前程序集内的所有类。

如果此刻需求改变,我们需要将所有的TxtLog换成DbLog,该怎么办?首先添加DBLog类,如下:

public class DbLog:ILog
{
    public void Save(string message)
    {
        //save to Db.
    }
}

然后在Application_Start的方法的末端处添加如下代码:

builder.RegisterType<DbLog>().As<ILog>();

运行程序,看到页面如下:

 

可看出ILog的类型已变成了DbLog。但为何要强调末端处?实际上,DBLog已经通过builder.RegisterAssemblyTypes被注册过一次,如果对同一个类型或接口注册多次(比如这里的ILog),在Autofac中会以列表的形式保存,如果只取一个,则Autofac会从列表返回最新的那个

为了证明,我们在HomeController中再加入一个变量,类型为IEnumerable<ILog>,然后将列表中所有类名显示出来,用逗号隔开。

public class HomeController : Controller
{
    private ILog _log;
    private IEnumerable<ILog> _logList;
    public HomeController(ILog log,IEnumerable<ILog> logList )
    {
        _log = log;
        _logList = logList;
    }

    public ActionResult Index()
    {
        ViewBag.Message = "Welcome to ASP.NET MVC!";
        ViewBag.LogTypeName = _log.GetType().Name;
        ViewBag.LogTypeNames = _logList.Select(x => x.GetType().Name).Aggregate((x, y) => x+","+y );
        
        return View();
    }

运行程序,页面显示如下:

 

可以看到,针对ILog接口注册了三个类型。前两个通过builder.RegisterAssemblyTypes注册,最后一个是我们手动添加。

但重复的注册类型总让人觉得不够优雅,因此实际开发中并不推荐一次性注册所有类,可条件筛选。翻开Autofac的源码,查看我们上文用到的builder.RegisterControllers会发现方法内部就是这种思路:

public static IRegistrationBuilder<object, ScanningActivatorData, DynamicRegistrationStyle>
    RegisterControllers(
        this ContainerBuilder builder,
        params Assembly[] controllerAssemblies)
{
    return builder.RegisterAssemblyTypes(controllerAssemblies)
        .Where(t => typeof(IController).IsAssignableFrom(t) &&
            t.Name.EndsWith("Controller"));
}

由代码可知,该方法筛选出所有实现IController接口且类型名以“Controller”结尾的类。你可以根据自己项目的实际情况来筛选,很多使用Autofac的开源站点也都是这么做的。

结语

未完,待续……^_^

 

你可能感兴趣的:(auto)