JParsec 与 JPJson
最近两天写了两个项目,一个是JParsec,另一个是JPJson.
JParsec
JParsec是Parsec的Java版本实现.由于暑假实习的时候已经写过了JSParsec,所以JParsec写起来还算是轻车熟路.但是说实话,动态语言写Parsec这种东西还是比Java这种静态的,朴素的纯面向对象语言写起来要舒服很多.由于JParsec涉及的类型转换比较多,所以写完JParsec之后,也算比较深刻的理解了为什么Java的类型系统这么招黑.
JPJson
JPJson是一个json数据解析引擎.对外设计的接口模仿了Google的Gson.
由用户提供一个Java Bean的Class对象和一段Json字符串调用fromJson方法之后即可返回一个JavaBean的实例.在实现JPJson的过程中由于要参考Gson的接口设计及相关实现,所以大量的翻阅了Gson的代码,学习到了不少的黑科技.
JPJson基于上面的JParsec实现,所以也间接证明JParsec也算是比较强大的.但是移植过两个版本的Parsec,却一直没有系统的学习过Haskell语言有点说不过去.学习Haskell的事情真的要开始提上日程了.
Why JParsec and JPJson?
在开发JParsec和JPJson的过程中呢,我也是第一次开始体验了TDD开发的模式.
为什么使用者两个项目作为学习实践TDD的起点呢.
实际上这也是这两个项目天然的特性决定的.
JParsec天生就是由一个个算子组成的,算子之间的耦合度极低,甚至可以说没有任何耦合度.这样的项目只需要按照TDD的一般方法来就可以了,不需要为了做单元测试而故意做什么设计,这在开始习惯单元测试的初期是非常有利的.
JPJson就是相对于JParsec而言耦合性稍微有点强的项目了.在开发完JParsec之后,已经基本熟悉了单元测试的使用,这时候开始开发JPJson库是最合适的,使用单元测试倒逼项目的设计与架构变得高内聚与低耦合,可以让我进一步体会TDD的好处.
Intelij IDEA中的单元测试的使用
在Intelij中使用单元测试功能非常的方便,只需要下面几步就可以了.
1. 创建一个用于测试的包结构
2. 打开想进行测试的类
3. 按下CTRL + SHIFT + T 选择Create a New test
4. 然后选择第一步中创建的包结构存放测试类,并且选择Junit4测试框架
5. Intelij会自动创建测试类,然后就可以写测试代码了.
JParsec中一个使用单元测试的实例
@Test
public void testSepBy() throws Exception {
State state = new TextState("a|a|a|a");
Parser s = AtomOperator.equal('|');
Parser eq = AtomOperator.equal('a');
Parser sepBy = CombinatorOperator.sepBy(eq,s);
List<Object> list = (List<Object>) sepBy.parse(state);
assertEquals(4,list.size());
assertEquals(7,state.pos());
}
在准备写SepBy这个算子之前,我写下了上面这个测试用例.
首先,SepBy算子接受的是一个State结构,所以我必须要构造一个State.
对于接受的State有什么具体要求,是不是只要实现了State接口就行了?SepBy算子是不是能够适应任何的State结构?
然后就是用例中的分割符号.这里选择了|
.
接下来就是构造算子的过程,这是又需要想清楚的问题就是SepBy是一个组合子,它应该接受几个算子作为参数?
分隔符的体现是不是要接受一个Char类型的或者String类型的参数?
经过考虑,为了更加广泛的适应性以及可拓展性,这里选择了分割符的匹配仍然使用算子来实现,由使用者自行构造分隔符算子,这样的设计就允许了更多的可能性.更多的类分隔符的情景也更加容易得到满足.
接下来就是返回值,这里可以返回数组,同时也可以返回Collection.各有各的好处,如果使用数组,就可以支持Primitive类型数组的直接返回,但是拓展性难以保证,并且没有充分利用到面向对象的特性.如果使用List看似很好,只是不能使用基本类型的数组而已,但是实际上还是挺麻烦的,因为SepBy
或者是Many
诸如此类的算子,返回值很大几率是一个char的集合.当使用List进行返回的时候,难以直接构造出一个String(大部分情况下人们都会这么做),即使可以使用toArray()
方法,也还是不行.这里就要吐槽Java的自动装箱机制了,虽然char和Character可以混着用,但是char[]和Character[]却不可以自动转换.
经过两相权衡,最终我选择了返回List类型.
接下来就是Junit库的功能了,使用assertEqual()方法,可以断言一次运行返回的结果.
这里有个小技巧,为了避免每次断言的时候都写上Assert.assertEquals(),我们可以使用java提供(se5)的静态导入功能
从上面就可以看出来,TDD让我在实现具体的功能之前,已经对整个功能有了一个比较具体的的概念
TDD开发体验
经过这两个项目的实践,我掌握了TDD的基本使用. 使用TDD在写代码之前强迫程序员先思考好某个模块的具体功能,边界条件等.为了能够达到比较高的测试覆盖率,TDD也会倒逼程序员改善整个项目的代码结构以便单元测试达到较高的测试覆盖率. 由于JParsec项目天然的特性,所以JParsec项目中测试的覆盖率达到了100%.