锁定老帖子 主题:OO design trap
该帖已经被评为精华帖
|
|
---|---|
作者 | 正文 |
发表时间:2005-12-28
age0 写道 明确下一阶段需求:
折扣方案主要分为三大类: 1. 会员等级折扣,会员等级是最重要的分类,大部分方案都会在等级上面作文章,所以单独归为一类 2. 条件型折扣,只有在符合特定条件下才会发生的折扣,比如前面针对女性会员节日的折扣 3. 货物优惠折扣,这些方案根据货物制定,举个例子:客户单独买电视或音响不会有任何折扣,一起买则会得到5%的折扣,电视+音响+DVD组合更会得到7%的折扣。 在一般情况下,这些折扣方案所产生的折扣是累加的(1+2+3),但是不排除其他可能性,例如2中可能出现的排它性条件折扣,也就是说这些折扣方案有可能会出现相对复杂的组合关系。 有点挑战,有没可能搞成开源的Modeling? |
|
返回顶楼 | |
发表时间:2005-12-28
看了age0比较贴近实际的需求,如果要考虑复杂的组合,排它性,条件折扣等等,那么很有必要建立一个特定的领域模型。我写了一个较为简单的实现,里面主要涉及到几个类:
DiscountContext,一次折扣计算的上下文环境,它的信息主要有当前提供的折扣类型(DiscountType),当前运算得信息来源(InfoProvider) ,以及一些kick off Calculate 和 finished calculate的方法。 DiscountContext相当于一个有状态的计算器,它作为参数传入DiscountType.calculateDiscount方法,使得DiscountType能够获得整个上下文环境,可以查询当前的状态,获得各种计算所需的信息等等,另外更重要的是,DiscountType可以在计算时直接任意修改当前上下文的信息,排他性的折扣类型甚至可以直接finish当前运算环境。 DiscountContext主要状态有: 1) currentDiscountPoint ,纪录当前折扣点 2) matchedDiscounts,当前已经匹配并计算过的折扣类型,排它性的折扣类型可以清除它 对于折扣运算过程来说,整体来说相当于一个输入,输出过程。 输入过程大致如下: context.addInfoProvider( infoIndicator, infoProvider);; ..... 将各种信息输入到disCountContext中。 运算: calculateDiscount();; 输出: context.getCurrentDiscountPoint();; context.getMatchedDiscountTypes();; 这个领域模型设计的核心在DiscountContext上,而以前一直热烈讨论的消除if else在这个设计中却很“恶劣“的到处存在。 其实对于这么一个复杂的需求,问题的关键已经不在于怎么消除if/else,而是怎么有效的处理各种策略的组合,使得代码的纯增量修改成为可能。要使得达到这个目的,一个有效的模型(引擎)的设计还是需要得,(CO的组合其实也是一个特定于这个领域的设计) ,而西枝末节的if/else在后期重构中可以轻松做出来。 |
|
返回顶楼 | |
发表时间:2005-12-29
各位先不用急着给方案,boss并不满足于增加各种折扣,可以方便灵活的调整折扣策略可能显得更重要,比如说A折扣今天适用于金卡等级,明天则适用于银卡等级,后天则全等级适用;折扣方案也可以设置有效时间段,方案只会在指定时间内生效;最好可以支持这种配置方式:
适用等级:钻石卡 适用时间:2005-5-1,2005-5-31 方案:(A+B+C) || (D) || (G) 适用等级:金卡、银卡 适用时间:2005-5-1,2005-5-31 方案:(A+B+C) || (C+E) 适用等级:所有 适用时间:2005-1-1,2005-6-30 方案:(B+F) || (D) 公式里面的 || ,表示取折扣最多的策略,也就是说原有的排它性概念转化为取最大值。 只要条件符合的方案都会被执行,最后取折扣最多的方案。 折扣类型现在可以归类为两种,一种是对特定货物生效,另一种则是对所有货物生效,例如原有的女性节日折扣,相当于对所有货物生效。 对于某一货物,如果出现符合多个折扣策略的情况,同样取最大值。 这样看起来,已经有点dsl的趋势了,为了简化问题,我们目前不考虑脚本解释之类的问题,暂时用对象解决就可以,当然,简单的公式分析是免不了的。 例如: class discount_case { // 等级 string type; // 有效期 Date begin_date; Date end_date; // 计算公式 string formula; } class discount_cases { static ArrayList m_cases = null; static public ArrayList GetCases(); { if(m_cases == null); { m_cases = new ArrayList();; Initialize();; } return m_cases; } static private void Initialize(); { // 方案1 discount_case dc = new discount_case();; dc.type = "钻石卡"; dc.begin_date = "2005-5-1"; dc.end_date = "2005-5-31"; dc.formula = "(A+B+C); || (D); || (G);"; m_cases.Add(dc);; // 方案2 dc = new discount_case();; dc.type = "金卡,银卡"; dc.begin_date = "2005-5-1"; dc.end_date = "2005-5-31"; dc.formula = "(A+B+C); || (C+E);"; m_cases.Add(dc);; // 方案3 dc = new discount_case();; dc.type = "所有"; dc.begin_date = "2005-1-1"; dc.end_date = "2005-6-30"; dc.formula = "(B+F); || (D);"; m_cases.Add(dc);; } } |
|
返回顶楼 | |
发表时间:2005-12-29
抽象出两个概念:触发器、支付策略
两种组合:触发器是“与”关系的组合,支付策略是递归的组合 所有的参数与组合关系写在配置文件,解析配置文件时,支付策略部分可能麻烦一点,可以使用简单的公式分析,也可以使用树形递归。 interface Trigger { boolean validate();; } // 会员类型触发器 class MemberTypeTrigger implements Trigger { Set validTypes; // 有效的会员类型 Integer memberType; // 实际的会员类型 void addValidType(int type); { validTypes.add(new Integer(type););; } void setMemberType(int memberType); { memberType = new Integer(memberType);; } boolean validate(); { return validTypes.contains(memberType);; } } // 时间触发器 class DateTrigger implements Trigger { // …… } interface Payment { double pay(double original);; } // 特殊货物类型支付策略 class SpecialGoodsPayment implements Payment { // …… } APayment implements Payment { // …… } BPayment implements Payment { // …… } CPayment implements Payment { // …… } DPayment implements Payment { // …… } PlusPayment implements Payment { List payments; double pay(double original); { // 计算累加折扣 } } MaxPayment implements Payment { List payments; double pay(double original); { // 取最大折扣 } } // client... List triggers = ...; List payment = 各种组合……; // 若任一触发器不满足条件,结果为原价 // 否则结果为payment.pay(double original); |
|
返回顶楼 | |
发表时间:2005-12-30
age0 写道 明确下一阶段需求:
折扣方案主要分为三大类: 1. 会员等级折扣,会员等级是最重要的分类,大部分方案都会在等级上面作文章,所以单独归为一类 2. 条件型折扣,只有在符合特定条件下才会发生的折扣,比如前面针对女性会员节日的折扣 3. 货物优惠折扣,这些方案根据货物制定,举个例子:客户单独买电视或音响不会有任何折扣,一起买则会得到5%的折扣,电视+音响+DVD组合更会得到7%的折扣。 在一般情况下,这些折扣方案所产生的折扣是累加的(1+2+3),但是不排除其他可能性,例如2中可能出现的排它性条件折扣,也就是说这些折扣方案有可能会出现相对复杂的组合关系。 rule engine自然是一个方案了。 另外就是script了。把各个规则用比较自然的脚本表现出来。比如quake wang的bean shell。最直接,也最容易理解。对简单的情况非常有效。一个问题在于,规则的重用。比如,所说的累加,或者排他。这不是简单你if-else,而是规则间的组合。这时候,简单的script就不太够用了。 接下来,为了对付规则组合,first class rule,或者高阶逻辑就要登场了。 首先,可以把所有相关的信息抽象进一个无所不知的Context接口。通过这个接口可以知道客户都买了什么货物,买了多少,哪天买的,是不是和男朋友/女朋友买的;可以知道客户家里几口,老妈贵姓;知道今天几号,卡特里纳飓风现在的强度等等等等。总而言之,所有作决策的信息必须在Context接口里面。 然后,定义一个高阶的rule接口: interface Rule{ RuleResult apply(Context ctxt);; } class RuleResult{ final boolean applicable; final Object result; } 接下来,定义各种组合规则和基本组合子。比如:bind,if-else,map,max等等等等。 这就构成了一个CO的框架。 限于java的语法限制,用co写出一个足够复杂的规则会牵扯到一大堆匿名类,相当难看,往往给这种co外面包一个脚本或者xml的外壳会比较好读。所以我们这里用haskell的语法来写: rule-by-level = caseLevel [ "gold", 3.0, "silver" 1.0, 0 ]; rule-by-condition = rule(eq 'female' getGender, return 0.5);; rule-by-purchase = or( rule(purchased["dvd", "tv", "speaker"], 0.7);, rule(purchased["tv", "speaker"], 0.5] );; final-rule = fold (+); 0 [ rule-by-level, rule-by-condition, rule-by-purchase ]; 简单介绍一下几个组合子: rule: 把一个返回boolean的rule和另外一个rule组合成一个新的rule。 caselevel: 根据customer level来决定规则返回的值。 eq: 判断一个rule的返回值是否等于某个值。 return: 生成一个rule,这个rule直接返回某个值。 or: 顺序尝试每一个rule,直到某个rule被apply成功。 fold: 执行每个rule,把结果加起来。 这些规则和组合子都可以用co的方法实现出来。 嗯。考虑找时间拿这个例子继续我的CO系列。 |
|
返回顶楼 | |
发表时间:2005-12-31
引用 接下来,为了对付规则组合,first class rule,或者高阶逻辑就要登场了。
首先,可以把所有相关的信息抽象进一个无所不知的Context接口。通过这个接口可以知道客户都买了什么货物,买了多少,哪天买的,是不是和男朋友/女朋友买的;可以知道客户家里几口,老妈贵姓;知道今天几号,卡特里纳飓风现在的强度等等等等。总而言之,所有作决策的信息必须在Context接口里面。 然后,定义一个高阶的rule接口: java代码: interface Rule{ RuleResult apply(Context ctxt); } class RuleResult{ final boolean applicable; final Object result; } 接下来,定义各种组合规则和基本组合子。比如:bind,if-else,map,max等等等等。 这就构成了一个CO的框架。 与实际领域模型已经比较贴近了,跟我设计的思维也基本一致,不过你主要基于 CO来解决这类问题,从这个例子可以看出CO对于OO在某些方面做了非常好的补充。 |
|
返回顶楼 | |
发表时间:2006-01-02
花了大半天时间写了一个,暂时没有script的外壳,只是java api。
测试代码如下: 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(););; } } 每个rule都返回boolean的值来表示这个rule是否applicable。另外有一个Variant的对象用来储存rule计算的结果 组合子简介: 一般性组合子: any: 顺序使用一系列rule,直到某个rule是applicable的。这个实现了排它组合。 or: 逻辑或,每个参与计算的rule都需要产生一个boolean结果。short-circuited,遇到true后面的就不再计算。 and: 逻辑与。遇到false后面的就不再计算。 productDouble: 计算所有applicable的rule的结果乘积。遇到0后面的就不再计算。这个,还有sumDouble等等,实现了非排它组合。 then: 逻辑判断。如果rule1结果为真,使用rule2。 业务相关组合子: discountByMember, isGender, isMonth, isDay: 各种和业务逻辑相关的组合子。 还有其它一些组合子这个例子没有用到。 没有基于Rete的专业rule engine那么快,那么智能。但是,对题目给的需求还是很好对付的。而且代码短小,简单,纯java的api,易于扩展。 |
|
返回顶楼 | |
发表时间:2006-01-04
偶考虑了一下偶现在的系统,往少里估计, 总共50种不同的交易种类, 500家不同的银行, 50种不同的商户, 等等还有很多其他因素, 估计总共计费算法有50*500*50种,分润算法也超多, 虽然有些确实是重复的, 但是数量的却是惊人. 如果分解成rule的话, 数量的确实太多了
|
|
返回顶楼 | |
发表时间:2006-01-04
rule可以重用的啊。不见得需要分解那么多rule的。除非一点共同规律也没有。
你可以试着用最方便的伪码写一下需求,然后就可以试着写成rule。 当然,其实对大系统,也许还是专业的规则引擎更合适,毕竟人家有Rete算法在那撑着,效率上没的说。 |
|
返回顶楼 | |
发表时间:2006-01-05
推荐一个商业的C++语言可以用的规则引擎?
|
|
返回顶楼 | |