临下班的紧急任务
时钟指向6点半,张大胖今天不太忙,想着今天终于可以早点儿下班了。
收拾好东西准备离开的时候,领导布置了一个新任务,张大胖很无奈,哀叹一声,老老实实地坐下来。
新任务看起来非常简单:从一个CSV文件中读取数据,形成Java对象,然后对外提供一个API,让别人调用。
这个CSV文件叫做employee.csv, 张大胖打开这个CSV文件,里边的内容一看就懂。
name,age,level
Andy,25,B7
Joe, 22, B6
张大胖的API就需要返回一个List
public class Employee{
private String name;
private String age;
private String level;
......
}
class中的每个字段和csv文件的“表头”的“列名”保持一致。
这样简单的任务对张大胖来说是小菜一碟,他写了一个EmployeeParser,专门解析CSV文件,形成Employee对象,半个小时不到就收工了,赶紧下班!
还没来得及溜走,又被领导叫住了:“大胖,那个CSV文件新加了一个字段,叫做salary ,快把你的程序改一下啊!”
name,age,level,salary
Andy,25,B7,3000
Joe, 22, B6,2500
张大胖极不情愿地坐下来,给Employee类增加了一个salary的字段,又修改了EmployeeParser类,增加对这个字段的解析。
然后又听到领导在喊:“又加了一个字段,叫做tax !”
没辙,继续修改Employee类和EmployeeParser类吧。 这一次修改完,领导终于放他走了。
模板:用程序来生成程序
等了两趟车,终于在西二旗挤上了13号线,张大胖心里一直在想:明天保不齐还要增加字段,这真是让人厌烦的重复劳动啊。大家都说,Don't repeat yourself, 我这怎么才能减少重复呢?
关键点就在于,那个Java类的字段要和CSV的表头的列名做对应,CSV变化了,Java类的字段以及解析的方法都要做相应得修改才可以。
对了,能不能根据CSV的列名自动地生成那个Employee类啊,这样问题不就解决了吗? CSV变化, Employee类跟着变化,多好!
CSV的“列名”经过读取,可以变成一个Java 的List ,例如["name","age","level"], 如何写一段代码,把这个List变成一个Employee Class呢?
张大胖聚精会神,在地铁上想了一路,完全无视地铁上那拥挤的人群和污浊的空气。
快要下车时,他灵机一动,可以用模板技术嘛,比如velocity模板,定义一个employee.vm :
public class Employee{
#foreach ($field in $headers)
private String $field;
#end
##其他代码略
}
然后再写一个代码生成器,读取employee.csv的“表头”,形成List,把List传递给这个employee.vm模板,就可以输出Java类了:
写成具体的代码就是这个样子:
VelocityEngine ve = new VelocityEngine();
...初始化引擎的代码略...
Template template = ve.getTemplate("employee.vm");
VelocityContext context = new VelocityContext();
List<String> headers = readCSVHeaders();
context.put("headers",headers);
Writer writer = new PrintWriter(new FileOutputStream(
new File("C:\\Employee.java")));
//把headers变量传递给模板
template.merge(context, writer);
writer.flush();
(友情提示:可左右滑动)
(码农翻身注:这里做了简化只关注了Empployee的字段,还需要处理getter/setter方法,尤其是也需要通过模板的方法生成EmployeeParser,用来形成Employee对象。此外还有数据类型的问题。)
在小区对面的田老师红烧肉吃了一份盖饭以后,张大胖立刻投入到程序的编写中来,一边写一边想:我这是用程序来生成程序啊!
元编程
第二天,领导果然要加新的字段了,张大胖心中暗自佩服自己的自知之明,调出昨晚写的“宝贝”执行了一下,不到一秒钟,新的Employee和EmployeeParser就生成了。
下午的时候,张大胖洋洋得意地给Bill展示自己的工作成果,Bill说:“不错啊,都开始元编程了!”
“元编程?”
“对啊,你不是用程序来生成程序嘛,这就是一种元编程。”
张大胖没想到的工作居然就是高大上的“元编程”,更高兴了。
“还有,如果把CSV文件看成数据库的表,代码生成器自动生成的EmployeeParser不就相当于DAO吗?Employeeb 不就是和数据表映射的Domain对象吗? 你的代码实现了Object-relational mapping !”
就是啊,我怎么没想到,虽然距离真正的O/R Mapping还很远,但思想是一致的,大神就是厉害,看透了本质,张大胖暗想。
可是Bill很快给它泼了一盆冷水:“不过这种用模板生成的方式还是有些‘低级’,每次CSV文件有变化,都需要运行一下代码生成器才可以。”
“那怎么办?”
“其实吧,这个Employee的类没有必要在编译期存在,如果能在运行时动态地生成就行了。”
运行期动态生成? 张大胖有点懵。
“对于Java语言来说,运行期在内存中动态生成一个Class,还是有难度的,你需要透彻理解Java Class的文件格式,还需要在底层需要用ASM这样的东西去操作Java字节码。”
“文件格式和字节码?就是那些0xCAFEBABE,iload ,iadd, putfield,invokespecial ? ” 张大胖看过虚拟机的书,知道有很多字节码,但是操作它们形成符合要求的类,实在是难以想象。
Bill 笑道:“你可以用动态语言,比如Ruby,元编程很强大,实现你这个功能简直是小菜一碟。”
Bill很快就写出了一段代码:
#在内存中创建一个名称为Employee的类
klass = Object.const_set("Employee", Class.new)
names= ...读取csv文件第一行,形成数组,如 ["name","age","level"]...
#对这个内存中的类进行"手术"
klass.class_eval do
#现在 name,age,level...变成了这个Employee类的字段!
attr_accessor *names
#再定义一个Employee类的构造函数
define_method(:initialize) do |*values|
names.each_with_index do |name, i|
instance_variable_set("@" + name, values[i])
end
end
end
(友情提示:可左右滑动)
张大胖没有学过Ruby , 看到这里更懵了。
Bill看到张大胖发呆的样子,说道:”经过上述处理,内存中创建了一个类,如果把它的源码展示一下,你就明白了。”
#动态生成的类
class Employee
#动态生成的属性,类似与java的getter方法
def name
@name
end
#动态生成的属性,类似java的setter方法
def name=(str)
@name = str
end
def age
@age
end
def age=(str)
@age = str
end
def level
@level
end
def level=(str)
@level = str
end
#动态生成的构造函数
def initialize(*values)
@name = values[0]
@age = values[1]
@level = values[2]
end
end
#一个使用Employee类的例子
p = Employee.new("andy","22","B6")
(友情提示:可左右滑动)
(码农翻身注:对CSV文件内容的读取没有包括在其中。)
张大胖明白了,这个类是由数据驱动,动态生成的,CSV的header 中有多少字段,这个类就会生成多少个属性。
和自己的代码生成器比较了一下,Ruby写的这段代码更加精炼,不需要模板,没有所谓代码生成器,或者说,代码生成器和生成的类已经合二为一了。
即使是CSV文件发生了变化,也不需要额外运行代码生成器,只需要执行那段Ruby代码就行。
Bill问道:“怎么样,元编程不错吧?”
张大胖说道:“嗯, 这Ruby的元编程能力很强大啊,可惜的是,我们的项目都是Java的,这动态的脚本语言Ruby没法直接使用,如果是微服务,对外提供的是HTTP的API,我可以学学Ruby,单独写个Ruby项目。”
Bill说:“其实吧,编程语言中,元编程能力最强大的还属LISP,在LISP当中,程序和数据的表现形式是一致的,造就了它无以伦比的元编程能力,LISP程序可以像操作数据一样操作代码。 有人甚至说,LISP根本不是编程语言,它是编程元语言,专门为了生成程序而生。”
张大胖听得云里雾里,黯然道:“不知道你在说什么,太抽象了!等我学学LISP以后再回来和你讨论吧。”
(完)
码农翻身,用故事讲解技术本质, 更多精彩文章,请移步《码农翻身三年文章精华》