老翅寒暑

一个老鸟的自白
随笔 - 75, 文章 - 0, 评论 - 660, 引用 - 24
数据加载中……

运算表达式类的原理及其实现

运算表达式类的原理及其实现

运算表达式在编程中是一个很重要的概念,但是实际工作中,需要使用到运算表达式的机会并不太多。但是日前在研究报表系统的时候,发现了它的用处,于是就研究了一下,做了一个比较运算表达式类。

我的表达式类的实现主要使用了visitor模式。这个类的实现核心分为两个部分,一个部分是具体的表达式类,比如 AddExpression,EqExpression类,他们的工作就是保存语义,把 +、-、*、/等操作记住。另外一部分就是具体的转换类,这里叫做Calculator,他们负责把表达式运算为所需要的结果,通过使用不同的Calculator,我们可以运算出整个表达式的值,也可以把表达式转化为其他的表示形式,比如xml格式等等。我们先看一个例子:

[代码1]

 void Sample
 
{
  Expression expr;
  ICalculator cal 
= new Expressions.AlgorithmCalculator();
  IContext context 
= Expression.DefaultContext;

  expr 
= (Expression)3 + (Expression)4;
  Console.WriteLine(expr.ToXml());
  
  
string text = "<add><int>3</int><int>4</int></add>";
  expr 
= Expression.FromXml(text);

  Console.WriteLine(expr.ToXml());
 }

 

这个例子是从我的测试用例中摘录下来的。表达式类的基本用法就和代码中的表达式形式差不多,目的是为了直观和简单使用。代码1中的工作是把表达式变成xml形式并从xml形式中重新构造一个新的表达式。虽然从中我们不能很明显看出表达式的两个部分的关系,但是这一段代码应该可以让你看得很明显,这是Expression.FromXml和ToXml的实现代码:

[代码2]

  static public Expression FromXml(XmlNode root, )
  
{
   XmlExpression xe 
= new XmlExpression(root);
   Expression expr 
= new Expression(xe);

   
   
return (Expression) xe.Evaluate(new FromXmlCalculator(), new XmlContext(expr));
  }


  
public DataElement ToDataElement()
  
{
   
return (DataElement)Evaluate(new XmlCalculator(), new XmlContext(this));
  }

  
  
public string ToXml()
  
{
   DataElement element 
= ToDataElement();
   
return element.ToString();
  }




大家从代码2中就可以看到,原来Expression的ToXml和FromXml就是用专门的Calculator来实现出来的。既然Calculator可以实现这样的效果,其他效果我们也一定可以实现。在文章的源代码中,我提供了两个实现:MSSQLCalaulator负责把表达式变成Microsoft SQL Server 2000格式的sql语句,HibernteExpressionCalculator负责把表达式变成NHibernate的表达式形式。

我是如何实现的?如果您有好奇心的话,一定会想这样问我。别着急,这就给您慢慢道来。首先看一下所有表达式类的基类定义:

[代码3]

 /// <summary>
 
/// 抽象的运算表达式接口
 
/// </summary>

 public interface IOperation
 
{
  
/// <summary>
  
/// 计算表达式的值
  
/// </summary>
  
/// <param name="cal">计算器</param>
  
/// <returns>计算之后的值</returns>
  
/// <remarks>
  
/// 计算器是专门根据不同类型的需求,实际进行操作运算的接口
  
/// </remarks>

  object Evaluate( ICalculator cal, IContext context ); 
 }


从这个接口我派生出来了ConstValueExpression,UnaryExpression,BinaryExpression,TripleExpression。他们之间的差别仅仅是内涵的参数不同而已,分别是0个,1个,2个和3个参数,用来避免我写重复代码而已,并不重要,重要的是IOperation中的函数Evaluate。

Evaluate是最重要的函数,它负责传递给表达式Calculator和Context,上文已经说过Calculator的用处了,您可以把Context看成是一个参数的容器,因为IOperation或者Calculator在工作的时候,或多或少需要知道一些调用环境的特殊信息,Context的作用就是把这些信息传过来,至于到底传哪些信息,就看使用者您的需要了,大家看IContext的接口定义可以知道的更清楚:

[代码4]

 /// <summary>
 
/// 负责为计算时候提供额外的信息
 
/// </summary>

 public interface IContext
 
{
 }



原来接口IContext定义里边就是空的,不过为了实现上文的Expression的XML存取,我倒是实现了自己专用的Context,它负责把Calculator不认识的表达式或者xml节点放到事件中撒播出去,让用户自己去处理,我的 Context 如下:

[代码5]

  public class XmlContext : IContext
  
{
   Expression m_Expr;
   
private XmlNode m_Node;

   
/// <summary>
   
/// 当前的节点
   
/// </summary>

   public XmlNode Node get return m_Node; } set { m_Node = value; } }

   
public XmlContext(Expression expr){m_Expr = expr;}

   
public IOperation UnknownXmlNode(string operation, object[] parameters)
   
{
    
return m_Expr.OnUnknownXmlNode(m_Node, operation, this, parameters);
   }


   
public DataElement UnknownOperation(string operation, object[] parameters)
   
{
    
return m_Expr.OnUnknownOperation(operation, this, parameters);
   }

  }



大家看到了吗?我的这个Context就是把不认识的节点撒播出去,让程序员自己挂接事件来处理这些不认识的东西,它和我专用的Calculator配合的很好:
[代码6]

  public object Extension(string operation, IContext context, params object[] parameters)
  
{
   
if( context is Expression.XmlContext )
   
{
    Expression.XmlContext xc 
= (Expression.XmlContext)context;
    
return xc.UnknownOperation(operation, parameters);
   }


   
throw new NotSupportedException("不能支持操作:" + operation);
  }




提了事件当然就不能不提表达式类的扩展性了,我们先看看表达式本身有哪些限制,看一下ICalculator的定义吧:

[代码7]

 /// <summary>
 
/// ICalculator 的摘要说明。
 
/// 负责把表达式变成实际结果的抽象接口
 
/// </summary>

 public interface ICalculator
 
{
  
object Add(object left, object right, IContext context);
  
  
object Mod(object left, object right, IContext context);

  
object Eq(object left, object right, IContext context);
  
  
object LessEq(object left, object right, IContext context);

  
object And(object left, object right, IContext context);
  
object Or(object left, object right, IContext context);

  
object BitAnd(object left, object right, IContext context);
  
object BitOr(object left, object right, IContext context);
  
object BitNot(object val, IContext context);

  
object Plus(object val, IContext context);
  
object Neg(object val, IContext context);

  
object Not(object val, IContext context);

  
object BoolValue(bool val, IContext context);  
  
  
object DateTimeValue(DateTime val, IContext context);
         
object MemberRefValue(Member val, IContext context);
         
object MemberRefValue(LooseMember val, IContext context);

  
object Extension(string operation, IContext context, params object[] parameters);
     }




熟悉设计模式的大虾立马就可以看出来了,典型的visitor模式。ICalculator实现了几乎所有的运算操作接口,而那些AddExpression之类的东西所作的事情无非就是把自己的参数传递给ICalculator适当的接口函数中去而已。既然是visitor模式,当然ICalculator就有visitor模式固有的缺陷:扩展性不好!

如果我要实现一个between的操作,我就要来修改 ICalculator 接口的定义,增加一个接口
  object Between(object cond, object low, object high, IContext);
然后还要到每一个实现了ICalculator的类中增加这个Between的实现,哦,这样做简直就是噩梦!为了避免这个噩梦,我在ICalculator中增加了Extension接口函数,让所有自己定义的表达式都来调用Extension。看看我扩展的BetweenExpression吧:
[代码8]

 public class BetweenExpression
  : TripleExpression
 
{
  
  
  
override public object Evaluate( ICalculator cal, IContext context )
  
{
   
return cal.Extension( BETWEEN, context
       ,
base.m_Left.Evaluate(cal, context)
       ,
base.m_Middle.Evaluate(cal, context)
       ,
base.m_Right.Evaluate(cal, context), context);
  }

 }


然后在实现自己的Calculator中把Extension实现一下就可以了,我的 MSSQLCalculator 的实现如下:
[代码9]

  public object Extension(string operation, IContext context, params object[] parameters)
  
{
   
if( operation == LikeExpression.LIKE )
    
return Like(parameters[0], parameters[1], context);
   
else if( operation == BetweenExpression.BETWEEN )
    
return Between(parameters[0], parameters[1], parameters[2], context);
    
   
else if( context is Expressions.ICallbackContext )
    
return ((ICallbackContext)context).OnCalculateExtension(operation, parameters);

   
throw new NotSupportedException("不能支持操作:" + operation);
  }


  
大家要注意代码9中的第二个else if语句,对 ICallbackContext 的回调一定要加上,这样就可以支持其他形式的扩展了。至于如何扩展法,不妨自己动手试一下吧。

附带源代码到这里下载。

posted on 2005-05-28 16:45 老翅寒暑 阅读(2435) 评论(7)  编辑 收藏 网摘 所属分类: 模式与代码

评论

#1楼    回复  引用    

发觉博客园的高手太多了。。值得研究!
2005-05-28 17:14 | CsOver [未注册用户]

#2楼    回复  引用  查看    

2005-05-28 18:35 | 编写人生      

#3楼    回复  引用    

--------
熟悉设计模式的大虾立马就可以看出来了,典型的visitor模式。ICalculator实现了几乎所有的运算操作接口,而那些AddExpression之类的东西所作的事情无非就是把自己的参数传递给ICalculator适当的接口函数中去而已。既然是visitor模式,当然ICalculator就有visitor模式固有的缺陷:扩展性不好!
=====

这段话有问题。

visitor visitee都没提到。 我还真看不出visitor模式。
2005-05-28 18:57 | visitor [未注册用户]

#4楼    回复  引用    

--------
熟悉设计模式的大虾立马就可以看出来了,典型的visitor模式。ICalculator实现了几乎所有的运算操作接口,而那些AddExpression之类的东西所作的事情无非就是把自己的参数传递给ICalculator适当的接口函数中去而已。既然是visitor模式,当然ICalculator就有visitor模式固有的缺陷:扩展性不好!
=====

这段话有问题。
ICalculator实现 接口也能实现?
AddExpression在哪?

visitor visitee都没提到。 我还真看不出visitor模式。
还有一个建议: uml比代码容易懂的多,写这类文章最好画一两个图
2005-05-28 18:58 | visitor [未注册用户]

#5楼 [楼主]   回复  引用  查看    

嗯,画一个图确实会容易看很多,等一下抽时间补一下。谢谢提醒!
2005-05-30 09:03 | 老翅寒暑      

#6楼    回复  引用    

佩服。
在实际项目中,推荐使用JFlex和Java_cup配套作词法及语法解析,生成语法树来实现。如果要灵活的话当然也要像那样老翅寒暑大哥那样首先必须定义表达式Expression对象。
小弟推荐的方法相对来说比较老套,类似Unix的lex/yacc思路。不过好处就是实际系统随着应用可能需要更改表达式规则,这种方法只用修改词法及语法定义即可;并且基本不用编写源码。
2005-06-27 14:37 | JBean [未注册用户]

#7楼    回复  引用  查看    

虽然用了Visitor模式,但我感觉本文在使用上仍有几分不妥之处。
首先,我看到了一个庞大的接口ICalculator。无论是谁要实现这个接口,它所要承担的责任是否太重了?
其次,我看到了Extension方法中烦人的if else判断。当表达式的操作越来越多的时候,if语句会否比计算机屏幕更长呢?
第三,作者提供了Extension方法,无非是为扩展使用。但这种扩展似乎违背了封闭-开放原则。将这个可能随时变化的东西和其他的操作如Add、MOd等放在一起,总感觉不爽啊。

其实,对运算表达式这类操作,我们通常用Interpreter模式来完成。但在本文的要求中,对表达式操作而言,是可能存在变化的,因此引入Visitor模式也是很好的方案。

我们先想想运算表达式中,哪些是经常变化的?显然,表达式和操作符就是变化的根源。按照OOP的思想,我们就需要封装这些变化。那么是否需要将表达式和操作符各自进行封装呢?仔细想想,其实这里的操作符我们也可以把它看作是特殊的表达式(这也符合Interpreter模式的思想)。

这样分析后,问题就简单了。由于表达式是可能变化的,我们就可以将表达式抽象为一个接口IExpression,并提供一个共同的操作Evaluate()。然后根据表达式的不同,创建不同的类型且实现该接口,如:ConstValueExpression,UnaryExpression,当然也应包括AddExpression等操作符的表达式。通过这个抽象,我们就能非常容易地对付表达式的变化了。

那么表达式所要解析的是什么呢?是根据Context来进行操作的。这些Context也许是通过文本文件,也可能是通过Xml文件,甚至是数据库。所以根据本文的定义,应对这些Context进行抽象,接口为IContext。此时,我们已经能够看到Visitor模式的雏形了。IExpression就是模式中的Visitor,而IContext即为被访问的对象。当然,在这里我们还结合了Interpreter模式。

我对文章提出的XmlContext的功能还不太清楚,为了简单化,我自己写了个简单的XmlContext类。
public interface IContext
{
IExpression Accept(IExpression expr);
}
public class XmlContext:IContext
{
public Expression Accept(IExpression expr)
{
return expr.EvaluateXmlContent(this);
}
public XmlContext(string context)
{
_context = context;
}
public IExpression ReadValue(string xmlNode)
{
//解析xml;
}
……
}
其中,Accept()方法就是接受访问操作的方法。XmlContext还有一些自己的方法,如ReadValue()等等。当然也可以提供Write()等方法。

下面是对表达式的抽象。我需要把庞大的接口ICalulator进行分解,把其内部的这些操作均封装为继承了IExpression的类类型。此时,使用Visitor模式的威力就出来了,我们可以任意添加新的操作,只要实现IExpression接口即可,而不需要再去实现Extension()方法了。
public interface IExpression
{
IExpression EvaluateXmlContext(IContext context);
IExpression EvaluateTxtContext(IContext context);
IExpression EvaluateDBContext(IContext context);
}
public class AddExpression:IExpression
{
public AddExpression(IExpression left,IExpression right)
{
_left = left;
_right = right;
}
public IExpression EvaluateXmlContext(IContext context)
{
return left.EvaluateXmlContext(context) + right.EvaluateXmlContext(context);
}

private IExpression _left;
private IExpression _right;
}
public class ConstValueExpression:IExpression
{
public ConstValueExpression(string xmlNode)
{
_xmlNode = xmlNode;
}
public IExpression EvaluateXmlContext(IContext context)
{
return (XmlContext)context.ReadValue(_xmlNode);
}
}
最后,客户端的调用如下:
IExpression left = new ConstValueExpression("left");
IExpression right = new ConstValueExpression("right");
IExpression addOp = new AddExpression(left,right);
IContext context = new XmlContext("<add><left>4</left><right>5</right></add>");
Console.WriteLine("the value of Add operation is:{0}",addOp.EvaluateXmlContext(context));
当然,我们还可以引入工厂模式,来创建表达式类型和Context类型。
2005-06-30 17:31 | wayfarer      




标题  
姓名  
主页
Email (博主才能看到) 
验证码 *  看不清,换一张 [登录][注册]
内容(请不要发表任何与政治相关的内容)  
  登录  使用高级评论  新用户注册  返回页首  恢复上次提交      
该文被作者在 2005-05-28 16:50 编辑过
Google站内搜索

China-pub 计算机图书网上专卖店!6.5万品种 2-8折!
近千种 9-95 新二手计算图书火热销售中!
开发者征途系统新作:《设计模式——基于C#的工程化实现及扩展》

相关文章:

相关链接: