项目框架概要
今天讲一下我所牵头的项目中用到的系统框架结构。这个框架的使用是起于权限系统的开发的,然后在数据中心中发挥出它的功效,现在包括个人空间、通知服务等项目都沿用了这种框架,这里先来一张系统框架图,然后再慢慢解说。
1、典型的三层结构,主体包含了表示层(USL)、业务逻辑层(BLL)、数据访问层(DAL)。猛然看上去好像与别的三层结构没什么不同,不过这里的不同之处是业务逻辑层和数据访问层是以接口的形式提供的,也就是说这样的业务逻辑和数据访问均可能有不同的实现。这一点对于不同的服务调用、不同的数据来源、不同的表现逻辑是很具有灵活性的。
2、 IOC容器粘合了接口组件和实现接口的不同实现模块,使它们成为一个内聚的应用程序。接口的不同实现通过配置文件在运行时进行了运态的匹配,这样保证了框架有足够的灵活性来应对可能发生的变化和不同的场景。
3、对WCF的支持。通过分离Contract、Service和Host提供清晰的逻辑结构和便捷的WCF调用,当然WCF的数据来源或者操作也是严格遵从对IBLL的调用,调用的实际匹配也是通过IOC来配置的。
4、应用数据缓存层。在IDAL层可以看到数据层接口的实现有直接访问数据库的SqlDataProvder以及应用缓存的CacheDataProvider实现,把它定位到IDAL层是为使缓存的使用和清除集中起来以便于跟踪、管理和排错,特别像个人空间这样具有大量缓存类型的情况来说,这样做就很有必要。在缓存排错过程中通过动态指定不同的数据来源能有效的提高排错的效率。
5、隔离所有的对外服务访问。在IBLL层我们看到有一个专门的Extensions组件(实际上可以有任意多个)来处理对外服务的访问,例如:BlogService、MessageService、MiniBlogService、NotifyService、PassportService、ProfileService、ResService、WebIMService、CmsService等,每一种服务都对应一个服务接口,比如:IProfileService,针对这个接口都会有ProfileServiceStrategy、ProfileServiceInvokeService、ProfileServiceInnerBuild的不同实现来对应于这个服务的“调用策略”、“实际服务调用”、“内部实现(或测试)”,具体在使用中使用哪种的实现来调用服务就取决于配置文件或者运行时来指定。因为这些服务都有调用异常或者调用超时的问题,这样的机制可以有效的选择调用策略,并且在调用的容错和排错、开发的调试和测试都十分有用。当然这其中的粘合剂也是IOC容器。
不难看出,这样的结构体现了“接口+实现+IOC”的应用,其实WCF的应用"Contract+Service+配置(Host/Client)"也与之类拟,调用服务的客户端可以通过配置或者运行时选择不同的契约实现,当然这里也可能是同一实现的不同通道。
具体是怎样应用IOC容器呢?IOC的注入有很多形式,使用方式也很灵活,如果不控制对它的使用,就会使得整个项目都充斥着对第三方组件的引用,并且如果要替换不同的容器也变得十分困难,所以这里就需要进行必要的封装,并提供一个默认实现的应用。这样调用核心ObjectFactory就出来了,任何对接口的调用都通过这个类来获取,例如:
{
IDataProvider provider = ObjectFactory.CreateIDataProvider();
provider = ObjectFactory.CreateIDataProvider("CacheDataProvider");
provider = ObjectFactory.Create<IDataProvider>("CacheDataProvider");
}
通过这样的封装,对IOC容器的使用就被抽象出来,大多数的使用者也不需要了解它的工作细节。我具体看一下ObjectFactory是怎么写的
{
private static IContainerDefaultImplement defaultImplement = null;
static ObjectFactory()
{
if (defaultImplement == null)
defaultImplement =ObjectFactory.Create<IContainerDefaultImplement>("ContainerDefaultImplement");
}
public static TInterface Create<TInterface>(Type type)
{
return Create<TInterface>(type.Name);
}
public static TInterface Create<TInterface>(string name)
{
return (TInterface)ContainerBuilder.Create()[name];
}
public static IDataProvider CreateIDataProvider()
{
return ObjectFactory.Create<IDataProvider>(defaultImplement.IDataProviderImpl);
}
public static IDataProvider CreateIDataProvider(string name)
{
return ObjectFactory.Create<IDataProvider>(name);
}
}
其实很简单,所有的CreateXXX工厂方法都调用Create<TInterface>(string name)这个泛型方法,另外,注意到有一个默认实现的配置用来指定CreateXXX()方法的默认实现到底是来源于何种实现,配置是可以外置到配置文件中,IContainerDefaultImplement的实现本身也是通过Create<TInterface>(string name)方法获取的。从这个方法中我们看到了另外一个封装ContainerBuilder,这个类是对Castle的IOC容器调用的真实封装。我们来看一下这的实现:
{
private static readonly object ton = new object();
private static IWindsorContainer container = null;
public static IWindsorContainer Create()
{
string fileName = GetFileName();
IWindsorContainer iWindsorContainer = Create(fileName);
return iWindsorContainer;
}
private static string GetFileName()
{
string subPath = ConfigurationManager.AppSettings["Components.ConfigurationPath"];
string fileName = "";
HttpContext context = HttpContext.Current;
if (context != null)
{
fileName = context.Server.MapPath(String.Format("~/{0}", subPath));
}
else
{
//支持非网站程序
fileName = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, subPath);
}
return fileName;
}
private static IWindsorContainer Create(string fileName)
{
if (container == null)
{
lock (ton)
{
if (container == null)
{
container = new WindsorContainer(new XmlInterpreter(fileName));
}
}
}
return container;
}
}
上面代码的基本流程是:a)、定位配置文件。b)、加载配置文件。c)、使用单件模式实例化WindsorContainer这样的容器对象。
对于容器的使用者来讲就只需通过WindsorContainer的索引器来获得,也可以通过ContainerBuilder.Create().Kernel.HasComponent(name)这样的方式来判断容器是否已加载了接口的对应实现,以便决定是否调用对应的索引器来获取对象。
在ObjectFactory中提到的IContainerDefaultImplement,配置了接口的默认实现,它通过容器的配置文件可进行属性注入,从而可以通过配置文件来决定默认实现,代码片段如下:
{
string IDataProviderImpl { get; set; }
}
internal class ContainerDefaultImplement : IContainerDefaultImplement
{
private string iDataProviderImpl;
public string IDataProviderImpl
{
get
{
if (String.IsNullOrEmpty(iDataProviderImpl))
return "SqlDataProvider";
return iDataProviderImpl;
}
set { iDataProviderImpl = value; }
}
}
现在来看一下配置文件是怎么写的,以下为一个代码片段:
<configuration>
<components>
<component id="ContainerDefaultImplement"
service="CSDN.XXX.Components.IContainerDefaultImplement, CSDN.XXX.Components"
type="CSDN.XXX.Components.ContainerDefaultImplement, CSDN.XXX.Components">
<parameters>
<IDataProviderImpl>CacheDataProvider</IDataProviderImpl>
</parameters>
</component>
<!-- IDataProvider -->
<component id="SqlDataProvider"
service="CSDN.XXX.IDAL.IDataProvider, CSDN.XXX.IDAL"
type="CSDN.XXX.DAL.SqlDataProvider, CSDN.XXX.DAL" />
<component id="CacheDataProvider"
service="CSDN.XXX.IDAL.IDataProvider, CSDN.XXX.IDAL"
type="CSDN.XXX.DAL.CacheDataProvider, CSDN.XXX.DAL" />
</components>
</configuration>
IDataProvider接口的默认实现通过IDataProviderImpl的属性注入进行了配置。
这里也把IDataProvider接口的两种实现SqlDataProvider和CacheDataProvider加入了容器。
对于没有加入容器的实现但又通过索引器调用了,这时会抛出异常。
在属性注入的过程中对于基本数据类型(string,int,bool等)已经内置了类型转换的方法,不需要特别处理,例如:如果要注入的属性是一个bool值,我们直接使用<IsCallback>true</IsCallback>就可以了,但是如果注入的属性是一个自定义的类型(如Observer[]等),这样就必须为这样的类型实现对应的ITypeConverter接口,我们通过一个例子来说明一下如何使用这样的高级功能:
假设现在有个Observer类,定义如下:
public class Observer
{
public string IUpdatingSenderImplement { get; set; }
}
另外,需要注入的属性是Observer的数组,比如:public Observer[] Observers { get; set; }
这样配置文件的写法可能就如:
<Observers>
<item></item>
<item></item>
</Observers>
</parameters>
但具体Observers节点怎么写呢,我们知道Observer类是一个自定义类,容器它并不知道怎样把Item里面的字符串转换为Observer对象,所以必须要在容器加载前告诉框架,有这样的类型需要转换。比较简单的方法是可以把类与xml字符串之间进行转换,在<item></item>填写Observer对象的xml字符串,然后在加载配置到容器时转换为Observer对象,要实现这样的功能,需要为这样的转换过程实现ITypeConverter接口,以下是一个例子:
{
public override bool CanHandleType(Type type)
{
return type.IsAssignableFrom(typeof(Observer));
}
public override object PerformConversion(string value, Type targetType)
{
return (Observer)Serializer.UTF8.ConvertToObject(value, targetType);
}
public override object PerformConversion(IConfiguration configuration, Type targetType)
{
return PerformConversion(configuration.Value, targetType);
}
}
重点看一下PerformConversion方法的重写,就是在这里把配置文件<item></item>里的字符串value反序列化为targetType所指定的类型(Observer)。
好了,现在可以重写一下刚才的配置文件了:
<Observers>
<item type="CSDN.Space.UpdatingService.ComponentModel.Observer">
<item>
<![CDATA[<Observer><IUpdatingSenderImplement>UpdatingSenderForUpdatingObserver1</IUpdatingSenderImplement></Observer>]]>
</item>
<item>
<![CDATA[<Observer><IUpdatingSenderImplement>UpdatingSenderForUpdatingObserver3</IUpdatingSenderImplement></Observer>]]>
</item>
</item>
</Observers>
</parameters>
注意:这里先定义了一下item所属的类型:type="CSDN.Space.UpdatingService.ComponentModel.Observer"
最后,需要在加载配置之前通知容器存在这样的类型转换。我们可以从IWindsorContainer对象的创建着手,
//添加Observer的类型转换器
IKernel kernel = iWindsorContainer.Kernel;
IConversionManager conversionManager = (IConversionManager)kernel.GetSubSystem
(SubSystemConstants.ConversionManagerKey);
conversionManager.Add(new ObserverTypeConverter());
通过引用到IConversionManager接口并为它注册新的ITypeConverter实现ObserverTypeConverter
这里就简单介绍了我们项目中所用到的系统框架,总的来说使用这样的框架使得系统具体清晰的结构、充分可扩展可配置的灵活性,同时使用接口编程也有效提供一种多人开发的模式,可以有人专门做接口定义和协调、接口测试,并且可以方便的把任务分发给不同的开发人员进行独立开发和单元测试,对于界面开发人员也可以通过实现自已的“测试实现”来提前开始界面的开发工作而不需要等待其它模块或功能的完成,只需要通过配置文件的配置来把各个分散的实现粘合在一起就可以形式一个完整的应用程序,有利于多人协助开发。