论坛首页 Java企业应用论坛

OO design trap

浏览 55135 次
锁定老帖子 主题:OO design trap
该帖已经被评为精华帖
作者 正文
   发表时间:2005-12-28  
age0 写道
明确下一阶段需求:
折扣方案主要分为三大类:
1. 会员等级折扣,会员等级是最重要的分类,大部分方案都会在等级上面作文章,所以单独归为一类
2. 条件型折扣,只有在符合特定条件下才会发生的折扣,比如前面针对女性会员节日的折扣
3. 货物优惠折扣,这些方案根据货物制定,举个例子:客户单独买电视或音响不会有任何折扣,一起买则会得到5%的折扣,电视+音响+DVD组合更会得到7%的折扣。

在一般情况下,这些折扣方案所产生的折扣是累加的(1+2+3),但是不排除其他可能性,例如2中可能出现的排它性条件折扣,也就是说这些折扣方案有可能会出现相对复杂的组合关系。



有点挑战,有没可能搞成开源的Modeling?
0 请登录后投票
   发表时间: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在后期重构中可以轻松做出来。
0 请登录后投票
   发表时间: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);;
	} 
}
0 请登录后投票
   发表时间: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);
0 请登录后投票
   发表时间: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系列。
0 请登录后投票
   发表时间: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在某些方面做了非常好的补充。
0 请登录后投票
   发表时间: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,易于扩展。
0 请登录后投票
   发表时间:2006-01-04  
偶考虑了一下偶现在的系统,往少里估计,  总共50种不同的交易种类, 500家不同的银行, 50种不同的商户, 等等还有很多其他因素, 估计总共计费算法有50*500*50种,分润算法也超多, 虽然有些确实是重复的, 但是数量的却是惊人. 如果分解成rule的话, 数量的确实太多了
0 请登录后投票
   发表时间:2006-01-04  
rule可以重用的啊。不见得需要分解那么多rule的。除非一点共同规律也没有。

你可以试着用最方便的伪码写一下需求,然后就可以试着写成rule。

当然,其实对大系统,也许还是专业的规则引擎更合适,毕竟人家有Rete算法在那撑着,效率上没的说。
0 请登录后投票
   发表时间:2006-01-05  
推荐一个商业的C++语言可以用的规则引擎?
0 请登录后投票
论坛首页 Java企业应用版

跳转论坛:
Global site tag (gtag.js) - Google Analytics