在动态调用对象方法方面,可以使用重构。但是在速度方便,重构比直接调用方法要慢很多,这里有两篇文章Expression Tree vs reflection,Again on Expression Tree vs Reflection 对比了表达式树和重构,以及 用lambda表达式树替代反射 这篇文章,在一些场景下,我们可以直接使用表达式树来代替重构。
本文将以Fix框架源码来说明,在一些特定场景下使用表达式树来替代反射能达到的奇妙效果和优点。
什么是FIX协议
FIX(Financial Information eXchange)金融信息交换协议是一种主要用于证券交易过程交换的公开协议,它定义每条交易信息的内容和格式,这些信息内容与证券交易流程相对应,以保证交易信息安全准确地传送。FIX主要用于在各类参与者之间建立起实时的电子化通讯协议。
FIX协议的目标是把各类证券金融业务(包括外汇、证券、期货等)需求流程格式化,使之成为一个可用计算机语言描述的功能流程,并在每个业务功能接口上统一交换格式,方便各个功能模板的连接,从而提高整个行业的应用水平。
举个例子,流动性供应商是壮族,说的是壮语;交易平台是蒙古族,说的是蒙古语;风控系统是藏族,说的是藏语。如果这三个人交流,各说各自的语言,可能会因为无法沟通导致误会的产生。这个时候引进一个FIX协议,FIX作为普通话,三个人都可以使用FIX这个通用的官方语言进行交流和约定,避免了误解和交流障碍。
在很多地方,比如一些交易系统,行情系统,都是用FIX协议来进行传输信息。有专门实现了FIX协议的库,最著名的就是QuickFix类库,另外,针对.NET平台,有专门的QuickFix/n 它是开源的,我们可以在GitHub上看到其内部实现。
FIX简单使用方法
由于本文不是介绍FIX,但是对于FIX的用法还是需要简单介绍一下,主要就是有一个接口 IApplication 和一个抽象类 MessageCracker。比下面的代码:
public class MyApplication : MessageCracker, IApplication
{
public void OnMessage(QuickFix.FIX42.NewOrderSingle ord,SessionID sessionID)
{
ProcessOrder(ord.Price, ord.OrderQty, ord.Account);
}
protected void ProcessOrder(Price price,OrderQty quantity,Account account)
{
//...
}
#region Application Methods
public void FromApp(Message msg, SessionID sessionID)
{
Crack(msg, sessionID);
}
public void OnCreate(SessionID sessionID) { }
public void OnLogout(SessionID sessionID) { }
public void OnLogon(SessionID sessionID) { }
public void FromAdmin(Message msg, SessionID sessionID)
{ }
public void ToAdmin(Message msg, SessionID sessionID)
{ }
public void ToApp(Message msg, SessionID sessionID)
{ }
#endregion
}
这里,IApplication的相关方法是客户端和服务端通信:
- FromApp是接收到服务端的返回的回调信息,Message是一个基类,具体可以返回很多类型,比如撤单回报,成交回报,委托回报等等
- ToApp是客户端发送给服务端的比如心跳之类的。
- OnLogin,OnLogout,登录登出操作
- FromAdmin,ToAdmin,一些系统级别消息,比如心跳之类的。From一般是指服务端返回,To一般指发送给服务端信息。
- 如果客户端要给服务端发送消息,则需要使用OnCreate事件里的session = Session.LookupSession(sessionID),然后调用session.Send方法,发送相关的Message对象。具体可以查看源代码里的Examples.TradeClient部分(在QuickFIXn源代码中,Examples.TradeClient为客户端demo,Example.SimpleAcceptor和Example.Executor为对应的服务端demo)。
我们这里要重点查看的是,各种Crack方法里以及各种对应的OnMessage方法。在上面的例子中,在FromApp里,调用了抽象类MessageCrack里的Crack方法,在该Crack方法里,会跟拒Message的具体实际类型,去调用不同的OnMessage方法,这里卖弄OnMessage方法,可以有不同参数类型的重载,这里仅演示了NewOrderSignal的解析,这个就是ToApp下单后,服务端返回下单是否成功的回报。这里解析和处理回报信息。我们这里来看下MessageCrack抽象类的源代码。
表达式树代替反射的实现
核心的处理逻辑就是MessageCrack抽象类,这个在QuickFix/N的源代码里可以看到。
namespace QuickFix
{
/// <summary>
/// Helper class for delegating message types for various FIX versions to
/// type-safe OnMessage methods.
/// </summary>
public abstract class MessageCracker
{
private Dictionary<Type, Action<Message, SessionID>> _callCache = new Dictionary<Type, Action<Message, SessionID>>();
public MessageCracker()
{
Initialize(this);
}
private void Initialize(Object messageHandler)
{
Type handlerType = messageHandler.GetType();
MethodInfo[] methods = handlerType.GetMethods(BindingFlags.Public | BindingFlags.Instance);
foreach (MethodInfo m in methods)
{
TryBuildCallCache(m);
}
}
/// <summary>
/// build a complied expression tree - much faster than calling MethodInfo.Invoke
/// </summary>
/// <param name="m"></param>
private void TryBuildCallCache(MethodInfo m)
{
if (IsHandlerMethod(m))
{
var parameters = m.GetParameters();
var expParamMessage = parameters[0];
var expParamSessionId = parameters[1];
var messageParam = Expression.Parameter(typeof(Message), "message");
var sessionParam = Expression.Parameter(typeof(SessionID), "sessionID");
var instance = Expression.Constant(this);
var methodCall = Expression.Call(instance, m, Expression.Convert(messageParam, expParamMessage.ParameterType), Expression.Convert(sessionParam, expParamSessionId.ParameterType));
var action = Expression.Lambda<Action<Message, SessionID>>(methodCall, messageParam, sessionParam).Compile();
_callCache[expParamMessage.ParameterType] = action;
}
}
static public bool IsHandlerMethod(MethodInfo m)
{
return (m.IsPublic == true
&& m.Name.Equals("OnMessage")
&& m.GetParameters().Length == 2
&& m.GetParameters()[0].ParameterType.IsSubclassOf(typeof(QuickFix.Message))
&& typeof(QuickFix.SessionID).IsAssignableFrom(m.GetParameters()[1].ParameterType)
&& m.ReturnType == typeof(void));
}
/// <summary>
/// Process ("crack") a FIX message and call the registered handlers for that type, if any
/// </summary>
/// <param name="message"></param>
/// <param name="sessionID"></param>
public void Crack(Message message, SessionID sessionID)
{
Type messageType = message.GetType();
Action<Message, SessionID> onMessage = null;
if (_callCache.TryGetValue(messageType, out onMessage))
{
onMessage(message, sessionID);
}
else
{
throw new UnsupportedMessageType();
}
}
}
}
非常简单美妙,MessageCracker是抽象类,不能直接实例化,所以在之前的例子中,MyApplication实现了MessageCracker,所以在实例化MessageCracker的时候,就会执行Initialize方法,把MyApplication类型传进去。获取MyApplication中定义的所有公共或实例方法,然后针对每个方法调用TryBuildCache方法。
在TryBuildCache方法中,收先通过IsHandlerMethod过滤掉需要缓存的方法,在IsHandlerMethod方法中,我们需要的是方法是:方法名为OnMessage,有两个参数,第一个参数是Message的子类,第二个参数是SessionID类型,返回值为void的类型。这里九八MyApplication中所有的以OnMessage开头,并且具有两个参数,第一个参数为Message子类,第二个为SessionId的都过滤粗来了。
接下来,在TryBuildCache方法中,通过表达式树,构造了lamda表达式编译成了委托。这些委托参数为Message,SessionId,返回值为void,就是MyApplication中定义的那些OnMessage方法。最后将这些方法以第一个参数Message的具体类型作为key,方法的委托作为value,缓存到了_callCache中,这里的处理非常美妙,对委托的缓存能够极大提高效率。
最后,当用户调用Crack方法时,即在MyApplicaiton中的FromApp里调用MessageCrack中的Crack方法时,首先根据Message的实际类型,从_callCache中根据Key查找对应的Action委托,如果有就调用,这些委托就是之前在MyApplication中定义的那些OnMessage方法。
结语
在QuickFix/n的MessageCracker中,巧妙的使用了缓存表达式树产生的反射的方法,通过调用自身Crack方法,根据参数类型,调用所有实现了MessageCracker方法的子类里的OnMessage方法,相对于需要强制实现接口或者虚方法OnMessage,这里而是采用了”约定大于配置“的思想,只要是通过固定”OnMessage“方法名,以及第一个参数为Message子类,第二个参数为SessionID的方式,提供了极大的灵活性,因为实现了MessageCracker类的子类,并不需要全部去解析所有的消息类型,只需要实现需要的消息,并以OnMessage方式实现即可。如果这里使用接口或者虚方法,这对于子类所有接口或者虚方法都需要实现,太痛苦。当然这里缺点时,约定的OnMessage方法必须正确拼写。
最后,这一实现展示了如何使用缓存表达式树生成的委托来优化性能,从而使得程序更加灵活而高效的一个例子。
参考
- http://www.codewrecks.com/blog/index.php/2008/10/04/expression-tree-vs-reflection/
- http://www.codewrecks.com/blog/index.php/2008/10/09/again-on-expression-tree-vs-reflection/
- http://quickfixn.org/
- https://www.cnblogs.com/fode/p/10079630.html