前传,母模 method chaining
我们要用 antlr+stringtemplate 开发的东西,是 method chaining 的生成器。
因此,我们得先知道 method chaining 个什么样子。正如当我们想做个蜡烛小东
西的时候,我们得先做出这个小东西的母模来。
母模与最终的批量产品看起来样子完全相同,但是,母模是用手工打造的。
method chaining 是实现 Fluent interface 的一种手段,这俩在 wikiepdia
上都是条目,详情请自己去查。
Fluent interface 是软件工程中的一种方法,希望能让代码更可读。试对比:
方案A:
o_mario->pipe_a(123);
o_mario->pipe_b();
o_mario->pipe_c();
方案B:
o_mario->pipe_a(123)->pipe_b()->pipe_c();
或者
方案C:
o_mario->pipe_a(123)
->pipe_b()
->pipe_c();
是不是觉得方案B和方案C的可读性更好一些?况且,我们少键入几次o_mario,
也减少了错误的可能。
Fluent interface 的提出者是牛人 Martin Fowler。如果你读书不注意作者的
话,可能会不记得他。他的作品包括 分析模式(不是四人帮的设计模式),UML
精粹,重构,Domain-Specific Languages 等。他是敏捷方法、极限编程、UML
和模式领域的专家,还推动了 依赖注入(控制反转)一词的流行 。
以上八卦结束,无外乎想说明 method chaining 这技术系出名门,有纯正的民间
血统。
Fluent interface 能够让调用这一方法的程序员写出的代码,看起来像是一种
领域定义语言,更专门,也更容易被本领域的专家(或人类)识读。也就是说,
读者可以经受更少的训练即可读懂。
凡是具有平易近人特性的东西,似乎也都具有另一个特性,那就是精美的包装。
而包装是需要代价的。
不过,我们遵循这样的原则:当你是一个库函数的程序员的时候,库函数的接
口,应该以令使用它的人感到愉悦为原则。你在此时多花费的时间,不仅楚人失
之楚人得之,而且将以十倍百倍在别人那里得到节省。
这也正是我们的价值所在。那种认为"我依赖你对你撒娇,那都是看得起你"的
人,可能从来没想过它的反命题,"我不允许你依赖不允许你撒娇,那都是看得起
你",因为把你视为平等的人类而不是低一等级的什么。
工程中没有小情小调。你真的以为现在撒娇的你,将来遇到问题的时候能够替大
家挡下风雨么?
所以,如果我们是库函数程序员,我们要提供令别人觉得享受的接口去调用。
method chaining就令人愉悦,所以我们要考虑一下怎么实现了。
我们的盟友需要的效果是这样的:
o_mario->pipe_a(123)->pipe_b()->pipe_c();
那么,我们考虑一下实现。
1. o_mario->pipe_a(123),第一个函数的调用
o_mario(严格的说,是这个指针类型的变量的类类型)需要有一个方法,这
个方法是pipe_a.这个容易,就是这样:
代码1:
class mario
{
public:
pipe_a(int par);
};
以上是头文件中的内容,函数的实现非常简单,先不讨论.
2. o_mario->pipe_a(123)->pipe_b(),第二个函数的调用,及第一个函数的声明
后面这个pipe_b()是个什么东西呢?
这应该是另一个方法。谁的方法呢。从库函数使用者的角度看,它应该是
o_mario->pipe_a(123) 这段代码的返回值 的成员函数。
这里有两件重要的事。第一,重申:它应该是o_mario->pipe_a(123) 这段代码的
返回值 的成员函数。因为pipe_b()的前面有 ->,所以无疑的,前面是个类的实
例的指针。看不懂的同学,请回去复习一下C语言中的structure的成员变量如何
引用 和 这个structure的指针如何引用成员变量。
第二,一种思维方法。当我们讨化如何实现的时候,我们可以从接口(广义的)
的形势入手,而不从对实现的猜测本身入手。也就是说,我们先明确它应该是个
什么样子,而不是应该如何实现。因为在这里,接口,是动机,要首先明确,而
实现手段,可以有千千万万,我们一个个穷举起来代价比较大。
以上两件重要的事讨论完毕,我们再回头来看,这句话有点长:
o_mario->pipe_a(123)->pipe_b()中的pipe_b()是 o_mario->pipe_a(123) 这段
代码的返回值 的成员函数。
即 第二个函数,是第一个函数的返回值的成员。
理解了这一点以后,我们可以再进入一步问,我们需要的是第一个函数返回值的
成员,那么,第一个函数的返回值是什么东西么?
它应该是一个类类型(class type)实例的指针。
哪一个类类型为好呢? class mario 就非常适合。
所以,我们把代码1改一下,确定第一个函数的返回值类型,变成下面这样:
代码2:
class mario
{
public:
mario* pipe_a(int par);
};
以上是这个函数的"接口",即怎么去调用它。
3. 第一个函数的实现
我们需要的已经明确,下一步才是如何得到。
在这里,我还是想再一次强调,明确自己需要的是什么非常重要,远远比知道如
何去实现要重要。始终坚守并提醒自己想要的是什么,希望能有效地避免走上与
自己的愿望相反的道路。
WG同学提到过一个问题,如果把一群人关起来,不让他们了解外部的世界,给他
们好吃好喝,或者教育他们认为自己得到的是最好的,这是否给了他们幸福。
这个问题可以用另外的两个似乎无关的事情来回答。
一是,有日本人称中国人为支那人,我们很不喜欢,认为受到了侮辱。有人提到
过,那不就是个名字么,为什么认定这是侮辱。提这个问题的人显然没有意识到
这样一个道理:当你认定自己受到了侮辱的时候,那么就是受到了侮辱。这与发
出行为的人的动机甚至关系都不那么紧密。所谓尊重,就是不做对方认为侮辱的
事情。
所以前面提到的类似观点"我欺负你正是爱你",可以问问对方,他喜欢这样的爱
么。这类似于百年前的男人问问自己的老婆,我殴打你才是爱你,你接受么?
对方不接受的,就不是他想要的。当我们讨论对方的感受时,我们必须讨论对方
的感受,而不是你的感受。你没看错,我说的就是"当我们讨论对方的感受时,我
们必须讨论对方的感受",即,当我们讨论A时,我们必须讨论A。
道理朴素到可以归结为 A就是A,但是有些人仍然不懂,因为他们加了很多附加
条件,却无视这些条件都与A无关。
WG同学的方案里,如果被关起来的那些人觉得好,那就是好呗。问题是,那些人
*真的*觉得好么?
家长包办婚姻(这个词对你来说,是不是史前时代的古拉丁语)的时候对孩子
说,"我都是为了你好啊"。且住,那得由你来判定。
第二个回答WG同学的例子。扯淡的。我喜欢一个人,我把他,对不起,我把她捆
起来,不放走,每天喂猴头燕窝鲨鱼翅。最后她终于逃脱,找来一群人要揍我。
我可不可以说:至少,你吃到并消化了那么多好东西,那都是我卖血换来的啊。
你如何报偿我呢。
这个例子如此浅显,以至于你都愤慨了吧。但是它和WG同学那个看似合理的方案
有一个共同点,即 被捆起来饲养的那位,她愿意吗。
愿意,意愿,希望得到的是什么,这非常重要。这比如何实现重要一百倍。
以上扯淡结束。我们已知库函数使用者想要的是 代码2。其实,至此我们还没有
做一点自己的库函数开发的工作,只是明确了盟友的需求。
应该是这样实现的。
代码3,加了行号。
1 mario* mario::pipe_a(int par)
2 {
3 std::cout << "I am running in " << __FUNCTION__ << std::endl;
4 this->data = par;
5 std::cout << "data: " << data << std::endl;
6 return this;
7 }
其中,第1行就是声明的重复。
第3行和第5行,是为了调试的时候方便,能看着点啥。__FUNCTION__ 是编译成
debug版内置的,函数名。第4行,是为了显示效果,有一个成员变量,data,int型
的。
第6行是有意思的一行,它的作用,即我们刚刚讨论的,我们的盟友需要的,返
回值。
返回值是什么呢?是this指针。它是一个指针,指向了这个类类型的这一个实例。
所以,pipe_a 和 pipe_b 是同一个实例(的指针)调用的成员函数,这些方法
的数据将存储在(或读取自)同一个实例中。也正因为同一实例这一点,经常有
人用 method chaining 来初始化类类型的变量。
比如这样;
代码4,出自[http://en.wikipedia.org/wiki/Fluent_interface#C.2B.2B]
FluentGlutApp(argc, argv)
.withDoubleBuffer().withRGBA().withAlpha().withDepth()
.at(200, 200).across(500, 500)
.named("My OpenGL/GLUT App")
.create();
4. o_mario->pipe_a(123)->pipe_b()->pipe_c()
我们目前实现到 pipe_b(),容易看出,后面的 pipe_c() 与 pipe_b() 没有区
别。
这样,代码5:
mario* mario::pipe_c()
{
std::cout << "I am running in " << __FUNCTION__ << std::endl;
return this;
}
5. 调用者
我们替盟友写一段代码,测试一下是否能工作,然后再交付。交付的时候才发现
很多麻烦没有解决,无论是多小的麻烦,都应该认识到,那是我们的责任,不能
推给库函数使用者去完成,因为他不替你领工资。
我们把这段调用库函数的代码称为 driver,推动事情运行的东西。
代码6:
#include <mario.h>
int main(int argc, char *argv[])
{
mario* o_mario = new mario();
o_mario->pipe_a(123)->pipe_b()->pipe_c();
delete o_mario;
return 0;
}
6. 对生成 method chaining 的展望
这样,我们需要三个文件:
mario.h 头文件,类及其成员的声明;
mario.cpp cpp文件,类及其成员的定义;
go.cpp driver文件,负责调用mario的函数们,即
o_mario->pipe_a(123)->pipe_b()->pipe_c()。
7. 重提母模
如果我们仅只需要马利这样一个类,那么手写就可以了,手写完交给库函数程序
员成千上万次像driver那样调用.
但是我们可能需要很多个这样的类,而它们的结构如此类似。所以,我们需要大
量地成批地生成它们。
对于每一个类,我们都需要 .h和.cpp文件各一个,用类的名字命名;所有这些
类,我们还需要一个driver.cpp,调用它们。
以上,明确了1.我们的代码需要生成,2.我们需要生成哪些东西,3.这篇博客的
主体,这些生成的代码都应该是什么样子的。
8. 附录,那些文件 的代码
以下是我们明天要生成的代码。它们现在也可以编译,这样:
g++ -I. *.cpp -o go
执行的时候:
./go
此外,从下面的driver.cpp中你可以看出,其实,我已经有了不止mario一个类。
它们都能通过我们接下来要介绍的杨氏语言编译器生成出来。
你猜到了,下面的代码,确实就是生成出来的。
8.1 mario.h
1 #ifndef _MARIO_H_
2 #define _MARIO_H_
3
4 #include <iostream>
5
6 class mario
7 {
8 private:
9 int data;
10
11 public:
12 mario* pipe_a(int par);
13 mario* pipe_b();
14 mario* pipe_c();
15 mario();
16 ~mario();
17 };
18
19
20
1 comment:
I commеnt each time I appreciаte a article on a ωebsite oг I have somеthing to valuable to contгibute tо the ԁіscuѕsion.
Usually it's triggered by the fire communicated in the post I read. And on this article "使用Antlr+Stringtemplate生成method chaining,一个不太简单的案例(2)". I was moved enough to post a comment :-P I actually do have 2 questions for you if it's oκaу.
Could it be just mе oг does it seеm likе a few
of the commеnts look like ωritten bу brаin
dead visitors? :-P And, if уоu are writіng
οn additional online siteѕ, I'd like to follow everything fresh you have to post. Would you list the complete urls of all your public pages like your twitter feed, Facebook page or linkedin profile?
Here is my web page - www.swaggybook.com
Post a Comment