《冒号课堂》连载之十九——情景范式

 

4.4  情景范式餐馆里的编程范式

理论是认生的孩童,多陪他玩玩,自会活泼起来。

题记

关键词:编程范式情景编程闭包规则引擎

  :编程范式在餐馆中的应用

预览

厨师只须提供一种服务:把纸上菜变成盘中菜,至于蒸、煮、炒、炖等具体做法纯属实现细节。

可以这么理解(闭包):所谓包,指函数与其周围的环境变量捆绑打包;所谓闭,指这些变量是封闭的,只能为该函数所专用。

合理地使用闭包能使代码更加简洁清晰,散发出函数式特有的优雅气质。

换盘子有两种方式:一种是服务员主动换,一种是客人要求换。前者是轮询,后者是通知。

如果把待加工的菜看作数据,技法看作算法,将数据与算法分离,以算法为中心,那是什么范式?

提问

什么是闭包?为什么被称为闭包?它有什么作用?

规则引擎有何用处?

能否设想一个生活中的场景,把介绍的9种编程范式都用上?

讲解

叹号摘下眼镜,揉了揉眼:“范式再好,多了也难免有些审美疲劳。”

逗号也搓着太阳穴:“这段时间脑子被灌得沉甸甸的。”

“彼此彼此!你们的脑袋老闹涝灾,我的喉咙老闹旱灾。”冒号说着,拿起矿泉水瓶一饮而尽。

大伙听着怪别扭的,这不是拐着弯说我们脑子进水了吗?

冒号清了清嗓子:“为尊重民意,也为避免消化不良,大家先轻松一下。下面我们来个情景编程。”

“情景编程?没听说过,只听说过情景英语。”问号感到挺新鲜。

“都是学语言嘛,有何两样?”冒号轻描淡写,“让我们试着用生活中的实例将一些编程范式串联起来。前面提到,OOP可以看作管理一个服务型公司,现在以餐馆为例,你们每人设计一类对象及其提供的服务。”

问号来了兴致:“我先来吧。构造一个前台接待员,负责迎客、引座、送客。”

句号很是不满:“还真不客气,上来就把最漂亮的对象抢走了。”

台下一阵笑声。

“我来构建最常见的服务员。”逗号一捋袖子,一副准备开干的样子,“负责斟茶、写菜、上菜、换盘。”

“嗯,很熟练。”冒号一本正经。

句号实在得很:“我设计收银员,专管收账、出具发票。”

引号颇为自豪:“我造一个技术含量最高的大厨,专门负责烹调。”

逗号不服:“你倒简单,那么高的技术含量,敢情炒肉和炖肉一个做法啊?”

引号自觉理亏:“那就负责蒸、煮、炒、炖吧。”

冒号为其辩护:“引号同学并没有错,可惜没能坚持。厨师只需提供一种服务:把纸上菜变成盘中菜,至于蒸、煮、炒、炖等具体做法纯属实现细节。”

叹号有点委屈:“唉,看来我只好做技术含量最低的厨工了,负责食品预加工、洗碗、打扫清洁。”

冒号将大家设计的类翻译成Java代码

// 前台接待员

Class Receptionist

{

    public void receive(Customer customer)  {…} // 迎客

    public void usher(Customer customer)    {…} // 引座

    public void send(Customer customer)    {…} // 送客

}

 

// 服务员

Class Waiter

{

    public void pourTea(Customer customer)    {…}   // 斟茶

    public List<Order> write(Customer customer){…} // 写菜

    // 上菜

    public void serve(Customer customer, Course course){…}

    public void exchangePlate(Customer customer) {…}   // 换盘

}

 

// 收银员

Class Cashier

{

    public void charge(Customer customer)    {…}    // 收账

    public void issueInvoice(Customer customer){…} // 出具发票

}

 

// 厨师

Class Cook

{

    public Course cook(Order order)        {…} // 烹调

}

 

// 厨工

Class KitchenHand

{

    public void prepareFood()        {…} // 准备食品

    public void washDishes()         {…} // 洗碗

    public void clean()               {…} // 打扫清洁

}

“你们造人,我来造物。”冒号构造了一个餐馆的类

// 餐馆

Class Restaurant

{

    // 每当有顾客来访,返回该顾客

    private Customer accept() {…}

 

    // 为指定顾客提供所有的餐馆服务

    private void serve(Customer customer) {…}

 

    // 餐馆服务

    public void service()

    {

        while (true) // 无限循环,假设餐馆7×24小时营业

        {

            final Customer customer;

            if ((customer = accept() ) != null) // 某顾客来访

            {

                serve(customer);  // 为该顾客提供服务

            }

        }

    }

}

冒号解说道:“这里accept类似Socketaccept,属于堵塞呼叫,意味着此方法将堵塞进程直至收到新数据。为简单计,把一行顾客当作一个Customer。大家对此段代码有何看法?”

“没什么,很简单啊。”逗号说完补充一句,“关键是serve方法的实现。”

“这里我们明显用到了两个范式,对象式过程式。”冒号提示道。

引号会意:“应该还需要并发式。serve如果与service在同一线程中运行,那么餐馆只有等服务完一个Customer后才能服务后面的,这显然是荒唐的。”

“对极了!”冒号将“serve(customer);”改写为

// serve(customer);  // 错误地使用单线程!

new Thread             // 构造一个线程

    (new Runnable()

    {

        public void run(){ Restaurant.this.serve(customer); }

    }).start();           // 启动该线程

冒号道:“这回serve在新线程中运行,不会耽误Restaurant服务下一位Customer了。”

问号眼尖:“我注意到声明customer时前面加上了关键字final,有必要吗?”

“如果不用线程,是不必要的。”冒号回应道,“我们在建造线程时用到了实现Runnable接口的匿名类(anonymous class),它是涉及局部变量customer的内部类(inner class),Java语法要求该局部变量必须是final类型。值得一提的是,这里不仅用到了并发式,而且与函数式也密切相关。”

“函数式?”逗号奇道。

“不错。”冒号坚定地点着头,“我们不是提过函数式中的函数是头等公民吗?也就是说,函数与其他基本数据类型一样,可以作为传递参数、返回值或与变量名绑定。闭包(closure)便是这样一种函数,并且还能保留当初创建时周围的环境变量。以上匿名类本质上是函数serve的包装,经实例化后作为参数传入Thread的构造函数,并且记住了外部类的局部变量customer—这也是为什么它必须是final以保证不被重新赋值的原因。应该说这是一种OO化的闭包形式,预计在Java 7中它的用法会更简洁,这也意味着Java代码中将飘进更多的函数式风味。”

引号喃喃道:“以前只听说过数学里有个闭包的概念。”

冒号略加指点:“可以这么理解:所谓,指函数与其周围的环境变量捆绑打包;所谓,指这些变量是封闭的,只能为该函数所专用。如果你真懂数学,就会发现它们本质上是相通的。”

叹号隐隐约约地觉得:“把函数与变量捆绑起来,怎么听起来像是OOP的做法啊?”

“嗯,的确相似。”冒号微颔,“不妨认为闭包就是封装了环境变量的隐形对象的方法—通常是匿名方法。我们用一段JavaScript代码来加深印象—”

/* 返回函数f的近似导函数 */

function derivative(f)

{  // dx最好作为参数传入,放在此处主要是为了说明闭包的用法

    var dx = 0.00001;  // dx越小,近似度越高

    return function(x) { return (f(x + dx) - f(x)) / dx; };

}

 

/* 返回一个数的平方数 */

function square(x) { return x * x; }

 

/* 返回一个数的双倍数  */

function double(x) { return 2 * x; }

 

/* 对任何一个不大的数值变量x(比如小于100),下列函数的返回值应该非常接近于零 */

function testSquareDerivative(x)
{ return derivative(square)(x) - double(x); }

叹号感到有点头痛:“怎么跑出了微积分?大学学的那点高数早就还给老师了。”

冒号笑着安慰他:“还给老师没关系,我再借给你一点:平方函数的导数是双倍函数。因此,函数derivative(square)应该很接近函数double的作用效果。作为检验,testSquareDerivative能将任何一个不大的数映射到近似于零的数[1]。”

引号这下彻底明白了:“在求导函数derivative中,传入的参数f和返回值都是函数,这是函数作为头等公民的特征。其中返回的匿名函数就是闭包,它附着了两个环境变量:外层函数的传入参数f和局部变量dx。”

“完全正确!”冒号作出积极的肯定,“如果不是闭包,这两个环境变量在derivative返回后就会失去效用。由此可见,合理地使用闭包能使代码更加简洁清晰,散发出函数式特有的优雅气质。”

句号有些按捺不住编程的冲动,自告奋勇:“我来具体实现餐馆的serve方法吧。”

得到冒号的默许,句号在黑板上写下如下代码

private void serve(Customer customer)

{

    // 找一个空闲的接待员

    Receptionist receptionist = findReceptionist();

    receptionist.receive(customer);

    receptionist.usher(customer);

    // 找一个空闲的服务员

    Waiter waiter = findWaiter();

    waiter.pourTea(customer);

    List<Order> orders = waiter.write(customer)    ;

    // 将菜单交给一位厨师

    Cook cook = waiter.pass(orders);

    for (Order order : orders) // 厨师照单做菜

    {

    Course course = cook.cook(order);

        // 找一个空闲的服务员

        waiter = findWaiter();

        // 服务员上菜

        waiter.serve(customer, course);

        // 顾客开始享用

        customer.eat(course);

    }

 

    // 顾客用餐完毕……

    // 找一个空闲的收银员

    Cashier cashier = findCashier();

    cashier.charge(customer);

    cashier.issueInvoice(customer);

    // 找一个空闲的接待员

    receptionist = findReceptionist();

    receptionist.send(customer);

}

句号写毕又复查一遍,拍拍手上的粉笔灰,心满意足地走下台来。

叹号提意见:“我的厨工没派上用场,应该在厨师烹调前调用KitchenHandprepareFood方法。”

问号挑出另外的毛病:“在for循环中,厨师、服务员和顾客的行为应该在不同的线程中,厨师不可能等服务员上完一道菜或顾客吃完一道菜后才做下一道。”

“可能更复杂呢!”逗号也来凑热闹,“一位顾客点的几样菜可能分别由几位厨师同时做,每位厨师都在不同的线程中工作。”

引号更严谨:“还应有一个后台线程,让服务员(Waiter)随时换盘(exchangePlate),让厨工(KitchenHand)随时洗盘(washDishes)和清洁(clean),这样所有服务人员提供的服务都用上了。”

句号倒抽凉气:“估不到漏洞这么多,并发式真是无处不在啊。”

冒号继续点拨:“换盘子有两种方式:一种是服务员主动换,一种是客人要求换。前者是轮询,后者是通知。”

“哦,事件驱动式!”句号迅即反应过来,“客人是事件源,服务员是事件处理器,客人不定期地招手呼唤是在发表事件以通知服务员。客人与服务员是多对多的松耦合关系。”

冒号点点头,又指着引号:“刚才有人不满你的大厨职责过于简单,现在你来实现一下,也好显显技术含量。”

引号在台上摸了半天头,编出如下代码

Class Cook

{

    public Course cook(Order order)

    {

        // 根据菜单查食谱

        Recipe recipe = lookupRecipe(order);

        // 找到食谱的烹调步骤

        List<Instruction> instructions =

          recipe.getInstructions();

        for (Instruction instruction : instructions)

        {

                follow(instruction); // 按食谱的指令操作

        }

    }

}

“堂堂大厨原来是靠查食谱做菜的。”逗号揶揄道。

引号为难地说:“这不是在编程嘛,好端端的人脑,不得不去模拟电脑,完全搞倒了。”

“要设计会烹调的机器人,兴许还真得这样呢。”冒号笑道,“不过由于各种菜式组合繁多,如果每种菜都配菜谱未免太庞杂,如何精简呢?”

句号建议:“菜式成千上万,烹调技法相对少许多,不妨以技法为主线。”

“好主意!”冒号挑起大拇指,“如果把待加工的菜看作数据,技法看作算法,将数据与算法分离,以算法为中心,那是什么范式?”

泛型式!”大家异口同声。

“至此我们已涉及了过程式、对象式、并发式、函数式、事件驱动式和泛型式。”引号扳着手指算着,“还差逻辑式、元编程和切面式了。”

冒号把目光转向逗号:“写菜单并不容易,如果客人不直接点菜,你的服务员如何向他推荐?”

逗号答:“最简单的方法是报菜名,并一一询问客人。”

冒号皱眉:“这样你是简单了:一个迭代就完事,可客人也该发火了。”

逗号赶紧修正:“先询问客人的口味,忌讳,等等,再向他建议一些菜式。”

“这还差不多。”冒号眉头舒展开来,“考虑到客人的口味、忌讳等各有不同,餐馆的菜单也随时可能变化,如果把这些都硬编码(hardcode),再加上层层叠叠的if-else语句,代码将成为懒婆娘的裹脚—又臭又长又难维护。”

引号提议:“可以把这些信息预先存入数据库,届时用SQL查询。”

“想法很好,只是有一点难度。”冒号提醒道,“这些信息并非简单的对应关系,包含一些逻辑推理,甚至需要一些模糊判断。”

句号一拍大腿:“前面不是提到领域特定语言DSL吗?将所有规则用自定义的DSL编写,再利用元编程转换成CJava之类的通用语言,不是很好吗?”

“棒极了!”冒号不吝赞词,“不过还有一种思路。我们可以搜集餐馆的菜式、顾客口味,忌讳,以及各种菜与口味、忌讳之间的关系等一系列事实和规则,用规则语言(Rule Language)来描述,通过规则引擎(Rule Engine)来导出符合顾客需求的菜肴。这种方式将业务规则与应用程序分离、将知识表示与逻辑实现分离,是SoC原理的一种应用,同时也是一种逻辑式编程。”

问号关心地问:“这些规则引擎与Java程序兼容吗?”

冒号回答:“不少规则引擎用Java实现或专为Java平台设计,如JessDroolsJLisaJRules等,Sun还发布了javax.rules APIJSR 94)以统一对各类引擎的访问接口。另外在.NET平台上也有业务规则引擎,Microsoft Business Rule EngineMS BRE)和Windows Workflow Foundation WF)中的WF Rules均提供了业务规则引擎。”

引号颇感意外:“既然是逻辑式编程,为什么不采用代表语言Prolog呢?”

冒号准备了一大段理由等着他:“刚才提到的规则引擎多是基于Rete算法[2]的,主要采用数据驱动的(data-driven)正向推理(forward chaining)法,而Prolog引擎采用目标驱动的(goal-driven)逆向推理(backward chaining)法。正向推理自底向上,利用推理规则从已有的事实数据推出更多的数据,直到达成目标;逆向推理正相反,自顶向下,从目标出发寻找满足结论的事实[3]。相比而言,正向推理适合针对不同输入作出不同反应,而逆向推理适合回答查询。现在是服务员根据客人的喜好提建议,当然用正向推理更合适。再说这类引擎与Java的集成更加方便,因此我们没有选择Prolog。”

讲到此处,每个人都意识到,只剩下最后一个范式了。

冒号提出一个新问题:“假如餐馆经理接到顾客投诉,反映服务人员态度不好,卫生状况也不理想,应该怎么办?”

问号抢先说:“首先我的接待员在迎客(receive)时要笑容可掬地对顾客说:‘欢迎光临!’,在送客(send)时要对顾客鞠躬:‘请慢走,欢迎下次再来’”

逗号接着说:“我的服务员在上完菜后应对客人说:‘请慢用’,句号的收银员也应加些礼貌用语,让人家高高兴兴地掏钱。”

句号补充道:“服务员在上菜(serve)前、厨师在烹饪(cook)前应洗手,厨工在洗碗(washDishes)后应对餐具消毒。”

冒号紧接着问:“如果餐馆对礼貌规范或卫生标准做修改,必然要牵扯不同类中的不同的方法,维护起来很不方便,怎样才能有效地解决这个问题呢?”

答案已经昭然若揭了。

冒号干脆自问自答:“不错,正是用切面式编程。只要创立两个AspectEtiquetteSanitation,分别负责礼貌规范和卫生标准方面的事务。一旦某一方面的要求发生变化,比如餐馆来了外宾,或者碰上非典或禽流感,只须在相应的Aspect模块中作调整:将礼貌用语换成英语或者提高卫生标准,等等。如果采用runtime AOP,甚至还可在运行期选择激活或禁用这些Aspect。”

下面开始有些骚动,大伙早已脑中满满而腹中空空,有点头重脚轻了。

冒号见状,遂发出激动人心的号召:“今天的课到此结束,让我们从虚拟的餐馆中走出,到真实的餐馆中去吧!”

众人齐声欢呼。

总结

Ø  闭包是一种能保留当初创建时环境变量的函数。它通常以匿名的方式存在,多用于函数式编程中,能使代码更加简洁清晰。Java中的匿名类可以看作OO化的闭包形式。

Ø  Java平台上的JessDroolsJLisaJRules.NET平台上的MS BREWF Rules都是规则引擎,主要基于正向推理。它们提供了逻辑式编程环境,能有效地将业务规则从应用程序中分离出来,提高了软件的灵活性和可维护性。

Ø  每种编程范式都能在生活中找到它的应用,它们本来就是人类思维方式的投影。

参考

[1] WikipediaClosure (computer science)
http://en.wikipedia.org/wiki/Closure_(computer_science)

[2] Ernest Friedman-HillJess 7.1p2 manual
http://www.jessrules.com/jess/docs/Jess71p2.pdf

[3] Mark Proctor等.Drools Documentation
http://downloads.jboss.com/drools/docs/4.0.7.19894.GA/html_single/index. html

 

插语

[1]若输入数过大,则须要设定更小的dx。此外,还可能产生计算溢出。

[2]Rete算法是一种高效的模式匹配算法,用于实现规则生成系统(production rule system)。文中提到的规则引擎除WF Rules外都是基于该算法的。

[3]用形式逻辑的语言来说,正向推理顺着从前件(即if语句)到后件(即then语句)的方向,逆向推理顺着从后件到前件的方向。

 

 

欢迎转载,转载时请注明:

本文出自电子工业出版社博文视点(武汉)新书《冒号课堂——编程范式与OOP思想》。

http://www.china-pub.com/196068&ref=ps

http://www.douban.com/subject/4031906/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值