.NET 业务框架开发实战之十 第一阶段总结,深入浅出,水到渠成(前篇)
前言:这个系列有段时间没有动了。主要是针对大家的反馈在修改代码。在修改的过程中,也有了一些新的体会,这里和大家分享一下,同时也发布一下业务框架的第一个版本。在本篇文章中,学习到的不是仅仅只是代码,而是设计的思想和实现这种思想的方法。在写本篇时有个感触:把一个东西彻底的讲清楚,不容易。希望大家
多提意见。而且在写本篇的时候,我个人也是很兴奋的,至于原因相信大家在看完之后就知道了。J
本篇的议题如下:
1. 打通业务层和数据层
2. 打通方法的选择和实现
3. 再次借鉴.NET Framework设计思想
4. 水到渠成
5. 代码的版本说明
1. 打通业务层和数据层
首先,回顾之前的文章一直讨论的问题:
1. 如何使得数据层”以不变应万变”。
2. 条件对象如何实现
3. 如何在业务层和数据层之间mapping数据
本篇就告诉大家,如何切切实实的解决上面三个问题。
首先,从一个图示开始讲述。
从上面的图中可以看出,架起在BLL和DAL之前的桥梁的就是中间的那个条件对象。正是因为有了这个,所以上面的提出的问题才得以解决。
下面先从操作上总体来讲述一下这个图的具体流程:
a. 在业务类中创建一个方法。例如在业务类User中,定义如下:
现在只看GetUserByAge这个方法:在方法中构造出一个条件对象,大家第一眼能看出来:是Linq的实现。其实最后的实现只是借用了Linq的思想,仅此而已。
b. 解析条件对象。在上面的构造的条件对象中,Age,Name等,都是业务类User的字段,这些字段的值肯定最终是从数据库中的表字段中获取的(或者是通过数据库中的值算出来的),所以在解析条件对象的时候,就要知道这些业务属性对应数据库中哪个表的哪个字段。因此在业务类中声明每个属性的时候就要同时保留它所对应的数据库字段的信息。一旦业务类的属性中保存了这些信息
c. 数据层操作SQL语句。在解析条件对象的时候,就会最终得到相对应的SQL语句,然后在数据层中执行这些SQL语句。
可能上面讲的比较的抽象,因为本篇的要讲述的东西确实比较的多(Word中写了超过了10页),上面讲述的三个步骤也是先按大家有个印象。不是很懂也没有关系。
2. 打通方法的选择和实现
接下来就是方法的探索和思考,以及实现的过程。我是想带着个大家跟着一起看看,为什么最后会采用这个解决方案的。
首先,就从条件对象开始看起。
在实现条件对象(条件对象和查询对象的区别之前讲过,这里重述一下:查询对象只是条件对象的一个子集,查询对象用来在查询的使用构造查询条件;条件对象不仅仅在查询时构造条件,而且在增加,删除,,修改时候也使用,例如:只修改Name=”admin”的数据,在修改数据库的时候也用了一定的条件。所以条件对象>查询对象)的时候,也是参看了其他开源框架的一些实现(Nhibernate中查询对象的,CSLA)。
同时要明白一点:设计出来的框架是给开发人员使用的,所以要考虑如何使得开发人员最快最好的使用框架,所以要从开发人员的角度看(这一点也是很重要的)。例如在Nhibernate中查询对象,使用的方法如下(仅仅是简单的举例而已): 其实本系列中的业务框架之前的条件对象的构造也是参看了Nhibernate中查询对象的方法来实现和使用的,如下:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
.Add(Restrictions.Like("Firstname", "Xiaoyang%"))
.Add(Restrictions.Between("Lastname", "A%", "Y%"))
.ListCustomer>();
因为现在.NET中的开发人员对Linq的一些操作比较的熟悉,而且如果把条件对象的使用方式改为下面的方式:
那么开发人员的学习成本就几乎为零(因为他们熟悉Linq,如果条件对象也采用这种比较统一的方法实现,他们就可以采用”以此类推”的思想来使用框架),更多的好处我就不说了,大家可以自己体会或者参看本系列之前的文章。
接下来就探索实现条件对象的方法(Linq to XXX篇)。
熟悉Linq的朋友可以看出:可以使条件对象实现IQueryable接口,然后采用实现linq to XXX的方法,实现自己的Linq Provider。这个方法是否可行,下面,我们就深入linq to XXX的来看一看,看完之后,结果就很清楚了。
首先来看下面两个接口:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
Type ElementType { get; }
Expression Expression { get; }
IQueryProvider Provider { get; }
}
IQueryable CreateQuery(Expression expression);
IQueryable<TElement> CreateQuery<TElement>(Expression expression);
object Execute(Expression expression);
TResult Execute<TResult>(Expression expression);
}
Linq的出现,使得很多东西都和linq扯上了关系:Linq to sql, Linq to Google, Linq to javascript, Linq to Nhibernate...... 所列出来的这些,我们统称为linq to XXX。这些Linq to XXX都是实现了上面两个接口。
下面就通过自己实现linq to sql来举例分析。
从总体来看:linq to sql的本质就是:把操作转换为sql语句,然后用ADO.NET执行,最后把结果转换为实体返回。
其实下面列出了这么多的代码,其中最关键的其实就是QueryProvider中的Execute方法:这个方法负责把你的操作进行解析,其实真正负责解析的QueryTranslator。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
QueryProvider provider;
Expression expression;
public Query(QueryProvider provider) {
if (provider == null) {
throw new ArgumentNullException("provider");
}
this.provider = provider;
this.expression = Expression.Constant(this);
}
public Query(QueryProvider provider, Expression expression) {
if (provider == null) {
throw new ArgumentNullException("provider");
}
if (expression == null) {
throw new ArgumentNullException("expression");
}
if (!typeof(IQueryable<T>).IsAssignableFrom(expression.Type)) {
throw new ArgumentOutOfRangeException("expression");
}
this.provider = provider;
this.expression = expression;
}
Expression IQueryable.Expression {
get { return this.expression; }
}
Type IQueryable.ElementType {
get { return typeof(T); }
}
IQueryProvider IQueryable.Provider {
get { return this.provider; }
}
public IEnumerator<T> GetEnumerator() {
return ((IEnumerable<T>)this.provider.Execute(this.expression)).GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator() {
return ((IEnumerable)this.provider.Execute(this.expression)).GetEnumerator();
}
public override string ToString() {
return this.provider.GetQueryText(this.expression);
}
}
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
protected QueryProvider() {
}
IQueryable<S> IQueryProvider.CreateQuery<S>(Expression expression) {
return new Query<S>(this, expression);
}
IQueryable IQueryProvider.CreateQuery(Expression expression) {
Type elementType = TypeSystem.GetElementType(expression.Type);
try {
return (IQueryable)Activator.CreateInstance(typeof(Query<>).MakeGenericType(elementType), new object[] { this, expression });
}
catch (TargetInvocationException tie) {
throw tie.InnerException;
}
}
S IQueryProvider.Execute<S>(Expression expression) {
return (S)this.Execute(expression);
}
object IQueryProvider.Execute(Expression expression) {
return this.Execute(expression);
}
public abstract string GetQueryText(Expression expression);
public abstract object Execute(Expression expression);
}
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
StringBuilder sb;
internal QueryTranslator() {
}
internal string Translate(Expression expression) {
this.sb = new StringBuilder();
this.Visit(expression);
return this.sb.ToString();
}
private static Expression StripQuotes(Expression e) {
while (e.NodeType == ExpressionType.Quote) {
e = ((UnaryExpression)e).Operand;
}
return e;
}
protected override Expression VisitMethodCall(MethodCallExpression m) {
if (m.Method.DeclaringType == typeof(Queryable) && m.Method.Name == "Where") {
sb.Append("SELECT * FROM (");
this.Visit(m.Arguments[0]);
sb.Append(") AS T WHERE ");
LambdaExpression lambda = (LambdaExpression)StripQuotes(m.Arguments[1]);
this.Visit(lambda.Body);
return m;
}
throw new NotSupportedException(string.Format("The method '{0}' is not supported", m.Method.Name));
}
protected override Expression VisitUnary(UnaryExpression u) {
switch (u.NodeType) {
case ExpressionType.Not:
sb.Append(" NOT ");
this.Visit(u.Operand);
break;
default:
throw new NotSupportedException(string.Format("The unary operator '{0}' is not supported", u.NodeType));
}
return u;
}
protected override Expression VisitBinary(BinaryExpression b) {
sb.Append("(");
this.Visit(b.Left);
switch (b.NodeType) {
case ExpressionType.And:
sb.Append(" AND ");
break;
case ExpressionType.Or:
sb.Append(" OR");
break;
case ExpressionType.Equal:
sb.Append(" = ");
break;
case ExpressionType.NotEqual:
sb.Append(" <> ");
break;
case ExpressionType.LessThan:
sb.Append(" < ");
break;
case ExpressionType.LessThanOrEqual:
sb.Append(" <= ");
break;
case ExpressionType.GreaterThan:
sb.Append(" > ");
break;
case ExpressionType.GreaterThanOrEqual:
sb.Append(" >= ");
break;
default:
throw new NotSupportedException(string.Format("The binary operator '{0}' is not supported", b.NodeType));
}
this.Visit(b.Right);
sb.Append(")");
return b;
}
protected override Expression VisitConstant(ConstantExpression c) {
IQueryable q = c.Value as IQueryable;
if (q != null) {
// assume constant nodes w/ IQueryables are table references
sb.Append("SELECT * FROM ");
sb.Append(q.ElementType.Name);
}
else if (c.Value == null) {
sb.Append("NULL");
}
else {
switch (Type.GetTypeCode(c.Value.GetType())) {
case TypeCode.Boolean:
sb.Append(((bool)c.Value) ? 1 : 0);
break;
case TypeCode.String:
sb.Append("'");
sb.Append(c.Value);
sb.Append("'");
break;
case TypeCode.Object:
throw new NotSupportedException(string.Format("The constant for '{0}' is not supported", c.Value));
default:
sb.Append(c.Value);
break;
}
}
return c;
}
protected override Expression VisitMemberAccess(MemberExpression m) {
if (m.Expression != null && m.Expression.NodeType == ExpressionType.Parameter) {
sb.Append(m.Member.Name);
return m;
}
throw new NotSupportedException(string.Format("The member '{0}' is not supported", m.Member.Name));
}
}
对于实现了IQueryable的对象,在他们进行的每一个操作(也就是调用实现这个接口的类上的方法)其实都没有立刻去执行,而且把进行的操作记录下来了,放在一个称为Expression Tree表达式数的数据结构中,然后再真正执行的时候(就是调用Execute来执行), QueryTranslator对象就遍历表达式树,对操作进行解析,例如linq to sql就是把表达式树中操作解析为对数据库进行的操作,以sql语句的形式体现出来.
当把操作解析为了sql语句之后,就是用ADO.NET的方法来执行SQL操作,然后通过反射,把ADO.NET执行的结果转换为数据实体。如下:
上面简单的介绍了如何实现linq to sql具体的实现代码,大家可以自己过后慢慢的看或者参看我的另外的一个linq系列,现在我们继续后面的话题。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
Enumerator enumerator;
internal ObjectReader(DbDataReader reader) {
this.enumerator = new Enumerator(reader);
}
public IEnumerator<T> GetEnumerator() {
Enumerator e = this.enumerator;
if (e == null) {
throw new InvalidOperationException("Cannot enumerate more than once");
}
this.enumerator = null;
return e;
}
IEnumerator IEnumerable.GetEnumerator() {
return this.GetEnumerator();
}
class Enumerator : IEnumerator<T>, IEnumerator, IDisposable {
DbDataReader reader;
FieldInfo[] fields;
int[] fieldLookup;
T current;
internal Enumerator(DbDataReader reader) {
this.reader = reader;
this.fields = typeof(T).GetFields();
}
public T Current {
get { return this.current; }
}
object IEnumerator.Current {
get { return this.current; }
}
public bool MoveNext() {
if (this.reader.Read()) {
if (this.fieldLookup == null) {
this.InitFieldLookup();
}
T instance = new T();
for (int i = 0, n = this.fields.Length; i < n; i++) {
int index = this.fieldLookup[i];
if (index >= 0) {
FieldInfo fi = this.fields[i];
if (this.reader.IsDBNull(index)) {
fi.SetValue(instance, null);
}
else {
fi.SetValue(instance, this.reader.GetValue(index));
}
}
}
this.current = instance;
return true;
}
return false;
}
public void Reset() {
}
public void Dispose() {
this.reader.Dispose();
}
private void InitFieldLookup() {
Dictionary<string, int> map = new Dictionary<string, int>(StringComparer.InvariantCultureIgnoreCase);
for (int i = 0, n = this.reader.FieldCount; i < n; i++) {
map.Add(this.reader.GetName(i), i);
}
this.fieldLookup = new int[this.fields.Length];
for (int i = 0, n = this.fields.Length; i < n; i++) {
int index;
if (map.TryGetValue(this.fields[i].Name, out index)) {
this.fieldLookup[i] = index;
}
else {
this.fieldLookup[i] = -1;
}
}
}
}
}
现在我们回到之前的话题:条件对象是否可以采用这种方式实现?
大家看到上面的代码Query<T>其中的T,和Execute方法的返回值。在上面的代码中,如果T是User类,即,Query<User>,那么最后的Execute方法返回的一定会是User的集合。也就是说:Execute方法已经对数据源(这里是数据库)进行了操作,并且把结果以数据实体的形式已经返回了,并且返回的数据实体的类型就是T,例如User。
但是我们这里的要实现的条件对象只是想把条件构造出来,不是立刻去执行。至于具体的执行数据操作者(DataProvider)可以任意选择的:使用ADO.NET方法,还是EF的方法,还是Nhibernate,都是可以配置的。如下:
上面的代码中,Create方法就是实例化一个ICriteria<User>,此时我们想做的仅仅只是一件事:把在 ICriteria<User>上的操作记录下来而已。然后把记录下来的结果解析,解析的最终结果就是一条sql命令,然后再给不同的DataProvider去执行。也就是说,在DataPortal内部可以配置用什么方法来执行数据操作:是直接使用ADO.NET执行sql命令,还是把sql命令给Entity Framework...通过配置决定。如果ICriteria<T>是从IQueryable接口进行了继承,那么在ICriteria实现这个结果的过程中就必须要去数据库中进行执行,因为Execute方法返回的是T的集合,而不是sql命令(字符串)。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
{
ICriteria<User> conditionPerson =
CriteriaFactory.Create<User>().Where(u => u.Age < this.Age).OrderBy<string>(u => u.Name).Skip(8).Take(8);
return DataPortal.Query(conditionPerson);
}
大家可能想到:那就在Execute方法中去实现不同的DataProvider,例如之前的例子在ObjectReader用ADO.NET实现了,那么也可以在ObjectReader中用EF实现数据操作。这个方法确实可以,也很不错。但是这个方法在分布式开发中(特别是在WCF中)有一点的局限性。例如你有一个界面,上面可以有很多的选项,如下:
在服务接口那边,你肯定不想定义N多差不多的接口方法:如 或者 这样都是很不灵活的,如果User的属性减少了或者增多了,那么如果要在服务器那边暴露的接口的方法也要修改,这样终究是不好。如下采用下面的方法:
GetUserByEmail(string email);
其中,Critera是条件对象。那么我们在客户端就可以任意构造条件对象,这个条件对象就把在它上面进行的操作记录下来,然后统一的交给GetUserByCondition方法去服务器解释并执行。此时,这个条件对象就是在客户端生成的,而且这个条件对象此时是不用去数据库中去执行的。如果条件对象是从IQueryable接口继承的,那么在客户端构造完条件对象之后,就要去数据库中执行了,如果再在ObjectReader搞个分布式调用,难度不说,也很别扭,这不是我们所要的。
所以,综合上面的一些考虑,那么可以确定:条件对象不继承IQueryable接口。但是我们又希望采用类似linq的操作,那么只有自己实现了。
本篇就暂时写到这里,因为太长了,所以分为前篇和后篇发布,因为博客园不能在一小时内发两篇,所以后篇将会在9点左右发布。希望大家见谅。
版权为小洋和博客园所有,,欢迎转载,转载请标明出处给作者。
http://www.cnblogs.com/yanyangtian
![](https://images.cnblogs.com/cnblogs_com/yanyangtian/chapter10002.jpg)