論面向組合子程序設計方法 之 微步轂紋生

最近。age0提出了一個OO設計的問題。因爲這個例子更加貼近生活,是我們老百姓所喜聞樂見的商場折扣問題,所以我準備改鉉更張用這個例子了。具體的例子請看:
http://forum.iteye.com/viewtopic.php?t=17714&start=0

簡要的說,需求是:
[quote]有這樣一家超市,對顧客實行會員制,會員目前分爲兩個等級:金卡及銀卡。
每次會員購物時,都會根據會員等級提供不同的折扣優惠和返點。[/quote]
這個需求並不複雜。任何一個普通的java程序員都可以輕鬆搞定。
age0就給出了幾個方案供大家選擇。

一個大家普遍認可的方案是:
// client:

string id = input_id;

Member member = Members.GetMemberByID(id);;

int discount = member.GetDiscount();;
int point = member.GetReturnPoint();;

// service

class Members
{
static public Member GetMemberByID(string id);
{
string type = GetMemberTypeByID(id);;

switch(type);
{
case "金卡":
return new GoldenMember();;

case "銀卡"
return new SilverMember();;
}

return null;
}

static protected string GetMemberTypeByID(id);
{
string type;

// get type by id
...


return type;
}
}

class Member
{
protected Member();
{
}

virtual public GetDiscount();
{
return 0;
}

virtual public GetReturnPoint();
{
return 0;
}
}

class GoldenMember
{
override public GetDiscount();
{
return 10;
}

override public GetReturnPoint();
{
return 1.5;
}
}

class SilverMember
{
override public GetDiscount();
{
return 5;
}

override public GetReturnPoint();
{
return 1;
}
}

也就是說,你不是說不同級別的客戶有不同的折扣[b]策略[/b]嗎?策略模式啊。老子design pattern白學了?哈哈。

這個方案其實不錯。簡明易懂。可以輕易擴展出白金卡,鑽石卡,九天十地菩薩搖頭怕怕霹靂金剛雷電卡等等。

不過,age0同學開始憋壞了。他不知道跟老闆進了什麼讒言,老闆愣插進來亂七八糟地提了一大堆新的需求。比如:
[quote]對於女性會員,決定在3.8當天在原來的優惠基礎上增加5個百分點的折扣[/quote]
所謂“始作俑者,豈無後乎?”,壞消息還不算完,總結起來,目前的需求如下:
[quote]
1. 會員等級折扣,會員等級是最重要的分類,大部分方案都會在等級上面作文章,所以單獨歸爲一類
2. 條件型折扣,只有在符合特定條件下才會發生的折扣,比如前面針對女性會員節日的折扣
3. 貨物優惠折扣,這些方案根據貨物制定,舉個例子:客戶單獨買電視或音響不會有任何折扣,一起買則會得到5%的折扣,電視+音響+DVD組合更會得到7%的折扣。

在一般情況下,這些折扣方案所產生的折扣是累加的(1+2+3),但是不排除其他可能性,例如2中可能出現的排它性條件折扣,也就是說這些折扣方案有可能會出現相對複雜的組合關係。[/quote]
而且,更要命的是,不知道明天age0同學會不會又給老闆出什麼損招。(這老闆莫不是age0的小舅子? :lol: )

於是,早先的OO設計面對這種亂拳打死老師傅的外行老闆,有些秀才遇到兵的感覺了。

在那個帖子的後面,一些高手還提出了一些OO的改進方案。不過,比我們最初看到的那個策略模式就複雜的多了。

本文裏,我們還是看看CO怎麼處理這種問題。


首先,大致分析一下我們的處境。所謂“動手的不如動嘴的”,我們上面有一個對技術狗屁不通卻喜歡指手畫腳的老闆,還有一個讓大家恨得牙癢癢的age0這個狗頭軍師。這讓我們對我們真正需要處理的需求茫然不知所措。老闆今天說對女性優惠,明天就可能說對單身18歲以下,胸圍32C以上的纔給優惠(我們有着從來不憚以最壞的惡意推測自己老闆的美德),肯給老闆“大功告成”的乾脆白送,外帶小費。(NND,開始YY自己是那個老闆了:-))

從技術上說,這些“如何折扣”完全是非常多變的需求。把這些需求寫成OO,CO,AO, PO並不是關鍵。我們最主要的目的是把他們寫在一個統一的容易更改的模塊中。不管明天老闆出什麼新的妖蛾子,我們都在一個模塊中更改或者增添新的需求。

這個模塊可以用Java, groovy, ruby, xml,這些都是可以考慮的方案。
Quake Wang就用bean shell的腳本寫了一個解決方法。

public class BeanShellDiscountStrategy implements DiscountStrategy {
public static final String DISCOUNT_BY_POINT = "if(member.point > 5); return 0.05; else return 0.02;";
public static final String DISCOUNT_BY_GENDER = "if(member.gender == 0); return 0.01; else return 0.03;";

private Interpreter i = new Interpreter();;
private String discountScript;

public BeanShellDiscountStrategy(Member member, String discountScript); {
this.discountScript = discountScript;
try {
i.set("member", member);;
} catch (EvalError e); {
throw new RuntimeException(e);;
}
}

public double getDiscount(); {
try {
return ((Double); i.eval(discountScript););.doubleValue();;
} catch (EvalError e); {
throw new RuntimeException(e);;
}
}

//getters and setters
public String getDiscountScript(); {
return discountScript;
}

public void setDiscountScript(String discountScript); {
this.discountScript = discountScript;
}
}

這個方法,把多變的業務邏輯轉移到bean shell腳本中,一定程度地減少了工作量。我們可以用ioc,把不同的腳本注射進去來得到不同的行爲。

然而,這個方法對複雜的業務邏輯並沒有提供什麼本質性的解決方案。它沒有提供把簡單的規則組合成複雜規則的方法。比如前面age0給我們提出的“排他性”。三個規則,如果第一個成功了,就用第一個,否則順序執行第二個,第三個,直到某一個規則成功。這種邏輯,實際上已經不是在操作具體折扣,而是在操作規則本身。

我們希望能夠寫:
exclusive(rule1, rule2, rule3),而不用關心rule1, rule2, rule3到底都是些什麼rule。

類似的規則還有一些,比如:如果rule1返回true,才計算rule2;如果rule1返回false,才計算rule2;如果rule1的返回值等於某個預定值,才計算rule2;只有rule1和rule2都返回true,才計算rule3;把rule1和rule2的結果加起來;把一系列的rule的返回值起來;取rule1, rule2, rule3中返回值最大的。等等等等。這些,簡單地用bean shell腳本是沒用的。


頭大了吧?嘿嘿。

其實,這是好消息呀。

我們前面分析了,老闆是兇殘的,鬥爭是殘酷的,需求是不可預測的。但是,上面的這些組合,卻是往往不會變的。因爲老闆畢竟還是人嘛,他的邏輯畢竟還是跑不出我們普通的“如果,那麼”,與,或,非,加減乘除等等。

變化的,往往只是老闆怎麼組織這些“如果,那麼”罷了。

變化的東西我們不好掌握,但是這些不變的東西還是可以啃一啃的。如果我們把這些搞定了。那麼不管age0給老闆出什麼餿主意,我們都可以更輕鬆的應對。

比如:
Rule single = married.not();;
Rule young = age < 18;
Rule good = breast > 32C;
Rule boss_likes = and(single, young, good);;
Rule boss_pays = and(boss_likes, 大功告成able);;
Rule paid_by_boss = boss_pays.then(120);;
Rule big_discount = boss_likes.then(50);;
Rule boss_afair = exclusive(paid_by_boss, big_discount);;


哈,老闆啊,您的代號爲“選美”的折扣計劃搞定了。


嗯。yy結束。理想是真美好啊。那麼,怎麼變成現實呢?熟讀經典yy文學(武俠小說,比如說)的我們,一定知道被狗屎運纏身所煩惱的主角的成功方法,那就是:夏夢————對不起,是“瞎蒙”,拼音輸入給搞錯了。(傳說yy大師金老先生從前的yy情人就叫夏夢還是“瞎蒙”來着?小生八卦功力淺薄,也不知是否和老先生的作品yy風格有不可說的聯繫?)

就是說,管你東邪吸毒多狠,管你大輪明王多拽,老子我一不用研究你的武功招式,二不用找領導給你穿小鞋,閉着眼睛,亂走一氣凌波微步你也拿我沒轍。咱就是運氣好,你咬我?


咱們下面就來當一把段呆子。把你的ipod nano耳機戴上,音樂音量放到最大,不要理老闆在那唧唧歪歪,什麼折扣?什麼客戶?俗!
咱們還是研究一些伏羲六十四卦,洛神的美妙步法(或者美妙身材也行啊。哈哈)。

所謂太極生兩儀,兩儀生四象(不是“思想”,更不是“死相”),一個rule的基本是什麼?其實很簡單,不過是兩條:
1。它是否能用?一個對大mm美女的rule不能用在如花身上。
2。它生成的結果。

這個rule還要知道很多facts。於是,一個Rule的接口可以這樣定義:

interface Rule{
boolean apply(RuleContext facts, Variant result);;
}

返回的boolean值表現這個rule是否可用。
那個Variant類型的result是一個placeholder,用來接收rule生成的結果。
RuleContext給rule提供所有需要的信息。

完了,Rule的定義就這樣了。失望吧?

下面來看看前面我們說到的那些組合:
ExclusiveRule用來實現排他性組合:

class ExclusiveRule implements Rule{
private final Rule[] rules;
boolean apply(RuleContext facts, Variant result);{
for(Rule rule: rules);{
if(rule.apply(facts, result););
return true;
}
return false;
}
}

IfElseRule用來實現簡單的if-else邏輯:
class IfElseRule implements Rule{
private Rule cond;
private Rule consequence;
private Rule alternative;
boolean apply(RuleContext facts, Variant result);{
if(!cond.apply(facts, result););{
return false;
}
if(result.getBoolean(););{
return consequence.apply(facts, result);;
}
else{
return alternative.apply(facts, result);;
}
}
}


NotRule用來把一個rule的bool返回值取反:
class NotRule implements Rule{
private Rule rule
boolean apply(RuleContext facts, Variant result);{
if(!rule.apply(facts, result);); return false;
result.setBoolean(!result.getBoolean(););;
}
}

AndRule用來把兩個rule的bool值進行邏輯與:
class AndRule implements Rule{
private Rule rule1;
private Rule rule2;
boolean apply(RuleContext facts, Variant result);{
if(rule1.apply(facts, result););{
if(!result.getBoolean(););
return true;
}
if(rule2.apply(facts, result);{
if(!result.getBoolean(););{
return true;
}
}
result.setBoolean(true);;
return true;
}
}

類似地,OrRule用來進行邏輯或。

其他的加減乘除,取最大值,都可以用類似的方法實現。我就不贅述了。

另外一點需要注意的,是我們實現了ifelse,但是前面所述的組合邏輯中我們只是說如果mm漂漂就如何,沒有說不漂漂如何。不能簡單地認爲不漂漂就不給折扣,在排它性組合中,一個規則是否被應用了決定後續其它規則是否有機會被執行。

不過,我們仍然可以用ifelse來處理單純if的情況。只需要定義一個NilRule就好了。這個Rule乾脆不返回任何值,它永遠都是一個不會被應用的規則(也就是說,apply()函數必然返回false)

class NilRule implements Rule{
boolean apply(RuleContext facts, Variant result);{
return false;
}
}

這樣,new IfElseRule(ppmm_rule, big_discount_rule, new NilRule());就是一個僅僅對ppmm有效的rule。

上面的這些Rule的基本組合實現,還是有一些重構的空間的。不過目前的實現更加簡單,也足以表達CO的思想,所以,進一步的重構我就不做了。

在前面YY時,我們用了rule1.then(...), rule.not()這種東西。而現在的Rule接口只有一個apply(),要做rule.not()必須寫"new NotRule(rule)",語法上相對繁瑣一些。

爲此,我們可以把Rule從接口變成抽象類,把一些常見的組合子放進去,於是我們就可以有rule.not(), rule.ifelse(a,b), rule.then(a), rule.unless(b)等等更加方便乾淨的語法了。

呵呵,到此爲止,我們的迷你規則引擎已經建設得七七八八了。之所以這麼順利,都要歸功於你的ipod的音樂,它讓我們可以把老闆的喋喋不休拋在一邊,裝作就象盤古開天闢地以來就從來沒有那些混賬需求一樣。世界清靜了,我們才得以專心欣賞洛神曼妙的步履和身材。


學會了伏羲八卦,實現了這些可以反覆重用的組合規則之後,就剩下實現具體的原子規則了。比如,取得客戶性別,胸圍等。

這些信息我們都假設可以從RuleContext得到。

那麼,可以寫一個SimpleRule來簡化這些原子規則的創建:

abstract class SimpleRule extends Rule{
boolean apply(RuleContext facts, Variant result);{
result.set(run(facts););;
return true;
}
abstract Object run(RuleContext facts);;
}


對取得性別這個原子規則,我們就可以這樣寫:
class GenderRule extends SimpleRule{
Object run(RuleContext facts);{
return facts.getGender();;
}
}

這些原子規則可能不多,也可能不少。不過,總之是比把所有邏輯都實現在一起要簡單多了。而且,如果使用一些腳本語言來包裝這些規則的話,這些原子規則往往可以很簡單地從closure中直接構造出來,不用每次單獨寫一個java類。

實際上,當你發現你需要在一個Rule實現裏面放很多代碼的時候,往往可以停下來想一想了,很有可能這個Rule本身可以有若干個小規則組合而成,不用費勁寫了。

最後,讓我們精彩回放段呆子凌波微步戲鳩摩智的片斷(一個應用我們這個mini rule engine的測試代碼):


package jfun.cre.demo.test;


import java.util.Calendar;
import java.util.Date;

import jfun.cre.Rule;

import jfun.cre.Variant;
import jfun.cre.demo.MyRuleContext;
import jfun.cre.demo.MyRules;
import junit.framework.TestCase;

public class SimpleTestCase extends TestCase{
private Rule getRule();{
final Rule gold_member = MyRules.discountByMember("gold", 0.1);;
final Rule silver_member = MyRules.discountByMember("silver", 0.05);;
final Rule platinum_member = MyRules.discountByMember("platinum", 0.2);;
final Rule by_member = MyRules.any(new Rule[]{platinum_member,
gold_member, silver_member});;



final Rule is_female = MyRules.isGender("female");;
final Rule is_female_day = MyRules.isMonth(Calendar.MARCH);
.and(MyRules.isDay(8););;
final Rule female_discount = is_female.and(is_female_day);
.then(MyRules.discount(0.05););;



final Rule tvspeaker = MyRules.purchased(new String[]{"tv","speaker"});
.then(MyRules.discount(0.05););;
final Rule tvspeakerdvd = MyRules.purchased(new String[]{"tv","speaker","dvd"});
.then(MyRules.discount(0.07););;
final Rule by_purchase = MyRules.any(new Rule[]{tvspeakerdvd, tvspeaker});;

final Rule final_discount = MyRules.productDouble(new Rule[]{
by_member, female_discount, by_purchase
});;
return final_discount;
}
public void test1();{
final MyRuleContext mrc = new MyRuleContext();;
final Variant result = new Variant();;
assertTrue(getRule();.apply(mrc, result););;
assertEquals(0.837, result.getDouble(););;
}
public void test2();{
final MyRuleContext mrc = new MyRuleContext();{
public Date getNow();{
Calendar cal = getCalendar();;
cal.setTime(super.getNow(););;
cal.set(Calendar.MONTH, Calendar.MARCH);;
cal.set(Calendar.DAY_OF_MONTH, 8);;
return cal.getTime();;
}
};
final Variant result = new Variant();;
assertTrue(getRule();.apply(mrc, result););;
assertEquals(0.837*0.95d, result.getDouble(););;
}
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章