版权声明:本文由韩伟原创文章,转载请注明出处:
文章原文连接:https://www.qcloud.com/community/article/246java
来源:腾云阁 https://www.qcloud.com/communitypython
做者介绍:韩伟,1999年大学实习期加入初创期的网易,成为第30号员工,8年间从程序员开始,历任项目经理、产品总监。2007年后创业4年,开发过视频直播社区,及多款页游产品。2011年后就任于腾讯游戏研发部公共技术中心架构规划组,专一于通用游戏技术底层的研发。程序员
假设咱们但愿开发一套通用型的软件框架,这个框架容许用户自定义大量不一样的状况下的回调函数(方法),用来实现丰富多彩的业务逻辑功能,例如一个游戏脚本引擎,那么,其中一个实现方式,就是使用观察者模式,以事件的方式来驱动整个框架。用户经过定义各个事件的响应函数,来组织和实现业务逻辑。而框架也提供了自定义事件及其响应函数的入口。在一些实现代码中,咱们可能会发现有大量的“注册事件”的代码,或者是使用一个巨大的switch…case…对事件函数进行分发调用。譬如咱们想作一个服务器端的基本进程框架,这个框架让用户只须要填写一些回调函数,就能成为一个稳定持续运行的后台服务进程。其中一个部分,就是须要定义程序启动事件,以便用户自定义程序启动要作的事情。那么咱们能够定义一个”Init”的字符串来表明这个事件,在一个事件响应函数的回调哈希表里面,记录上”Init”pfunInit()。又或者是用一个常量宏INIT=12来表示此事件,在程序的主循环处,利用switch…case…来检查表明每一个事件的类型编码,若是发现是和INIT宏相等的,就调用case INIT下面的代码(每每是一个单独的函数,如pfunINit())web
维护长长的“注册事件”代码和长长的switch…case…都同样的让人昏昏欲睡,同时容易让人错漏百出。这些代码每每还带有大量的“常量”,由于用来做为回调函数的key的数据,每每都是一些自定义的常量。这些常量的同步维护,也每每让人筋疲力尽。这些长长的代码清单,常常还都须要由多个开发者一块儿来使用,天然就很容易发生你错改了个人,我覆盖了你的这一类问题。这些问题很是的“低级”,可是要找起来却一点都不容易。
[游戏的按键控制代码/JS]算法
难道咱们的框架代码中,就必定会充斥着长长的字符串常量,或者整数常量吗?答案是否认的,由于不少编程语言,都提供能反射的功能。在编译型语言如C/C++里面,也能够利用代码生成技术,模拟出相似反射的能力。数据库
要想知道什么是反射,咱们能够先来看一个观察者模式的例子。假设咱们在编写一个GUI的程序:在一个窗体上安放了一个按钮,此按钮的名字叫“ButtonA”,当这个按钮按下的时候,咱们但愿有一个咱们本身写的函数被调用。根据观察者模式的设计,这个按钮被用户按下后,程序底层应该能监测到这个事情,而后在进程内部产生一个“事件”,这个“事件”对象每每会带有这个信息:被按下的按钮名字。若是咱们用之前的注册事件的方法来编码,咱们必需要在按钮被按下以前,好比程序初始化的时候,就向观察者对象注册这样一个回调函数:RegisterEvent(“ButtonA”, ONCLICK, myOnClick) —— ButtonA
被按下的事件—myOnClick()
。这里的函数myOnClick()
就是咱们想处理ButtonA被按下的事件的响应函数。可是,咱们能够用另一个更省事的方法来解决:咱们把myOnClick()函数的名字改为ButtonA_OnClick()
,而后观察者在发生“ButtonA”被按下的事件后,自动去找有没有叫“ButtonA_OnClick
”这个名字的函数,若是找到的话,就调用这个函数。——显然这种作法无需预先手工去注册回调函数,而是仅仅根据函数名字的约定,简单的来决定要调用什么函数。通常来讲,咱们认为程序运行的过程当中,这些函数名字、类名字、属性名字都不起什么重要的做用,以致于咱们还会用一些“混淆器”软件来处理源代码,把这些自定义的名字都弄的乱七八糟,也不影响程序的运行。然而,若是咱们使用反射的技术,程序就能够在运行时,实时的用一些常量,来检索而且得到源代码中,函数、类、属性名字所对应的实体,而且还能调用这些东西。
[在Java里经过字符串类名反射构建一个对象]编程
反射这种功能,在编译型的C语言程序中,几乎是不可以使用的,由于C语言源代码中的名字“常量”,都被分离成“符号表”,而后在连接的过程当中从二进制可执行程序中去掉了。虽然动态连接库会保留部分相似反射的能力,可是也仅仅限于动态连接库的接口函数。在C++中,因为编译器支持RTTI(运行时类型检测),咱们能够经过typeof()操做符得到任何一个对象的类型信息,但咱们仍是不能实施用一个常量在运行时直接调用一个函数或对象的操做。不过,若是咱们使用IDL(接口定义语言)来用程序生成C++的源代码,却是能够把对象构造器函数、成员函数等等的名字常量,做为一个Map的key存放起来,对应把这些函数做为value放入Map,这样实现相似反射的功能(前提是要反射的对象都须要用IDL来描述,不然就要本身手工写一堆注册名字—函数的代码)。设计模式
若是咱们使用基于虚拟机的语言,好比C#或者JAVA,又或者脚本语言,如python, Lua, JavaScript这些,都很是适合使用反射功能。因为虚拟机在运行时是能彻底掌控全部代码的“符号表”,因此使用语言系统提供的一些API,就能很方便的经过任何一个字符串常量,查找这个常量对应(在源代码中)的类、方法、成员属性等等。数组
在咱们懂得反射的用法后,咱们就能够发现,源代码再也不是“数据结构+算法”这么简单的东西。咱们能够利用源代码做为数据自己的载体。一个最简单的例子,就是XML的解析:咱们能够定义一个和XML文件对应的类,这个类的成员属性的名字,和须要解析的XML文件结构中的字段名一致。当咱们在解析对应的XML文档的时候,就能够经过XML内容中的字段名,找到对应类成员属性对象,而后把XML字段值赋值进去。而这个过程当中,只要咱们按照XML文档的结构来定义类,就能很方便的把XML文档内的数据,赋值到一个类对象里面,这对于编写冗长的解析、赋值代码来讲,能介绍很多的代码篇幅。这种作法也许不是很是高效,由于反射查找自己须要额外的CPU消耗,可是,若是解析XML这个步骤不是“关键路径”,这点性能损失对比大段的相似代码,仍是很值得的。服务器
反射用于配置的另一个功能,是把类名、方法名放在配置文件里面,做为程序功能的配置项。之前咱们若是想要利用配置文件,来定制一个程序的行为,必需要在源代码中编写一段switch…case,来把行为函数和配置文件中的配置值对应起来。这对于频繁修改、增长这些可配置行为的框架来讲,是一个很是难以维护的工做。可是,若是咱们利用反射,就能够直接在配置文件中写入对应行为的类名或方法名,这样框架就能够经过这些常量名字,在运行时找到进程空间中对应的类、对象、方法,从而直接调用他们以生效。这方面最多见的场景,有Tomcat这一类web容器,它们每每把一个个对应不一样URL处理的servlet对象的类名,写入到配置文件中。或者如Spring框架,把互相依赖的各个对象的类名,都用配置文件管理起来,在运行时根据这样的配置文件,实时的反射出对应的类和对象,创建按配置要求的对象关系来。
[Spring经过XML来配置对象的关系]
从代码维护的角度来看,类、成员、方法的名字,被程序之外的一些“配置文件”所管理和知道,是有必定风险的。由于咱们经常不把配置文件当作是源代码那么重要的东西,错漏也没有编译器或者IDE协助,因此一些难以调试的BUG每每是从这些位置产生的。不过做为一种大大节省框架代码的技术,仍是受到普遍欢迎。而上文所说的问题,如今渐渐由另一种技术“元数据”(或者叫注解、特性),把配置文件和源代码合并起来,这样就能大大改善上述的问题。
咱们在编写通讯功能的程序时,传统的思路是要定义协议,也就是定义协议头部,协议包长度,协议包字段等等。在一个比较复杂的网络服务程序中,这样的协议很容易就有几十上百个。维护代码的程序员想要搞明白别人定义的如此众多的协议,其实是不太容易的。咱们很容易想到,能不能使用对象模型来代替通讯协议的定义呢?答案是能够的。可是,使用对象模型又有一个新的问题:对象是一个在运行时的内存结构,如何把对象中的数据,经过网络接收和发送呢?最简单的作法,就是使用memcpy(),Linux提供了这个功能强大的API,可让任何内存中的数据变成一段字节数组,而后咱们就能直接经过网络发送了。可是,若是咱们的对象不是一个简单的结构体(事实上简单的结构体也有问题),而是一个对象,这个对象里面可能存在指针类型的成员,这样的拷贝就不可能顾及到这些指针指向的数据了。并且,若是收发两端的程序,并非同一种语言(操做系统、平台),这样的内存结构数据可能毫无心义,好比把一个C++的对象内存直接拷贝给JAVA程序,确定没法直接使用。因此,咱们想要用对象结构来定义通讯协议,咱们须要一个把对象转换成通用的字节数组的方法,这就是“序列化/反序列化”的能力。在这里我不打算说太多关于序列化的内容,我只想说,当这些对象具有序列化能力后,就能成为通讯数据的载体。问题是,若是咱们收到了一段对象序列化的数据,如何构建出对应数据的对象呢?答案就是使用反射,反射机能能从数据中得到对象类的名字,而后经过这个名字构造出对象来,而后从数据中继续得到余下成员的数据,一一复制到这个对象身上。由此看,只要咱们有反射功能,咱们可让使用者,简单的构造一个对象,而后整个把这个对象发送给网络的另一端,对方也能直接收到一个对象,这样在编写通讯程序的时候,只要按照业务需求定义对象便可。对于阅读代码的程序员来讲,不用在脑子装一根叫“编码、解码”的弦,只要“无脑”的定义、处理对象便可。
在通讯程序中,有种叫命令模式的设计模式很是常见,它脱胎于传统的基于命令字的网络处理方式:解析出命令字经过switch…case调用对应的处理函数。命令模式下的通讯程序每每很简单,就是定义一个类型,这个类型的成员属性(通讯协议)是能够随便定义的,只要再定一个Process()方法便可——这个方法的内容,就是收到此类型对象,应该如何处理的容器。因为咱们利用反射能够在网络另一段重建这个对象,因此咱们也能够调用这个预约义的Process()方法,这个方法因为和协议对象类定义在一块儿,因此它是知道全部的成员定义的,这样这个处理方法,就无需好像之前的程序那样,费劲的经过强制类型转换,来获得具体的数据内容。在命令模式的通讯程序实现过程里,反射是相当重要的一环,由于当咱们收到一个数据包时,必需要从数据包中获得其对应的对象的类名,而后创建这个类所对应的对象。一旦这个对象创建后,咱们能够调用其反序列化函数,让对象的内容和数据包中一致,最后调用其Process()方法,就大功告成了。这种设计,能够用不一样的语言,定义同结构的类对象,用来在不一样的语言平台程序之间通信,而无需定义很复杂的协议定义规范。一些强大的对象数据工具,好比Google Protocol Buffer和Apache Thrift,直接能够用一个通用的IDL语言,生成各类语言的类定义源代码,就更方便了。
[Thrift、PB的自动序列化/反序列化的类型字段]
在我刚刚接触Delphi这款IDE的时候,我惊叹于它那便利的功能:能够对任何一个控件对象进行图形化的编辑。虽然咱们能够用初始化的代码,来对任何一个对象进行修改,可是直接在IDE界面修改这些属性,仍是很是方便的。甚至我会经过这些属性界面,来猜想和学习一款控件的用法。像这类功能,每每背后就须要反射的力量(固然delphi可能不是使用反射,而是利用组件模版等技术实现)。当咱们本身开发一个这样的程序,咱们必需要把一些对象、类的内部结构读取出来,而后才能以另外的途径展现出来。
[delphi上用界面设置ADO数据库控件的属性]
在JAVA中,JavaBean就是一个著名的利用反射来使用的“对象约定”:只要你编写的JAVA类型,其成员是相似setXXX()
或者getXXX()
的,不少框架都会自动识别和处理这些成员函数,从而实现诸如自动更新成员数据,自动关联界面内容等功能。另一个相似的例子是JMX,这个JAVA的通用监控标准接口,能够把你定义的类对象解析出来,成员属性的值能够变成统计图线、可修改的表格项,方法变成按钮。在游戏开发领域,反射还普遍的用于,把图形美术资源和程序代码结合的目的:好比Flash Builder就能够经过反射,把一个Flash动画对象,绑定到一个MovieClip类型上,从而得到一个既具有美术效果,又能让用户自定义行为的对象。Unity3D在绑定了3D的游戏对象和脚本组件后,对于脚本中的Start()/Update()函数调用,也是经过反射进行的,这样开发者就没必要要把脚本的类型,死死的和某个基类绑定到一块,并且这些反射调用的函数,仍是能够有不一样的返回值(不一样的函数原型),从而实现协程或者非协程的调用。
[在flash编辑器里,对一个动画指定关联的自定义类]
反射因为能够把源代码中的信息提取出来,和其余的数据结合,让源代码的能力大大的提高,因此在开发工具方面,具备很是重要的地位。咱们再也不须要经过写代码,一遍遍的把源代码的数据和外部结构作对接,而是简单的开发一个反射能力框架,就能让咱们实现某种源代码的“约定”,从而实现各类丰富的快捷开发能力。
在反射的使用过程当中,咱们每每会发现,源代码直接做为数据,仍是会有一些问题。譬如咱们的源代码可能会根据一些非业务因数作修改,更名、改参数类型是在重构的时候很是常见的。因此咱们每每仍是离不开配置文件,把源代码里的名字写到配置里面,而后框架再根据配置来运行。一个比较典型的例子就是Hibernate,这一款著名的ORM框架,能让你的源代码类型和数据库、表结构关联起来。按理说利用反射,咱们能够直接创建一些和数据库表、字段名字同名的对象,就能直接关联了,可是咱们的源代码若是须要修改这些名字,再去改数据库的内容,就显得太麻烦了。因此咱们要编写不少配置文件,来关联什么表对应什么类,什么字段对应哪一个属性……这些配置文件每每和使用数据库的表数量同样多,任何的修改都还要记得对应这些配置的修改,咱们被迫同时维护:数据库结构、配置文件、源代码这三个东西。然而,若是咱们的平台是支持“元数据”的话,问题就很好解决了。由于咱们能够在源代码里面直接写配置文件项目。咱们在源代码的类名前面,用相似注释的方式,标注这个类对应数据库的哪一个表;在属性名前面,用注释标注对应的字段、默认值等等。这样咱们只须要维护两个东西:数据库结构、源代码。这大大的减轻的项目的复杂程度。
我接触的最先最著名的元数据,是用来同步修改API文档的JavaDoc技术,这个技术让更新文档再也不成为一个苦力活。因为能够在源代码的注释里面编写文档,因此在修改代码的同时也能够同时更新文档。更重要的是,javadoc标记天然的把源代码中的“名字表”和相关注释自动对应起来了,要知道,这种对应若是人工来作,但是要费至关大的功夫。在javadoc的教育下,我对于java的注解、C#的attribute(特性)都以为很是亲切。之前那些须要登记大量类名、方法名的配置,通通均可以直接记录在源代码里面了。而一些和美术资源关联的客户端代码,也能够经过源代码的特殊标记,链接上正确的图形资源。
能让这些源代码里面的“元数据”生效的重要技术,其实就是反射。因为咱们的元数据处理程序,通常都须要和源代码里面的类、方法名字对应起来,因此都要使用反射的方法。而这种反射,又为咱们任意增长“元数据”提供了强大的机制。
咱们曾经相信:数据结构+算法=程序。可是从今天的软件产业来看,当然仍是有不少专事计算的软件在被开发着,然而咱们接触到更多的软件,都是所谓“信息管理系统”类的软件。这类软件要处理的并不是是复杂的计算任务,而是对各类各样现实世界中的信息,增删查改是这些信息处理最通俗的描述。咱们在处理这些信息的时候,若是仍是把程序的载体源代码,仅仅当作是编译过程当中不可缺乏的一环而已,那么咱们就必须额外处理大量的数据形式:数据库、配置文件、IDE配置……然而,在面向对象的风潮之下,源代码彻底能够做为一种“树状”的数据承载方式。面向对象定义的类、成员、方法,就是一个个现实世界中的实体映像,他们所包含的结构和常量,每每直接能够成为系统中的数据源头。在MUD文字游戏中,几乎整个游戏世界,都是以源代码常量的形式编写的,这不但没有成为维护的难题,反而让真个游戏的开发变得更轻松,由于程序员仍是最习惯于面对源代码去工做。
反射这种特性,能把源代码中的全部数据,包括“名字符号表”,都提供给开发者去使用,让软件开发过程,从单纯的算法实现过程,变成一个综合的信息管理的过程。这个作法看起来彷佛不够专业,可是在编程已经不算“高科技”的年代,这种技术能帮助大量的开发者,以某种“约定”的方式去编写源代码,从而自动得到框架的强大支持。——制造这种容许“约定”方式运行源代码的框架,正式新的框架应该拥有的特色,由于人类的创造时间,不该该被浪费在大量的重复而相似的工做之上啊!