Puzzle 面向服务/切面(AOP/IOC)开发框架 For .Net
Puzzle 面向服务/切面AOP开发框架 For .Net
AOP主要实现的目的是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合性的隔离效果。
日常的产品开发中最常见的就是数据保存的功能。举例来说,现在有个用户信息数据保存的功能,我们希望在数据保存前对数据进行校验,其中现在能想到的校验就包含数据完整性校验和相同数据是否存在校验。按照传统的OOP(面向对象程序设计),我们需要定义一个IUserDataSaveService(用户数据保存服务接口)以及对应的UserDataSaveServiceImpl(用户数据保存服务实现)。同时为了实现保存前数据校验的功能,需要在服务实现(UserDataSaveServiceImpl)中增加数据校验的代码。虽然我们可以通过增加数据校验接口的方式来降低耦合度,但是造成了代码的维护性不佳(假如以后需要增加其他类型的校验就需要修订现有的校验服务,造成程序的可维护性不佳)。同时在整个系统中也不可能仅有用户数据保存这一种保存服务,一定存在了各种各样不同的实现了同一保存接口(IDataSaveService)的保存服务实现,那么如何让这些保存服务实现能够有机会复用同类的数据校验,并且又能方便快速的实现各自不同的数据校验呢?这就是我开发Puzzle系统的原因,它使用AOP的思想,在面向服务设计的基础上,通过对服务中个各个功能步骤或阶段的分解,实现各个逻辑过程的分离,降低耦合,提高维护性。
讲述Puzzle系统前,我们先了解一下Puzzle系统中的一些概念。
1.首先是领域(Domain)的概念。任何一个特殊的实际应用均可被认为是一个特定的领域。
还是以保存案例进行说明,在系统中最常见的就是用户信息的保存和日志的保存。这时必然会首先定义保存功能的接口(ISaveService),并且分别在用户信息(UserInfoSaveServiceImple)和日志(LogSaveServiceImpl)中进行实现。那么很自然的用户信息(UserInfo)和日志(Log)就会被划分为一个特殊的实际应用场景,也就是领域(Domain)。如下图。
在实际业务系统中,可以根据实际业务情况对领域进行划分,在有条件的情况下,将领域划分的越细、越小,对日后功能的扩展和维护越有帮助。
2.服务的定义与实现(ServiceDefine,ServiceImplement)
使用Puzzle开发有一个基础思路是“一切皆为服务”。任何一个细小的功能都尽可能的定义为服务,从而便于此服务的扩展和被调用。
上例中的ISaveService就是一个服务的定义,而UserInfoSaveServiceImpl就是在UserInfo领域下的ISaveService实现。
简单示例如下:
public interface ISaveService { void Save(object value); } [ServiceClass(typeof(ISaveService),ServiceLifeCycle.SingleGet)] public class UserInfoSaveServiceImpl:ServiceComponent,ISaveService { public void Save(object value) { throw new NotImplementedException(); } }
所有的功能点都需要派生自服务组件类(ServiceComponent),具体原因会在后面提及。
3.事件拦截(EventInterceptor)
既然是面向切面编程,那么必然需要有提供切入程序逻辑的机会和方式。在Puzzle系统中,此功能有接口中定义事件,标准程序逻辑中抛出事件,一个或一组拦截器对抛出的事件进行拦截,从而实现对标准逻辑进行扩展或修改的目的。
Puzzle系统通过对类标记EventInterceptorClassAttribute特性标记来表明这个类是一个拦截器类。(同样拦截器类也需要派生自ServiceComponent)
同时在拦截器内部的拦截方法中,Puzzle系统提供了EventInterceptorAttribute的特性标记来标明这个方法是一个拦截方法。
using System; using Puzzle; namespace Example.EventInterceptor { [EventInterceptorClass] public class BeforeInterceptor : ServiceComponent { [EventInterceptor(typeof(IHelloWorldService), "BeforeShow")] public void Show(object sender, EventArgs e) { System.Console.WriteLine("我先说:)"); } } }
如上例:标记了BeforeInterceptor类是一个事件拦截器类,其中的方法Show拦截了IHelloWorldService接口的实现类中抛出的BeforeShow方法。BeforeShow方法的事件参数类型为EventArgs。
具体事例参见后文的“附加事件拦截示例”。
4.领域服务容器(DomainServiceContainer)
有了领域的定义和服务的定义后,就需要了解领域服务容器。从字面上就可以清楚的了解到这个容器是用来存放不同领域下的服务的定义和实现(根据服务的生命周期定义来确定是否缓存实现)。
依然使用前面的例子,当Puzzle系统启动后,Puzzle系统中此时就会存在两个相对独立的领域服务容器,分别是UserInfo和Log。在这两个服务容器中都记录了各自对ISaveService的实现定义。
//从UserInfo领域下获取ISaveService的实现(UserInfoSaveServiceImple) ISaveService userSaver = ServiceContainer.GetService<ISaveService>("UserInfo"); //从Log领域下获取ISaveService的实现(LogSaveServiceImpl) ISaveService logSaver = ServiceContainer.GetService<ISaveService>("Log");
5.功能点与功能包(FunctionPoint,FunctionPackage)
在设计时对业务进行不同的领域划分,并且在编码时对与抽象出的服务定义做出了实现后,还需要对于所有的服务进行拼装组合(就想玩拼图游戏一样)。一个服务实现就是一个功能点(FunctionPoint),一个服务领域下的功能点集合就是一个功能包(FunctionPackage)。
不同的系统中会有不同类型的功能点。Puzzle系统使用功能点特性标记(FunctionPointAttribute)来标记不同的功能点类型。
Puzzle系统默认提供了服务类型功能点(ServiceClass)和事件拦截器类型功能点(EventInterceptorClass)
功能点特性标记的详细说明和如何创建新的功能点类型会在后面提到。
使用XML文件方式来进行功能包和功能点的定义。
简单的功能包示例如下:
<?xml version="1.0" encoding="utf-8"?> <FunctionPackage xmlns="http://Puzzle"> <Name>Standard FunctionPackage</Name> <Domain>Standard</Domain> <BasePackages /> <Items> <FunctionPoint> <Name>HelloWorld</Name> <AssemblyQualifiedName>Example.ServiceClass.Console.Services.HelloWorldService,Puzzle.Example</AssemblyQualifiedName> </FunctionPoint> </Items> </FunctionPackage>
示例中的功能包定义表示该功能包所属的领域名称为“Standard”,只包含一个名称为HelloWorld的功能点,并且没有引用基础功能包。
Puzzle系统在启动后解析此功能包时会首先查询是否存在名称为“Standard”的DomainServiceContainer,如果没有会首先创建。之后会将HelloWorld功能点的定义放入该领域服务容器中,等待被调用。
6.服务的生命周期(ServiceLifeCycle)
在软件的设计开开发过程中,不可避免的会使用到单例模式(wikipedia)。那么就意味着领域服务容器中的不同服务的生命周期会有所不同。Puzzle系统中使用ServiceLifeCycle枚举标识了服务的生命周期定义。
public enum ServiceLifeCycle
SingleGet | 每次获取时重新创建 | |
Singleton | 每一领域的服务容器中只存在唯一实例 |
第二点介绍服务实现时的示例中可以看到UserInfoSaveServiceImpl被标记了ServiceClassAttribute,其中第二个参数为ServiceLifeCycle.SingleGet。此参数标识了UserInfoSaveServiceImpl这个服务实现是在每次创建时均创建新的实例(领域服务容器中不会保留该服务实现的实例,每次获取服务时均重新创建新实例)。如果将其标记为ServiceLifeCycle.Singleton则表示此服务实现的实例在领域服务容器中是一直存在的,直到所有系统被关闭回收(领域服务容器保留该服务实现的实例,每次获取到的实例均为同一个)。
7.Puzzle系统的启动和关闭
Puzzle.Application.Start()
开始运行系统(收集并解析功能包,创建领域服务容器)。
Puzzle.Application.Stop()
关闭系统,并释放系统分配的所有资源。
简单服务实现示例:
以下示例显示了如何创建了一个服务接口及对应的一个服务实现。并且如何通过功能包和功能点的配置使其在Puzzle系统中生效,及如何获取该服务实现。
首先定义服务接口及服务实现。
using System; namespace Example { public interface IHelloWorldSerivce { void ShowHelloWorld(); } } using System; using Puzzle; namespace Example.Console.Services { [ServiceClass(typeof(IHelloWorldSerivce), ServiceLifeCycle.SingleGet)] public class HelloWorldService : ServiceComponent, IHelloWorldSerivce { public void ShowHelloWorld() { System.Console.WriteLine("Hello World"); } } }
通过功能包的配置向Puzzle说明功能点及其所属的领域。
<?xml version="1.0" encoding="utf-8"?> <FunctionPackage xmlns="http://Puzzle"> <Name>Standard FunctionPackage</Name> <Domain>Standard</Domain> <BasePackages /> <Items> <FunctionPoint> <Name>HelloWorld</Name> <AssemblyQualifiedName>Example.Console.Services.HelloWorldService,Puzzle.Example</AssemblyQualifiedName> </FunctionPoint> </Items> </FunctionPackage>
启动Puzzle系统,通过ServiceContainer获取服务实现。
using System;namespace Example { public class Program { public static void Main(string[] args) { Puzzle.Application.Start(); IHelloWorldService service = Puzzle.ServiceContainer.GetService<IHelloWorldService>("Standard"); service.ShowHelloWorld(); } } } // 此示例将输出以下内容: // Hello World
附加事件拦截的示例:
在前例的基础上,当我们需要在真正的ShowHelloWorld方法进行附加操作时对其进行扩展该如何操作?
通过为实现服务增加可拦截的接口,并定义不同业务需要的拦截器即可实现相应业务需要。
以下示例显示如何在ShowHelloWorld方法输出既定内容之前和之后附加输出其他内容。
首先增加事件的接口定义。
using System; namespace Example { public interface IHelloWorldServiceEventArgs { event EventHandler BeforeShow; event EventHandler AfterShow; } }
对原有的HelloWorldService进行扩展,使其实现刚刚增加的事件定义。并且在真正输出内容的前后分别抛出对应事件。
using System; using Puzzle; namespace Example { [ServiceClass(typeof(IHelloWorldService), ServiceLifeCycle.SingleGet)] public class HelloWorldService : ServiceComponent, IHelloWorldService, IHelloWorldServiceEventArgs { public event EventHandler BeforeShow; public event EventHandler AfterShow; public void ShowHelloWorld() { if (BeforeShow != null) { this.BeforeShow(this, new EventArgs()); } System.Console.WriteLine("Hello World!"); if (AfterShow != null) { this.AfterShow(this, new EventArgs()); } } } }
此时我们就完成了对原有HelloWorldService的修改。使其有能力有机会在需要的情况下被扩展。
其次分别定义对应BeforeShow和AfterShow的事件拦截器(BeforeInterceptor,AfterInterceptor)。
using System; using Puzzle; namespace Example.EventInterceptor { [EventInterceptorClass] public class BeforeInterceptor : ServiceComponent { [EventInterceptor(typeof(IHelloWorldService), "BeforeShow")] public void Show(object sender, EventArgs e) { System.Console.WriteLine("我先说:)"); } } } using System; using Puzzle; namespace Example.EventInterceptor { [EventInterceptorClass] public class AfterInterceptor : ServiceComponent { [EventInterceptor(typeof(IHelloWorldService), "AfterShow")] public void Show(object sender, EventArgs e) { System.Console.WriteLine("我说晚了:("); } } }
接着在原有的功能包配置中增加以上两个事件拦截器的定义
<?xml version="1.0" encoding="utf-8"?> <FunctionPackage xmlns="http://Puzzle"> <Name>Standard FunctionPackage</Name> <Domain>Standard</Domain> <BasePackages /> <Items> <FunctionPoint> <Name>HelloWorld</Name> <AssemblyQualifiedName>Example.EventInterceptor.HelloWorldService,Puzzle.Example</AssemblyQualifiedName> </FunctionPoint> <FunctionPoint> <Name>BeforeInterceptor</Name> <AssemblyQualifiedName>Example.EventInterceptor.BeforeInterceptor,Puzzle.Example</AssemblyQualifiedName> </FunctionPoint> <FunctionPoint> <Name>AfterInterceptor</Name> <AssemblyQualifiedName>Example.EventInterceptor.AfterInterceptor,Puzzle.Example</AssemblyQualifiedName> </FunctionPoint> </Items> </FunctionPackage>
最后不用更改原有的服务调用方法,即可发现输出内容有所变化
using System; namespace Example { public class Program { public static void Main(string[] args) { Puzzle.Application.Start(); IHelloWorldService service = Puzzle.ServiceContainer.GetService<IHelloWorldService>("Standard"); service.ShowHelloWorld(); } } } // 原示例将输出以下内容: // Hello World // 此示例将输出以下内容: // 我先说:) // Hello World! // 我说晚了:(
继承基础功能包中提供的功能点
功能包默认会继承所有其指定的基础功能包中的功能点。
例如,基础功能包BasePackage
<?xml version="1.0" encoding="utf-8"?> <FunctionPackage xmlns="http://Puzzle"> <Name>BasePackage</Name> <Domain>Base</Domain> <BasePackages /> <Items> <FunctionPoint> <Name>HelloWorld</Name> <AssemblyQualifiedName>Example.ServiceClass.Console.Services.HelloWorldService,Puzzle.Example</AssemblyQualifiedName> </FunctionPoint> </Items> </FunctionPackage>
继承基础功能包BasePackage的InheritPackage功能包
<?xml version="1.0" encoding="utf-8"?> <FunctionPackage xmlns="http://Puzzle"> <Name>InheritPackage</Name> <Domain>Inherit</Domain> <BasePackages> <Name>BasePackage</Name> </BasePackages> <Items /> </FunctionPackage>
InheritPackage功能包并未标记任何功能点,只是标记了继承了BasePackage。此时在Puzzle系统中,Inherit领域下将自动包含BasePackage中定义的HelloWorld功能点。
using System; namespace Example { public class Program { public static void Main(string[] args) { Puzzle.Application.Start(); IHelloWorldService service = Puzzle.ServiceContainer.GetService<IHelloWorldService>("Inherit"); service.ShowHelloWorld(); } } } // 示例将输出以下内容: // Hello World
重写或隐藏基础功能包中提供的功能点
以下示例显示当前功能能包中如何定义功能点来重写或隐藏基础功能包所提供的功能点。
例如:功能包BasePackage中有HelloWorld功能点定义:
<?xml version="1.0" encoding="utf-8"?> <FunctionPackage xmlns="http://Puzzle"> <Name>BasePackage</Name> <Domain>Base</Domain> <BasePackages /> <Items> <FunctionPoint> <Name>HelloWorld</Name> <AssemblyQualifiedName>Example.ServiceClass.Console.Services.HelloWorldService,Puzzle.Example</AssemblyQualifiedName> </FunctionPoint> </Items> </FunctionPackage>
功能包CoverPackage中引用了BasePackage功能包作为基础功能包
<?xml version="1.0" encoding="utf-8"?> <FunctionPackage xmlns="http://Puzzle"> <Name>CoverPackage</Name> <Domain>Cover</Domain> <BasePackages> <Name> BasePackage </Name> </BasePackages> <Items> <FunctionPoint> <Name>HelloWorld</Name> <Enable>false</Enable> <AssemblyQualifiedName>Example.ServiceClass.Console.Services.HelloWorldService,Puzzle.Example</AssemblyQualifiedName> </FunctionPoint> </Items> </FunctionPackage>
功能包CoverPackage中的功能点HelloWorld重写了功能包BasePackage中的功能点HelloWorld,由于功能包CoverPackage中的HelloWorld功能点的Enable属性为false。 表示着领域CoverPackage下将不再提供基础功能包BasePackage提供的功能点HelloWorld。
通过 AssemblyQualifiedName 属性是否相同来确认是否为相同的功能点。
此时可以增加另外一个FunctionPoint节点,写入不同的IHelloWorldService接口的实现的AssemblyQualifiedName,从而实现复写BasePackage中提供的HelloWorldService接口实现的目的。
功能点之间的依赖关系
功能点之间的依赖关系由FunctionPoint对象的Dependency属性进行标记。
Dependency属性记录该功能点依赖的功能点的Name。用来标记其需要在依赖功能点执行完成后执行。
以前文提到过的事件依赖器的例子继续衍生,增加BeforeShow的事件拦截器(BeforeInterceptorInFirst)
using System; using Puzzle; namespace Example.EventInterceptor { [EventInterceptorClass] public class BeforeInterceptorInFirst : ServiceComponent { [EventInterceptor(typeof(IHelloWorldService), "BeforeShow")] public void Show(object sender, EventArgs e) { System.Console.WriteLine("沙发!!"); } } }
修改功能包文件,增加BeforeInterceptorInFirst功能点,并将BeforeInterceptor功能点的Dependency标记为BeforeInterceptorInFirst功能点的Name
<?xml version="1.0" encoding="utf-8"?> <FunctionPackage xmlns="http://Puzzle"> <Name>Standard FunctionPackage</Name> <Domain>Standard</Domain> <BasePackages /> <Items> <FunctionPoint> <Name>HelloWorld</Name> <AssemblyQualifiedName>Example.EventInterceptor.HelloWorldService,Puzzle.Example</AssemblyQualifiedName> </FunctionPoint> <FunctionPoint> <Name>BeforeInterceptor</Name> <Dependency>BeforeInterceptorInFirst</Dependency> <AssemblyQualifiedName>Example.EventInterceptor.BeforeInterceptor,Puzzle.Example</AssemblyQualifiedName> </FunctionPoint> <FunctionPoint> <Name>AfterInterceptor</Name> <AssemblyQualifiedName>Example.EventInterceptor.AfterInterceptor,Puzzle.Example</AssemblyQualifiedName> </FunctionPoint> <FunctionPoint> <Name>BeforeInterceptorInFirst</Name> <AssemblyQualifiedName>Example.EventInterceptor.BeforeInterceptorInFirst,Puzzle.Example</AssemblyQualifiedName> </FunctionPoint> </Items> </FunctionPackage>
可以看到XML文件中功能点定义的顺序并不会影响到功能点在系统中依赖关系的确定。
最后不用更改原有的服务调用方法,即可发现输出内容有所变化
using System; namespace Example { public class Program { public static void Main(string[] args) { Puzzle.Application.Start(); IHelloWorldService service = Puzzle.ServiceContainer.GetService<IHelloWorldService>("Standard"); service.ShowHelloWorld(); } } } // 原示例将输出以下内容: // 我先说:) // Hello World! // 我说晚了:( // 此示例将输出以下内容: // 沙发!! // 我先说:) // Hello World! // 我说晚了:(
功能点特性标记(FunctionPointAttribute)
前文提到过,所有的功能点均由其对应的功能点特性标记来进行说明。
Puzzle系统在启动时,首先查找所有可用的功能包。在完成查找和整理所有待处理的功能包的依赖关系后,会自动开始顺序解析功能包。
解析功能包前由会首先按照前文提到的功能点之间的依赖关系及功能点的优先级对当前功能包中所有的功能点的依赖关系进行整理,整理完成后顺序解析功能点。
解析功能点时就会首先解析功能点代码中标记的功能点特性标记,根据不同的功能点特性标记中提供的功能点解析器对功能点进行解析。
Puzzle系统在解析功能点时默认将ServiceClass类型的功能点优先级排为最高,其次为EventInterceptor类型的功能点。其他类型功能点优先级为正常。
如果需要在不同的业务系统中按照增加功能点类型,只需要在FunctionPointAttribute基础上增加派生类,并增加对应的IFunctionPointParse实现即可。
由于时间有限,本篇文章和Puzzle系统都暂时还只是一个雏形和只实现了最基础功能的AOP框架,后期还会不间断的进行维护和扩展。欢迎广大看客提出宝贵意见!
Alpha 1.1 版本