软件开发>杂文>2

共享软件开发的点滴心得

 [中文][English][日本語]

 [机器人制作] [软件开发] [豆知识] [Squeak] [关于]

 焚书不坑儒

刘新宇

2006年元月

 

ID,英文名称是Identifier,恐怕是很多人都熟悉的东西。它究竟是什么呢?打开你的钱包,也许里面有很多卡:银行卡,信用卡,打折卡,积分卡,俱乐部的年卡……,每张卡上都有一个唯一的序列号(serial number),这是ID的一种;走进超市,货柜上每一种商品都有一个条码,按照UPC或者EAN标准解码的话,也代表一个唯一的商品信息,这也是一种ID;身份证或者护照上有标明个人身份的ID;财务发票上有标识唯一交易的ID……。

 

在实际的开发中,相信大多数人都会处理带有ID的信息。无论是财务软件,网上论坛,交易系统,检索软件都会和ID打交道。但是似乎开发每次都要推翻重来,不同的要求、不同的内容,然而都要处理ID。也许某些人在自己的计算机的某个目录里存了一些老项目的代码,现在可以偷偷的copy-past,然后再find-replace。 这容易联想到古代中国,在推翻前朝“暴政”后,有的一把火烧个痛快——如“项羽”,然后再大兴土木,重新建造。也有的干脆采取务实的做法,直接利用前朝的 宫殿,如清朝利用明朝的都城。这是对待建筑,对待人也是如此。有的干脆杀个痛快,免得儒士“饶舌”,然后再开科举选拔本朝精英——如明朝的太祖皇帝。倒是 外族入侵时反倒大肆利用降臣降将。

 

现在,有这样一些前朝“遗老”、“遗少”,通常称为POD(Plain Old Data)

 

//

// 雇员信息

//

struct Employee{

     int       Number;

     string    Name;

     enum      JobType{ CONTRACT, PART_TIME, STANDARD};

     JobType   Type;

};

 

//

// 电子邮箱信息

//

struct Mailbox{

     short     Id;

     string    User;

     int       Size;

};

 

//

// 信用卡信息

//

struct CreditCard{

     string    SerialNumber; // 信用卡号遵循xxxx-xxxx-xxxx-xxxx的格式

     string    Owner;

     Date      ExpireDate;

};

 

[注:为了举例方便,免去字符串内存管理的诸多麻烦,这里用了C++标准中的string而没有用char*,所以严格意义上已经不是POD]

 

明显看出,这些前朝儒士,老得可以,即不支持流行的ctorcopy-ctor;也不能直接拿来比较、排序;更不用说有gettersetter从而能够serialization了。一点也不方便本朝管理,而为我所用。目前的问题是,有没有什么办法,能够在不必“屠城”般的推倒重来,而“书同文”,“车同轨”呢?

 

假设经过我朝“励精图治”,现在“河清海晏”、“有凤来仪”,准备“开科取士”,办法如下:

  1. 所有人才要有能唯一识别身份的唯一ID
  2. 不同类人才,分门别类进行选拔
  3. 所有人才要能够自行进入规定的考场(这里指直接放入STL容器)
  4. 进入考场后按顺序对号入座(这里指能够直接比较大小)
  5. 计算出考场中ID不连续(间隔〉1)的数量,以安插……“张好古”这样有背景的“人才”[4]

 

办法一出,考官就头疼了。上面列出的那些“前朝儒士”,第一,第二条倒是满足。可是后面的几条,就完全不成了。但是要是“顾炎武”、“吕留良”这些名人不能为本朝所用,皇帝(客户)又会不开心。于是第一套蹩脚的方案就出来了:

 

方案一,特别考场

你前朝遗老遗少不是特别么?那好我们就设置特别考场,专门给你老人家用,一人一个单间。其他考生么,就都进入贡院参加乡试和会试。本朝依照面向对象的这套方案设计如下:

 

给“顾炎武”们特设的考场:

class EmployeeExamRoom{

public:

// 由于不能直接放入STL容器,只好传递数组指针和元素个数了

     int test(Employee* members, int n){

          // 冒泡法按照ID排序,从小到大

          for(int i=0; i<n-1; ++i){

              int minID=i;

              for(int j=i+1; j<n; ++j)

                   if(members[j].Number < members[minID].Number)

                        minID=j;

              if(i != minID)

                   swapEmployee(members[i], members[minID]);

          }

 

          // 计算能够预留出来的位置

          int tables=0;

          for(int i=0; i<n-1; ++i)

              if(members[i+1].Number-members[i].Number>1)

                   ++tables;

          return tables;

     }

 

     void swapEmployee(Employee& a, Employee& b){

          Employee temp;

          temp.Number= a.Number;

          temp.Name  = a.Name;

          temp.Type  = a.Type;

 

          a.Number=b.Number;

          a.Name = b.Name;

          a.Type = b.Type;

 

          b.Number=temp.Number;

          b.Name = temp.Name;

          b.Type = temp.Type;

     }

};

 

给“吕留良”们设计的考场

“吕留良”们的性格特殊,他们的ID基本上能反映他们的出生先后,年长的ID大,年轻的ID小,但是他们坚持主张“长幼有序”,年轻的断然不肯坐在年老的前面。所以这个考场只好把座位按照ID从大向小排(逆序):

class MailboxExamRoom{

public:

     int test(Mailbox* members, int n){

          // 这次相反,从大到小排逆序

          for(int i=0; i<n-1; ++i){

              int maxID=i;

              for(int j=i+1; j<n; ++j)

                   if(members[j].Id > members[maxID].Id)

                        maxID=j;

              if(i != maxID)

                   swapMailbox(members[i], members[maxID]);

          }

 

          // 计算预留位置时,也要考虑逆序的因素,用前面的减后面的。

          int tables=0;

          for(int i=0; i<n-1; ++i)

              if(!(members[i].Id-members[i+1].Id>1))

                   ++tables;

          return tables;

     }

 

     void swapMailbox(Mailbox& a, Mailbox& b){

          // 内容差不多,为避免灌水嫌疑,略去了。

     }

};

 

给“黄宗羲”们设计的考场

黄宗羲”们又再次让考官们头疼了一次,他们的ID竟然是xxxx-xxxx-xxxx-xxxx形式的字符串!根本无法用“大于号>”和“小于号<”来计算。更为不方面的是,仅仅后四位不同的的情况下,可以肯定他们是出自同一门派(同一银行发行的信用卡),安插进“张好古”的话,肯定会被认出。所以这个考场设计如下:

class CreditCard{

public:

     int test(CreditCard* members, int n){

          // 只好写一个特殊的排序函数了

          for(int i=0; i<n-1; ++i){

              int maxID=i;

              for(int j=i+1; j<n; ++j)

                   if(IsSmall(members[j], members[maxID]))

                        maxID=j;

              if(i != maxID)

                   swapCreditCard(members[i], members[maxID]);

          }

 

          // 计算距离也要用单独的方法

          int tables=0;

          for(int i=0; i<n-1; ++i)

              if(distancd(members[i], members[i+1])>1)

                   ++tables;

          return tables;

     }

 

     bool IsSmall(const CreditCard& a, const CreditCard& b){

          return strcmp(a.SerialNumber.c_str() ,

    b.SerialNumber.c_str()) <0

     }

 

     int distance(const CreditCard& a, const CreaditCard& b){

// 忽略掉后4

          return abs(toNumber(a.SerialNumber) –

 toNumber(b.SerialNumber))/10000;

     }

 

     long long toNumber(string str){

          long long res=0;

          for(int i=0; i<4; ++i)

              res = res*10+str[i]-'0';

          for(int i=5; i<9; ++i)

              res = res*10+str[i]-'0';

          for(int i=10; i<14; ++i)

              res = res*10+str[i]-'0';

          for(int i=15; i<19; ++i)

              res = res*10+str[i]-'0';

          return res;

     }

 

     // swap的实现就暂时省略了

};

 

考官辛辛苦苦设计好这三个考场,突然收到皇帝的密旨:“朕闻除顾炎武、吕留良、黄宗羲外、尚有查继佐、王夫之、孙奇峰、傅山、朱之瑜等众未归本朝教化,其下门生众多,各自持绝学,流于山野。望卿等此次开科,尽纳此类贤才,以显我天朝容恕之策……”。

 

“人才爆炸”、“考场爆炸”(类爆炸[2])的灾难是本朝“面向对象”这样的“儒学”理论无法解决的。好在“汉家自有制度,本以霸王道杂之,奈何纯任德教?[3]”看来这个问题,也要用“霸王道”了。

 

方案二、殿试——只有一个考场

还记得《西游记》里面,为了归化孙悟空,太白金星给玉皇大帝出的主意吧。孙悟空也是“跳出三界外,不在五行中”,所以就弄个弼马温这样的官给他作。别看官小,好歹是按照天庭规则“正规化”了,所以后面什么都好办。

 

现在的方案也是这样,给这些前朝遗老遗 少,每人委派一个专门的“书童”秘书,负责接待事宜,除考试之外的一切杂事,均由此“书童”代理。这个书童,可就是本朝的“公务员”了,自然好办。进入考 场的事由“书童”负责,“搀扶”诸位老儒士入场,并带领他们对号座。考场也就设置一个,大家统一殿试。本朝就不是缺人,书童有得是,不怕你“人才爆炸”。

 

那么这个“书童”公务员,应该具备什么样的素质,才能出任呢?归纳起来大致分以下两类:

第一类,管理遗老本身,包括其ID和个人数据等内容。

第二类,协调统一遗老极其门下弟子之间的关系,包括长幼顺序,亲疏距离的计算等等。

 

用术语描述,大致如下:

第一类策略(policy[2]

//

// Resouce Holder Policy

// 1. provide ID type

// 2. provide Data type

// 3. provide get_id() method

//

 

第二类策略(policy

//

// Resouce Operation Policy

// 1. provide eq()

// 2. provide lt()

// 3. provide diff()

//

 

[正如Andrei[2]中指出的,C++语言本身,并不提供一种描述Policy的方法,所以我们只好用注释描述它们]

 

为了专人专用,我们索性把“书童”也分成两类,一类专门负责照顾“遗老遗少”的衣食住行,一类专门负责协调他们的关系,每两个书童组成一对“搭档”。这样,针对“顾炎武”们委派的第一个“书童”如下:

 

struct EmployeeHolder: public Employee{

     typedef int ID;

     typedef Employee Data;

    

     EmployeeHolder(){}

     EmployeeHolder(const Data& v){

Number=v.Number; Name=v.Name; Type=v.Type;

}

     EmployeeHolder& operator=(const Data& v){

          Number=v.Number; Name=v.Name; Type=v.Type;

          return *this;

     }

    

     const ID get_id() const { return Number; }

};

 

这个“书童”看上去好像其貌不扬,出奇的简单,然而他却是个初步符合本朝规范的“小顾炎武”。至少支持ctor, copy-ctor以及等号操作。只不过由于不能协调互相间的关系(比较大小,相等,做减法以计算距离)尚不能完成全部任务。

 

然后再给这个“书童”配备一个搭档:

 

template<class T>

struct NormalOp{

     bool eq(const T& v1, const T& v2) const {

return v1.get_id() == v2.get_id();

}

     bool lt(const T& v1, const T& v2) const {

return v1.get_id() < v2.get_id();

}

     typename T::ID diff(const T& v1, const T& v2) const {

return v1.get_id() - v2.get_id();

}

};

 

这个“搭档”不认人,管你是“顾炎武”也好,是“吕留良”也好,只管协调关系,所以他有可能身兼数职,即和“顾炎武”的书童作搭档,也和“王夫之”的书童作搭档。

 

现在,这对“书童”搭档可以带领“顾炎武”们参加殿试了。殿试时一律统一,不认个人,在考官看来,所有人员一律可以描述如下:

 

template<class DataHolder, template<class> class Op = NormalOp>

class Resource: public DataHolder, public Op<DataHolder>{

private:

     typedef Resource<DataHolder, Op>  _Self;

     typedef typename DataHolder::Data _Data;

public:

     Resource(){}

     Resource(const _Data& v):DataHolder(v){}

     _Self& operator=(const _Data& v){

          DataHolder::operator=(v);

          return *this;

     }

 

     const bool operator==(const _Self& v) const {

return eq(*this, v);

}

     const bool operator< (const _Self& v) const {

return lt(*this, v);

}

     typename DataHolder::ID operator- (const _Self& v){

return diff(*this, v);

}

 

     //to get data, just use up-cast to <Data>

};

 

所以,“顾炎武”和他的两个“书童”必须先准备一下:

typedef Resource<EmployeeHolder, NormalOp> EmployeeResource;

现在他们成了一个整体,这个整体完全符合本朝的所有5条规范,可以直接进入“殿试”了。

 

class ExamRoom{

public:

     template<class T>

     static int test(vector<T> coll){

          // 既然支持比较运算符,可以直接用STL

          sort(coll.begin(), coll.end());

          for(vector<T>::iterator it=coll.begin();

it!=coll.end(); ++it)

              cout<<it->get_id()<<" ";

 

          // 计算能够预留出来的位置

          int tables=0;

          for(unsigned int i=0; i<coll.size()-1; ++i)

              if(abs(coll[i+1]-coll[i])>1)

                   ++tables;

          cout<<"\n"<<tables<<"\n";

          return tables;

     }

};

 

这个“殿试”的方案不仅简单,而且通用,因为它不依赖于任何具体的考生。也许现在还看不大明白,不过可以针对“顾炎武”们初步测试一下了:

int main(int argc, char* argv[]){

     typedef Resource<EmployeeHolder> EmployeeResource;

     const Employee person[3]={{3, "Tom", Employee::CONTRACT},

                              {1, "Jack", Employee::PART_TIME},

                              {4, "Mike", Employee::STANDARD}};

     vector<EmployeeResource> coll;

     for(int i=0; i<3; ++i)

          coll.push_back(person[i]);

     ExamRoom::test(coll);

}

测试结果如下:

1 3 4

1

 

到此,就可以给出完整的方案了:

针对“吕留良”们的“书童”搭档:

struct MailboxHolder: public Mailbox{

     typedef short ID;

     typedef Mailbox Data;

    

     MailboxHolder(){}

     MailboxHolder(const Data& v){Id=v.Id; User=v.User; Size=v.Size;}

     MailboxHolder& operator=(const Data& v){

          Id=v.Id; User=v.User; Size=v.Size;

          return *this;

     }

    

     const ID get_id() const { return Id; }

};

 

template<class T>

struct ReverseOp: public NormalOp<T>{

     bool lt(const T& v1, const T& v2) const {

return v1.get_id() > v2.get_id(); // 实现逆序

}

};

 

好处在这里就体现出来了,虽然“吕留良”们和“顾炎武”们的学术内容千差万别,但是却被这一对“书童搭档”统一起来。“殿试”前只要这样准备一下就可以了:

typedef Resource<MailboxHolder, ReverseOp> MailboxResource;

 

现在可以再测试一下:

const Mailbox box[3]={{2, "Inbox", 128},

                     {1, "Outbox",32},

                     {5, "Draft", 5}};

 

vector<MailboxResource> coll2;

for(int i=0; i<3; ++i)

     coll2.push_back(box[i]);

ExamRoom::test(coll2);

结果如下:

5 2 1

1

注意,已经实现了逆序排序了。

 

最后,为了完整,再给出针对“黄宗羲”的一对“书童”搭档,这里用了一些小技巧,首先考虑前面的方案一的实现中,最终也是用toNumber函数把字符串变为64位整数,所以索性“黄宗羲”的书童,就背地里直接向上汇报方便使用的64位整数ID

struct CreditCardHolder: public CreditCard{

     typedef long long  ID;

     typedef CreditCard Data;

    

     CreditCardHolder(){}

     CreditCardHolder(const Data& v){

          SerialNumber=v.SerialNumber;

Owner=v.Owner;

ExpireDate=v.ExpireDate;

     }

     CreditCardHolder& operator=(const Data& v){

          SerialNumber=v.SerialNumber;

Owner=v.Owner;

ExpireDate=v.ExpireDate;

          return *this;

     }

    

     const ID get_id() const {

          ID res=0;

          string::const_iterator it=SerialNumber.begin();

          for(int j=0; j<4; ++j){

              for(int i=0; i<4; ++i, ++it)

                   res = res*10+(*it)-'0';

              if(it!=SerialNumber.end())

                   ++it;     //跳过信用卡号中的"-"字符

          }

          return res;

     }

};

 

template<class T>

struct CreditOp: public NormalOp<T>{

     typename T::ID diff(const T& v1, const T& v2) const {

          return (v1.get_id() - v2.get_id())/10000; //忽略后4

     }

};

 

现在可以简单的测试一下:

typedef Resource<CreditCardHolder, CreditOp> CreditCardResource;

     const CreditCard card[3]={

{"0514-2048-3016-3210", "Tom", "08/05"},

          {"0514-2048-3016-1024", "Jack", "06/07"},

          {"0514-2048-3012-4231", "Mike", "06/12"}};

 

     vector<CreditCardResource> coll3;

     for(int i=0; i<3; ++i)

          coll3.push_back(card[i]);

     ExamRoom::test(coll3);

 

结果如下:

514204830124231 514204830161024 514204830163210

1

 

完全符合要求。到这里,也许“翰林院主流学派”的官员要“上疏”了:“此举不合圣贤,不法先王,非为面向对象之典型实现……”云云。那么究竟哪里有些不对,引起非议了呢?

 

按照“儒家经典”传统的面向对象+设计模式的方案,大概是这样实现:

首先定义一个抽象的AbstractResource基类(或曰接口),然后再定义针对“顾炎武”们之类的Adapter,当然这些Adapter继承自AbstractResource,同时各个子类里面包含哪些老的PDO,并且实现接口定义的虚函数例如getID()之类的内容。算法(例如ExamRoom::test)则针对抽象的接口设计。

 

然而,一旦实行这套“先王之法”,就会发现有行不通的地方。例如“顾炎武”,“吕留良”们各自IDtype不同,有int形式的,有short形式的,还有字符串形式的。这就注定了AbstractResource中虚函数getID的头疼问题——返回什么类型呢?紧接着,是各个子类不同的排序准则由谁来实现?由各个子类实现?由抽象基类实现?或者是由单独的其他类实现而交给ExamRoom::test去决定该调用谁?

 

看起来古怪的“霸王道杂之”方案实际上是可 行的,虽然有一套看起来“父子颠倒”的继承关系,却能够达到“焚书不坑儒”的非血腥方式“文化重建”。然而究竟能否实施还要看“上意”,中国过去改朝换代 的历史中,有太多的情况是“杀一批人,焚部分书”。不论是明君还是暴君,而且这种推倒重来有的甚至延续几代君王,所以我们看到了太多的“文案”和“文字 狱”。这些都值得许多读书很好,但生存能力却很差的“聪明人”思考。

 

参考书目:

[1] Eric Gamma, etc. Design pattern: Elements of Reusable Object-Oriented Software. Addison-Wesley.

[2] Andrei Alexandrescu, Modern C++ Design: Generic Programming and Design Pattern Applied. Addison Wesley, Feb. 2001

[3] 黄仁宇《赫逊河畔谈中国历史》从霍光到王莽

[4] 刘宝瑞《连升三级》



[English][上一篇][下一篇][目录][主页][联系我们: liuxinyu95@gmail.com]