.NET面向上下文、AOP架构模式(实现)
1.上下文Context、面向切面编程AOP模型分析
在本人的“.NET面向上下文、AOP架构模式(概述)”一文中,我们大概了解了上下文如何辅助对象在运行时的管理。在很多时候我们急需在运行时能把对象控制在一定的逻辑范围内,在必要的时候能让他们体现出集中化的概念,如人群、车辆、动物等等。而Context与AOP有着密切的联系,Context表示逻辑抽象的范围而AOP描述了在这个逻辑范围内如何进行控制。其实这两者都是设计模式外的设计模式,与具体的技术实现无关。[王清培版权所有,转载请给出署名]
那么Context与AOP两者在逻辑上是一个怎样的概念呢?似乎只有图才能最贴切的表达人的理解思路。下图展现Context与AOP紧密合作的概念模型。
Context图:1
对象在运行时被上下文管理,在上下文中可以很方便的获取到所有的受管理的对象,这为后面的AOP做了铺垫。只有Context启动后AOP管理器的爪子才能伸进对象的运行时内部与AOP的连接点建立起通讯关系,才能真正的使对象能面向切面成功。
在模型图中,Context中心负责对所有Object进行管理,而Object体现出多面性属性、多面性行为都将包括多面性的特点,通过与AOP管理器进行连接将控制对象的运行时行为。
AOP图:2
通过合理的约定对象的AOP抽象接口,尽可能的最大化将控制权移动到客户所实现的“面”中去。比如对某类方法的调用,可能需要尽可能的控制方法的所有执行权。所以对具体的抽象定义有很大的难度。
在上图中,我们通过AOP核心管理器与对象的“面”连接上,根据具体的“面”类型进行动态调用,比如属性,可能需要在运行时根据业务环节进行呈现、动态绑定等等,都可以通过AOP去实现。对于方法,可能在面向SOA的架构中需要记录下当前客户端的调用信息,还有一些独特的业务认证等等。不同的“面”需要进行逻辑抽象,这也符合面向对象的设计原则。很多时候我们需要先“约定”而不是尽可能的提供扩展的机制,扩展点越多系统的复杂程度就越大,相对也就难以控制。
2.上下文的实现
对上下文、AOP模型我们大致分析了一下,通过模型图也很形象的体现出上下文、AOP的主要的工作原理。下面我们来通过具体的分析来搭建物理的代码模型。
面向对象是对抽象的逻辑模型进行物理性的建模,能否真正的体现出模型,需要我们细心的分析才行。
2.1上下文模型实现
我们对上下文模型进行抽象实现为代码模型。那么我们需要一个逻辑上代表上下文的对象,在这里我将它命名为ContextRuntime,上下文的生命周期控制在Using()语句的代码段中,那么ContextRuntime需要实现Idisposable接口,让Using()语句能起作用。
using (ContextModule.ContextRuntime.BeginContextRuntime())
{ //上下文的生命周期
}
为什么要这样设计上下文的生命周期呢,这样设计是最为灵活的。在诸如很多微软的内部上下文生命周期的入口也是这样设计的,最经典的就是System.Transaction事务处理。
当Using()代码段结束后,我们需要释放当前上下文实例,所以我们需要完成IDisposable接口的实现。
void IDisposable.Dispose()
{
_currentContextRuntime = null;
}
_ currentContextRuntime表示上下文对象实例的全局私有对象。
由于多线程应用框架的入口点不是我们所能控制的,所以在使用上下文模式的时候需要使用线程本地存储解决线程不安全访问的问题。
[ThreadStatic]
private static ContextRuntime _currentContextRuntime;
我们看一下全部的ContextRuntime对象代码:
[csharp]
/***
* author:深度训练
* blog:http://wangqingpei557.blog.51cto.com/
* **/
using System;
using System.Collections.Generic;
using System.Text;
namespace ContextModule
{
/// <summary>
/// 上下文运行时环境。
/// 上下文逻辑运行时环境,环境中的功能都是可以通过附加进来的。
/// </summary>
public class ContextRuntime : IDisposable
{
#region IDisposable成员
void IDisposable.Dispose()
{
_currentContextRuntime = null;
}
#endregion
protected ContextRuntime() { }
private DateTime _initTime = DateTime.Now;
/// <summary>
/// 获取运行时创建上下文的时间
/// </summary>
public virtual DateTime InitTime { get { return _initTime; } }
private Dictionary<object, object> _runTimeResource = new Dictionary<object, object>();
private ContextFilterHandlerMap _filterMap = new ContextFilterHandlerMap();
/// <summary>
/// 获取上下文中的方法、类过滤器映射表
/// </summary>
public ContextFilterHandlerMap FilterMap { get { return _filterMap; } }
private Guid _initPrimaryKey = Guid.NewGuid();
/// <summary>
/// 获取运行时创建上下文的唯一标识
/// </summary>
public virtual Guid InitPrimaryKey { get { return _initPrimaryKey; } }
/// <summary>
/// 获取上下文共享区域中的数据
/// </summary>
/// <param name="key">数据Key</param>
/// <returns>object数据对象</returns>
public virtual object GetValue(object key)
{
return _runTimeResource[key];
}
/// <summary>
/// 设置上下文共享区域中的数据
/// </summary>
/// <param name="key">数据Key</param>
/// <param name="value">要设置的数据对象</param>
public virtual void SetValue(object key, object value)
{
_runTimeResource[key] = value;
}
[ThreadStatic]
private static ContextRuntime _currentContextRuntime;
/// <summary>
/// 获取当前上下文运行时对象.
/// </summary>
public static ContextRuntime CurrentContextRuntime { get { return _currentContextRuntime; } }
/// <summary>
/// 开始运行时上下文
/// </summary>
/// <returns>ContextRuntime</returns>
public static ContextRuntime BeginContextRuntime()
{
//可以通过配置文件配置上下文运行时环境的参数。这里只是实现简单的模拟。
_currentContextRuntime = new ContextRuntime();
return _currentContextRuntime;
}
}
}
这里只为了实现基本的模型原型,不会涉及太多的功能。上下文主要是在当前线程中开启,然后保持在静态对象的多线程安全访问,最后就是对象的稳定释放。
2.2上下文对象绑定实现
有了上下文之后,如何才能使对象在运行时动态的绑定到上下文中来。这个需要在前期编码的时候就确定对象是否要绑定到当前上下文以便进行管理。
那么我们需要对客户使用的对象进行一个抽象,让所有需要绑定的对象实现我们高层定义的抽象。这里我将命名为ContextModuleBaseObject,由于需要向AOP提供对象的“面”的连接点,所以我们需要在运行时反射获取到绑定对象的一些基本信息,如:属性的“面”、行为的“面”、包括对象本身的“面”,这些面我们需要将其对应关系建立起来才能让后面的AOP起作用。
所以我们将ContextModuleBaseObject定义为泛型类,并且需要加上Class的约束。对于绑定的对象在运行时一旦进入上下文的生命周期管理,就需要灵活的表示为ContextRuntime对象,这样设计符合上下文一视同仁的观念,更便于ContextModuleBaseObject对象在运行期动态的使用上下文的内部存储区域。[王清培版权所有,转载请给出署名]
这里我们需要将ContextModuleBaseObject 对象继承自ContextRuntime对象。使用经典的装饰者模式,让ContextModuleBaseObject 对象使用ContextRuntime 行为。
[csharp]
/***
* author:深度训练
* blog:http://wangqingpei557.blog.51cto.com/
* **/
using System;
using System.Collections.Generic;
using System.Text;
using System.Reflection;
namespace ContextModule
{
/// <summary>
/// 上下绑定基类,强制派生类绑定到上下文。
/// 逻辑上下文的策略构造都在这里进行。
/// </summary>
/// <typeparam name="T">受管理的上下文绑定对象类型,通常是ContextModuleBaseObject派生类。</typeparam>
public class ContextModuleBaseObject<T> : ContextRuntime where T : class
{
/// <summary>
/// 当前上下文绑定对象所绑定到的上下文物理对象实例。
/// </summary>
private ContextRuntime _contextRunTime;
public ContextModuleBaseObject()
{
if (typeof(T).GetCustomAttributes(typeof(ContextEveningBoundAttribute), false) != null)
{
_IsEvening = true;
return;
}
//前期静态绑定上下文
if (ContextRuntime.CurrentContextRuntime == null)
throw new Exception("上下文环境未能初始化,请检查您的代码入口是否启用了ContextRuntime对象。");
_contextRunTime = ContextRuntime.CurrentContextRuntime;
_InitContextHandler<T>();
}
/// <summary>
/// 构造上下文的类过滤器、方法过滤器映射表。
/// </summary>
private void _InitContextHandler<ChildType>() where ChildType : class
{
//构造类过滤器
ContextOperationBaseAttribute[] classattr =
typeof(ChildType).GetCustomAttributes(typeof(ContextOperationBaseAttribute), false) as ContextOperationBaseAttribute[];
if (classattr.Length > 0)
{
ContextOperationBaseAttribute joinoper = _JoinOperation(classattr);
_contextRunTime.FilterMap.MapOperation(typeof(T).FullName, joinoper);
}
//构造方法过滤器
foreach (MethodInfo method in typeof(ChildType).GetMethods())
{
ContextOperationBaseAttribute[] methodattr =
method.GetCustomAttributes(typeof(ContextOperationBaseAttribute), false) as ContextOperationBaseAttribute[];
if (methodattr.Length <= 0)
continue;
ContextOperationBaseAttribute joinoper = _JoinOperation(methodattr);
_contextRunTime.FilterMap.MapOperation(string.Format("{0}.{1}", method.DeclaringType.FullName, method.Name), joinoper);
}
}
internal bool _IsEvening { get; set; }
/// <summary>
/// 后期动态绑定上下文。
/// </summary>
internal void _EveningBoundChildClass<ChildType>() where ChildType : class
{
if (_contextRunTime != null)
return;//说明之前已经进行过动态调用
_contextRunTime = ContextRuntime.CurrentContextRuntime;//动态绑定当前运行时上下文
_InitContextHandler<ChildType>();
}
private ContextOperationBaseAttribute _JoinOperation(ContextOperationBaseAttribute[] operationarray)
{
//必须对数组进行排序后才能连接
for (int i = 0; i < operationarray.Length; i++)
{
for (int j = 0; j < i; j++)
{
if (operationarray[j].OperationSort > operationarray[j + 1].OperationSort)
{
ContextOperationBaseAttribute oper = operationarray[j];
operationarray[j] = operationarray[j + 1];
operationarray[j + 1] = oper;
}
}
}
ContextOperationBaseAttribute opernext = operationarray[0];
for (int i = 1; i < operationarray.Length; i++)
{
opernext.NextOperation = operationarray[i];
opernext = operationarray[i];//保持对当前循环对象的上级对象的引用。
}
return operationarray[0];
}
public MethodInfo GetMethodInfo(string methodname)
{
return this.GetType().GetMethod(methodname);
}
public override Guid InitPrimaryKey
{
get
{
return _contextRunTime.InitPrimaryKey;
}
}
public override DateTime InitTime
{
get
{
return _contextRunTime.InitTime;
}
}
public override object GetValue(object key)
{
return _contextRunTime.GetValue(key);
}
public override void SetValue(object key, object value)
{
_contextRunTime.SetValue(key, value);
}
}
}
ContextModuleBaseObject 类主要实现的功能就是将对象动态的添加到当前上下文中。然后为AOP做些辅助性的工作,包括对类、属性、行为的特性元数据的缓存,这里只实现了行为的特性缓存。可以根据自己的需要扩展AOP的功能,在对象的属性上标记特性让属性也发挥作用。这里的特性就是AOP公布的指定接口。
对_JoinOperation方法的解释我们留在后面,这里是一个数据结构。将ContextOperationBaseAttribute
类型串成链表,让方法的执行穿过所有的ContextOperationBaseAttribute处理类。
2.3上下文对象的后期绑定实现
为了让绑定对象支持上下文后期绑定,需要一个特性作为表示。
[csharp]
/***
* author:深度训练
* blog:http://wangqingpei557.blog.51cto.com/
* **/
using System;
using System.Collections.Generic;
using System.Text;
namespace ContextModule
{
/// <summary>
/// 确定设置类是否需要后期动态绑定到上下文。
/// 使用该特性的类将是上下文活跃的,只有在使用的时候才确定当前上下文。
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public class ContextEveningBoundAttribute : Attribute
{
public ContextEveningBoundAttribute() { }
private bool _isEvening;
/// <summary>
/// 指定对象是否需要后期动态绑定上下文。
/// </summary>
public bool IsEvening { set { _isEvening = value; } get { return _isEvening; } }
}
}
仅仅为了标识后期绑定说明。在ContextModuleBaseObject 对象的构造函数中可以看到。
[csharp]
public ContextModuleBaseObject()
{
if (typeof(T).GetCustomAttributes(typeof(ContextEveningBoundAttribute), false) != null)
{
_IsEvening = true;
return;
}
//前期静态绑定上下文
if (ContextRuntime.CurrentContextRuntime == null)
throw new Exception("上下文环境未能初始化,请检查您的代码入口是否启用了ContextRuntime对象。");
_contextRunTime = ContextRuntime.CurrentContextRuntime;
_InitContextHandler<T>();
}
到这里我们已经实现对象的动态绑定到上下文来,下面我们来分析Context如何用AOP配合完成面向切面编程的机制。
2.4.AOP中的对象行为的契约设计实现
其实这里的契约设计也就是图2中对AOP中的“面”的约定。
AOP全称为“面向切面编程”。对象在运行时具备多个面,其实在.NET里面我们习惯性的用特性(Attribute)来表达这个概念。因为不需要改动任何代码就可以将特性加到对象中的任何元素中去,在不同的业务环节或者说是功能环节就能动态的转动元素体现出“切面”的优势,当然具体的实现可能很多种,这里使用特性来完成。
在此约定任何处理对象方法的“面”都将被抽象。这里我将命名为ContextOperationBaseAttribute该特性表示所有附加到方法上的特性的基类,对“面”的抽象。
那么不同类型的面将有着不同的操作行为,比如:记录日志的特性、计算性能的特性、认证安全的特性他们都有着不同的行为和属性,所以这里我们还需要提取一个顶层接口作为行为类的特性处理抽象。这里我将命名为IContextOperationHandler该接口作为统一执行行为特性的高层依赖。其实这里也体现出依赖倒置原则,依赖抽象不依赖具体实现。
完整的ContextOperationBaseAttribute 类:
[csharp]
/***
* author:深度训练
* blog:http://wangqingpei557.blog.51cto.com/
* **/
using System;
using System.Collections.Generic;
using System.Text;
namespace ContextModule
{
/// <summary>
/// 上下文操作动作特性化基类。
/// 所有对上下文中的类、方法的过滤操作都必须继承此类。
/// </summary>
public abstract class ContextOperationBaseAttribute : Attribute, IContextOperationHandler
{
/// <summary>
/// 过滤器的处理顺序,从小到大的方式进行处理。
/// </summary>
public int OperationSort { get; set; }
/// <summary>
/// 下一次过滤操作类
/// </summary>
internal ContextOperationBaseAttribute NextOperation { get; set; }
/// <summary>
/// 开始处理过滤对象
/// </summary>
/// <typeparam name="Result">方法的返回值类型</typeparam>
/// <param name="actionmethod">调用的方法包装</param>
/// <param name="paramarray">方法的有序参数</param>
/// <returns></returns>
public virtual Result ResultAction<Result>(ContextMethodInfo actionmethod, params object[] paramarray)
{
object result = null;
if (!actionmethod.IsPost)
{
result = (this as IContextOperationHandler).Operation(actionmethod, paramarray);
if (this.NextOperation != null)
return this.NextOperation.ResultAction<Result>(actionmethod, paramarray);
}
if (result != null)
return (Result)result;
return default(Result);
}
public abstract object Operation(ContextMethodInfo contextmethod, params object[] paramarray);
}
}
作为抽象的顶层类需要完成派生类重复的劳动。这里实现了一个ResultAction泛型方法,该方法是外部调用绑定对象的方法时的入口点。但是具体的实现区别在于IContextOperationHandler 接口的定义。
由于行为的特性可能存在多个,所以对于最后一个处理完的特性需要结束整个的调用链表,并且返回值。在ResultAction虚方法里面对IContextOperationHandler 接口的Operation方法执行调用,该方法将会在实现特定行为特性里面实现,这里又体现出“模板方法”设计模式。在抽象类中约定行为,在派生类中实现具体。
这里比较有意思的是,特性不在像大家实现ORM的那中简单的标识了。其实特性真正强大的地方在于运行时能动态的获取到,这得益于.NET元数据的功劳。并且动态实例化然后当作普通的对象实例使用。这个观念很多.NET程序员不宜转变。
在这里的ContextOperationBaseAttribute 又描述了另外一种数据结构“单向链表”,为了将绑定对象的行为最大化的在特性中实现,我们将方法的调用完全的传递到实现特性中去。那么对方法上多个作用的特性如何穿过呢,并且能保证数据的正常传递和返回。有两点我们需要注意,一个是特性的作用顺序,二个是特性对方法的执行是否完成。这两点我们都要考虑进去,所以在ContextOperationBaseAttribute 类中用public int OperationSort { get; set; }属性表示特性的执行顺序,记录日志的特性和计算性能的特性我们很难在这里定死,需要根据后期程序的执行情况而定,如我要先记录日志然后在执行方法。
那么我们又如何将ContextOperationBaseAttribute类型串联起来呢?在ContextModuleBaseObject
泛型绑定类中我们在构造的时候就将通过ContextOperationBaseAttribute. OperationSort 属性初始化了特性处理链表。
那么我们如何将具体的对象与特性关联建立起对应关系呢?一个行为可能有多个ContextOperationBaseAttribute的实现。所以这里我们需要一个能满足行为与特性之间的数据结构。
这里我将它定义为ContextFilterHandlerMap该类继承自Dictionary<string,ContextOperationBaseAttribute>泛型字典类,使用KEY—VALUE的方式存放行为与ContextOperationBaseAttribute处理特性的对应关系。
[csharp]
/***
* author:深度训练
* blog:http://wangqingpei557.blog.51cto.com/
* **/
using System;
using System.Collections.Generic;
using System.Text;
namespace ContextModule
{
/// <summary>
/// 特定于上下文的过滤器映射表。
/// 上下文中的任何方法如果需要进行上下文管理的,则使用ContextModule.ContextOperationBaseAttribute特性派生类进行管理。
/// 所有附加于方法、类上的特性管理类都将被映射到ContextModule.ContextFilterHandlerMap实例中。
/// </summary>
public class ContextFilterHandlerMap : Dictionary<string, ContextOperationBaseAttribute>
{
public ContextFilterHandlerMap() { }
/// <summary>
/// 获取方法对应的过滤器处理特性
/// </summary>
/// <param name="mapname">映射Key</param>
/// <returns>ContextOperationBaseAttribute特性实例</returns>
public ContextOperationBaseAttribute MapOperation(string mapname)
{
return this[mapname];
}
/// <summary>
/// 设置过滤器与特定方法的映射
/// </summary>
/// <param name="mapname">映射Key</param>
/// <param name="operationlist">过滤器特性基类ContextOperationBaseAttribute</param>
public void MapOperation(string mapname, ContextOperationBaseAttribute operationlist)
{
this.Add(mapname, operationlist);
}
}
}
最后只需要向外提供IContextOperationHandler 接口就可以实现方法与处理特性的串联了。
[csharp]
/***
* author:深度训练
* blog:http://wangqingpei557.blog.51cto.com/
* **/
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
namespace ContextModule
{
/// <summary>
/// 上下文操作管理接口
/// </summary>
public interface IContextOperationHandler
{
/// <summary>
/// 开始上下文处理
/// </summary>
/// <param name="contextmethod">CRL目前正在执行的上下文方法的信息。
/// 可以通过ContextMethodInfo实例获取方法详细信息。</param>
///<param name="paramarray">参数数组</param>
object Operation(ContextMethodInfo contextmethod, params object[] paramarray);
}
}
通过对外公开接口,让实现“面”的客户端去完成对具体对象方法的执行。ContextMethodInfo 类型是包装System.Reflection. MethodInfo方法元数据的,将通过调用切入到方法内部。
这里基本上实现了AOP对行为的多面支持,下面我们来看一下如果动态的切入到方法中。
2.5.动态入口的实现
对所有方法的调用将是比较头疼的。由于一般面向上下文、面向切面都是有编写者控制对方法的调用,可以很方便的通过后台的隐式的调用。但是作为普通的方法的入口调用主要有三种方式实现。
1):委托实现入口
通过使用System.Delegate动态派生类型来完成对方法的调用,但是委托对于方法的签名必须是强类型的,所以很难做到通用的调用入口。
2):反射实现入口(通过扩展方法在OBJECT基类中加入获取MethodInfo对象的方法,使用时通过该方法获取调用方法的信息)
通过扩展方法在System.Object中加入一个扩展方法用来获取调用方法的信息,然后通过反射动态的调用,这种方法只比较常用的。但是如何框架是在.NET2.0中使用的扩展方法还不能实现,这里我是在ContextModuleBaseObject基类中加了一个类似扩展方法的方式。绑定对象可以很方便的获取到调用方法的MethodInfo对象。
3):完美的动态编译(向抽象、多态敬礼)
最为完美的是扩展代码生成提供程序,在使用的对象里面在派生一个类,专门用来进行多态的转移,让高层的调用顺利进入到派生的类中。不过比较复杂。
这里是使用第二种方式使用的:
[csharp]
/***
* author:深度训练
* blog:http://wangqingpei557.blog.51cto.com/
* **/
using System;
using System.Collections.Generic;
using System.Text;
using System.Reflection;
namespace ContextModule
{
/// <summary>
/// 面向上下文的操作类。
/// 对上下文发起的方法调用需要通过该基类进行调用才能让我们的扩展点使用成为可能。
/// </summary>
public static class ContextAction
{
/// <summary>
/// 在面向上下文的环境中进行方法的调用。
/// </summary>
/// <typeparam name="PostObjectType">调用的上下文绑定对象类型</typeparam>
/// <typeparam name="ResultType">方法的返回类型</typeparam>
/// <param name="post">调用的上下文绑定对象的实例</param>
/// <param name="method">方法的信息对象MethodInfo,通过Oject.GetContextMethodInfo方法自动获取。</param>
/// <param name="paramarray">方法的有序参数集合</param>
/// <returns>ResultType泛型类型指定的返回实例</returns>
public static ResultType PostMethod<PostObjectType, ResultType>(PostObjectType post, MethodInfo method, params object[] paramarray)
where PostObjectType : ContextModuleBaseObject<PostObjectType>
{
_LockPostObejctIsEveningBound<PostObjectType>(post);
string key = string.Format("{0}.{1}", method.DeclaringType.FullName, method.Name);
if (!ContextRuntime.CurrentContextRuntime.FilterMap.ContainsKey(key))
{
throw new Exception(string.Format("方法{0}未经过上下文进行管理。", key));
}
ContextMethodInfo contextmethod = new ContextMethodInfo(method, post);
return ContextRuntime.CurrentContextRuntime.FilterMap[key].ResultAction<ResultType>(contextmethod, paramarray);
}
/// <summary>
/// 检查调用实例类是否属于后期绑定。
/// 通过使用ContextModule.ContextEveningBound(IsEvening = true)方式指定后期绑定上下文。
/// </summary>
private static void _LockPostObejctIsEveningBound<PostObjectType>(PostObjectType post)
where PostObjectType : ContextModuleBaseObject<PostObjectType>
{
ContextModuleBaseObject<PostObjectType> contextclass = post as ContextModuleBaseObject<PostObjectType>;
if (contextclass._IsEvening)
contextclass._EveningBoundChildClass<PostObjectType>();
}
}
}
所有的调用均使用PostMethod泛型方法启动。_LockPostObejctIsEveningBound私有方法,判断当前类型是否是后期绑定,如果是则需要切入到基类中调用_ EveningBoundChildClass方法进行ContextOperationBaseAttribute 类型的链表构造,然后直接通过头对象进行调用。
3.实例上下文与静态上下文
对于实例上下文同时也就存在静态上下文的概念,对于静态对象的逻辑归纳有点难度,由于静态对象在面向对象设计方面很难抽象。只能通过特性注入的方式强制性的将静态对象拉入上下文。但是在多线程的情况下,确实是可以研究的。将静态对象全部进行线程本地存储,强制性的进行类似实体对象的管理。
4.面向上下文的领域模型(DMM)
基于上下文的使用模式可以进行领域模型的初步构造,可以先向领域中的大比例结构靠近,将业务模型逻辑归纳到一定的Context中,对业务的模块化梳理也是一种实现。[王清培版权所有,转载请给出署名]
在分层架构中的业务逻辑层可能需要加入上下文的管理,将业务模型进行运行时控制。比如订单处理,将订单业务流程相关的模型对象归纳为一块。比如用户相关,将用户管理的业务流程相关的模型对象归纳为一块,确实是很有意思。
作者:wangqingpei557
下一篇: 鸡蛋里挑骨头