您的位置:首页 > 职场人生

累病倒了我两次的面试题--移动用户资费统计系统

2011-04-06 10:57 471 查看
后期补充:网友对我诟病最多的就是我帮学生做面试题,说这是小偷和骗子行为,在此,我对自己给学员做面试题做出如下解释:

(1)学员拿着面试题来找老师,学生也事先思考和尝试后实在没有办法,又求职心切才想到找老师帮忙的。老师出于对题目和技术的好奇,也出于对学生的感情,才不辞劳苦去帮助做题的。明知小孩自己做不到,还不帮小孩去做,非要锻炼孩子自己去做,现在估计很多父亲都做不到吧。何况,学生也是我们的客户,我们不去帮一下,这个显得太冷酷无情了。

(2)有人说,应该让学生自己去做,做不出来就别去招聘单位冒充好汉,帮学生做题就是鼓励学生行骗和作恶!从这一点上来说,事情做得确实有点不光明磊落,但用行骗和作恶来形容,就言之过重了。毕竟用人单位也不是傻子,随便找个农民工把结果交上去,用人单位就会录用吗?用人单位在做题之前就对学员进行过一些基本的技术考核和交流,肯定也是觉得差不多了,才让学生把题目拿回家去做的,学生做出来后,也要给他们去讲解代码思路的,只要学生能说清楚代码思路,用人单位未必真关心是学员自己单独做的,还是有朋友帮忙做的,因为很多单位的招聘岗位是对事不对人的,只要能把工作中安排的事情搞定,那就不管这个人是否是自己亲自搞定的,还是靠外援搞定的,公司要的是事情的结果。很多公子哥在一些大公司都挂职“副总经理”,难道这个公子哥真有“副总经理”的能力吗?不管他们有没有,但是,他们能靠自己的关系把“副总经理”要办的事情办妥,这就是公司的目的。同样道理,公司不管学生是怎么做出来,只要学生做出来了,就说明他或他的亲友团能解决公司日后分配给他的任务,才不管他是怎么解决的呢?公司也许要的就是这一点。如果是这样,我们正好满足了公司的需求,怎么能说是行骗和作恶呢!

(3)我历来的一个观念就是:对于我花时间研究透和解决过的技术问题,只有把这些知识分享出去,才能实现个人价值和社会价值的更大化。如果一个人花了很多时间和精力搞明白的知识只装在自己的肚子里,不找机会把它应用出来,那么这个知识就没有什么价值,一个知识只有被很多人使用,被反复地使用,才能实现这个知识的价值最大化。所以,我把这些题目和解题思路都公布出来了,为了吸引更多人来学习,我当然要告诉大家这是面试题,并且是决定工作成败的面试题,这也是提高大家来学习这个题目的热情的一种激励手法罢了,没想到被送到了道德审判的十字架上了。

前面的两篇博文写到:有两名同学出去面试,招聘单位分别给他们两个小项目拿回家做,三天内做出来即可给到7k以上的月薪,我都帮他们做了,详情参看《交通灯管理系统》和《银行业务调度系统》的介绍。

不久,又有一名学员给我打电话,让我帮他做一道类似的题目,不过,这次项目变成了“移动用户资费统计系统”,再细问,知道这次的招聘公司不是软通动力了,而是联想利泰,这两家公司提供的word文档格式都完全一样,我奇怪现在的软件公司怎么都学会用这招来考核求职者了,我真怀疑这些题目出自同一人之手,难道是软通动力的技术经理跳槽到联想利泰啦?他的这招面试手法确实够高!

我看到“移动用户资费统计系统”的需求文档后,发现这个系统的业务比《交通灯管理系统》和《银行业务调度系统》的业务都要复杂得多,一时间真不知道该从哪里着手,
只能回复那名学员:“这个项目题好像不是一两天就能搞定的事情,我努力做做,别抱太大希望”。如果说“交通灯管理系统”或“银行业务调度系统”做出来就可以拿到7k月薪的工资的话,我认为这个“移动用户资费统计系统”做出来的话,起码应该可以拿到1w以上的月薪。我按照“移动用户资费统计系统”的需求文档认认真真地做了两天,体力透支很大,加之十几年的编程生涯,我一直处于亚健康状态,这个“移动用户资费统计系统”终究成了压倒骆驼的最后一根稻草,我累得病倒了!等到身体康复后,我不甘心事情只做了一半就放弃了,接着又花了两天的时间,总算把这个项目题给做完了,结果又再次累病倒了。

后来又有好几名去联想利泰面试的学员拿着这个《移动用户资费统计系统》找我,我每次都把源代码给了这些学员,并讲解清楚了其中的代码结构,后来问这些学员,他们一个也没被录取,他们一致认为该公司可能只是想找个人来向他们讲解如何实现此系统的代码,看来着实枉费了我一番心机和功夫!最近,又接到多名学员的反馈,除了联想利泰,美国阿格雅技术有限公司也在使用此题目考核面试者,这个题目虽然没有华丽的外表,但它确实是一个有技术含量的项目,绝非是一个普普通通的程序员容易做出来的东西!这道题目是在解决实实在在的复杂业务问题,其中涵盖了技术含量极高的编程技巧和设计手法,这才是真正做高级项目所要学习的知识,对于广大程序员来说,它是一个不错的案例,不同于那种只是大量重复性工作的增删改查项目。

特别说明: 由于电信和银行实际环境的复杂性和产品一旦上线运行后对错误的容忍度几乎为0,电信和银行的项目在做完后都不能直接上线测试运行,而是要通过编写非常完全和严格的模拟程序来进行测试,确保万无一失后再实际上线运行,所以,为电信和银行项目编写模拟程序和比对检查程序运行的结果就非常重要了。虽然我写完了《移动用户资费统计系统》的代码,但不能保证最终的运行结果就没有错误,这需要仔细分析和比对运行结果才能发现问题,由于《移动用户资费统计系统》的业务逻辑实在有点复杂,仅凭我个人之力,实在难以比对出每个细节,为此,我专门编写了《测试用例编写指导书》和《比对检验指导书》,供感兴趣的网友参考。如果有哪位网友发现我写的程序统计出来的结果有问题,请来信详细告知,我将不胜感激。

下面是”移动用户资费统计系统“的需求:

移动用户资费统计系统

原始需求文档下载
讲座ppt下载
讲座源码下载 讲座视频下载(待补充)

-->
模拟实现简易的移动用户资费统计系统逻辑,具体需求如下:

移动运营商A设置两种类型的用户:普通用户及VIP用户,现该运营商已有5个VIP用户和15个普通用户,共计20个用户。
普通用户资费标准如下(不考虑漫游和长途):

【基准资费】

无月租费用。

通话费:0.6元/ 分钟(仅拨打收费,接听免费)

短信费:0.1元/ 条

数据费:5元/ M

【优惠套餐】

话费套餐:月功能费20元,最多可拨打60分钟电话,超出时间按照0.5元/分钟计费。

短信套餐:月功能费10元,最多可发送200条短信,超出条数按照0.1元/条计费。

数据套餐:月功能费20元,最多可获50M的流量,超出流量按照3元/M 计费。

注:用户可以选择多种套餐,各功能(通话、短信、数据)计费时,如已选择对应套餐,则按套餐标准计费;如未选择对应套餐,则按对应的基准资费计费。

VIP用户资费标准如下(不考虑漫游和长途):

【基准资费】

月租费用:按天收取,2元/ 天

通话费:0.4元/ 分钟(仅拨打收费,接听免费)

短信费:0.1元/ 条

数据费:3元/ M

【优惠套餐】

套餐1 :月基本费用100元(无月租费用),提供如下服务:

①最多可拨打750分钟电话,超出部分按照0.3元/ 分钟计费。

②最多可发送200条短信,超出条数按照0.1元/ 条计费。

③最多可获得100M数据流量,超出流量按照1元/ M计费。

套餐2 :月基本费用200元(无月租费用),提供如下服务:

①最多可拨打2000分钟电话,超出部分按照0.2元/ 分钟计费。

②最多可发送500条短信,超出条数按照0.1元/ 条计费。

③最多可获得300M数据流量,超出流量按照0.5元/ M计费。

注:用户最多只能选择一种套餐,如未选择任何套餐,则按照基准资费计费。

各类型用户只能选择提供给本类型用户的套餐。
新用户入网。

①对于新入网的普通用户,入网当月赠送如下服务:免费拨打60分钟

电话,免费发送200条短信,免费获得50M流量。超出赠送的部分

按照普通用户基准资费进行计费。

②对于新入网的VIP用户,入网当月赠送如下服务:免费拨打200分

钟电话,免费发送200条短信,免费获得100M数据流量。超出赠送

的部分按照VIP用户基准资费进行计费(注意:需按入网天数计算月

租费用)。

每月为用户计算一次账单,用户订制的套餐信息和账单信息采用文件方式进行存储(提示:可使用java中的Properties API进行文件操作)。
用户可自由订制或退订所属用户类型的套餐,并从下月起生效。
异步随机生成客户操作如下:

①拨打电话,每次拨打时长为1至10分钟不等(随机决定,以分钟为单位)。

②发送短信,每次发送条数为1至10条不等(随机决定)。

③上网获取数据,每次获取数据流量可为50K,100K,200K,500K,1M(随机决定)。

④订制或退订相应套餐。

⑤新用户入网(随机决定用户类型)。

注:随机生成客户操作时间间隔自定,可设置。

不要求实现GUI,只考虑系统逻辑实现,可通过Log方式展现程序运行结果。

解题思路

数据分析与统一计算公式:

分析本系统的业务,可以看到普通用户和VIP用户在订购套餐的方式以及月底计算账单的公式上都有很大的不同:

(1)普通用户没有月租费和月基本费、而VIP用户有月租费或月基本费。

(2)普通用户是单独订购电话、短信和数据套餐,每项套餐单独收取月功能费;VIP用户不能单独订购电话、短信和数据套餐,VIP用户订购的套餐中同时包含了电话、短信和数据等服务功能。

我们可以为普通用户和VIP用户分别设计出一个月底计算账单的公式,但是,为了简化编程,我们也可以为两种不同用户设计出一个统一的月底计算账单的公式,这就好比“大象有尾巴,而蚂蚁没有尾巴,大象没有触角,而蚂蚁则有触角,能否用同一个累加所有器官的公式来计算蚂蚁和大象的体重呢?当然可以,这时候只需要假设蚂蚁也有尾巴,只是蚂蚁的尾巴重量为0,假设大象也有触角,只是大象触角的重量为0,这样,就可以用同一种累加所有器官的公式来计算蚂蚁和大象的体重了。”我们可以采用如下方式来统一各类用户在各种情况下的费用计算公式:

(1) 月基本费或月租费:月基本费方式为固定值,月租费方式为当月总天数*每天费用或者(当月总天数-入网日+1)*每天费用,只有vip用户才存在此项费用,但是为了统一计算公式,可以认为普通用户也有此项费用,值为0。

(2) 电话收费时长:等于(电话时长-免费时长),计算后的值小于0则记为0,免费时长又分为两类:新入网的免费和套餐中的免费,新入网的免费在用户对象中处理,套餐中的免费封装在套餐策略对象中处理。

(3) 电话、短信、数据套餐月功能费:只有普通用户定了套餐才有此项费用,但是为了统一计算公式,可以认为没定此功能套餐的普通用户和vip用户也有此项费用,值为0。

(4) 月电话费用=电话套餐月功能费+单位计费价格*电话收费时长

(5) 按月电话费用的相同规则计算月短信费用和月数据费用

(6) 月总计费用=月基本费或月租费 + 月电话费用 + 月短信费用 + 月数据费用

我们可以用如下一幅“月账单费用的组成成分”图来直观地理解上面的计算公式:



采用一种便于程序代码读取的格式在配置文件中存储各项数据

刚开始猛然看到这么多数据项,肯定会感觉纷繁杂乱,理不出头绪来,但是,不管这些数据项有多么多,归结起来,不就是某个用户要使用自己的某种数据吗?只是不同的用户有不同的数据罢了,每个用户只需要关心和使用自己的数据、而不用关心其他用户的数据就显得简单多了,因此可以写一个类来专门读取用户的数据,在配置文件中存储的各项数据应想办法采用一种便于该类读取的格式。

(1) 要存储的数据项有:功能单价费用、功能套餐免费数量、功能套餐月费用、新入网免费数量、整体月基本费或月租费。

(2) 一些数据还要随以下类型进行区分:用户类型、套餐类型、功能类型。

(3) 在配置文件中通过用点(.)对数据项名称进行分级的方式来区分各个数据项所属的类别和功能,如下所示:

common.normal.phone.price --> 表示普通用户/非套餐/电话/单价

common.pack1.phone.price --> 表示普通用户/套餐/电话/单价

common.pack1.phone.free --> 表示普通用户/套餐/电话/免费数量

common.pack1.phone.rent ?表示普通用户/套餐/电话/套餐月功能费用

vip.normal.phone.price --> 表示VIP用户/非套餐/电话/单价

vip.pack1.phone.price --> 表示VIP用户/套餐1/电话/单价

vip.pack2.phone.price --> 表示VIP用户/套餐2/电话/单价

common.new.phone.free --> 表示普通用户/新开户/电话/免费数量

vip.new.phone.free --> 表示VIP用户/新开户/电话/免费数量

(4) 对于值为0的数据项,不用在配置文件中存储,这样,当程序代码从配置文件中没有读取到该数据项时,即认为该值为0。

(5) 对于vip用户的整体月基本费或月租费,由于计费单位不一样,采用配置文件方式存储将增加程序的复杂度,所以,决定直接在程序代码中硬编码。

为了便于程序编写,在配置文件中要注意如下两点:

(1) 由于程序中要求每次传输的数据量都是10k的整数倍,因此可以将数据通信费的单价单位由M转换成K表示,由于数据通信费的价格5元/M,转换后则是0.5分/K,这样程序中就涉及到小数处理了。由于在程序中处理小数是很繁琐和容易出现误差的事情,所以,最好还是想办法先统一转换成整数形式进行处理,由于数据传输量都是10k的整数倍,因此,想到将数据通信费的价格5元/M转换成5分/10K。因此,在配置文件中将所有的价格和费用的计量单位由元转换成分表示。

(2) 后来在配置文件中填写各项数据时,发现VIP用户订购套餐2时的数据费仅为0.5元/M,这时候转换的结果是0.5分/10k,又还是出现了小数,故想到把计费单位转成5厘/10k,所以,在配置文件中最终还是应将所有的价格和费用的计量单位由元转换成厘进行计费。

按照上面这些思想设计和编写出来的配置文件conf.properties的完整内容如下:

public class ConfigManager {
private static Properties config = new Properties();
static{
InputStream ips = ConfigManager.class.getResourceAsStream("/conf.properties");
try {
config.load(ips);
} catch (IOException e) {
throw new ExceptionInInitializerError(e);
}
}

private static String makePrefix(int customerType,int packType,int businessType){
String customerTitle = customerType==0?"common":"vip";
String packTitle = packType==0?"normal":("pack"+packType);
String businessTitle = businessType==0?"phone":businessType==1?"message":"data";
return customerTitle + "." + packTitle + "." + businessTitle;
}

private static int getNumber(String key){
String value = config.getProperty(key);
try{
return Integer.parseInt(value);
}catch(Exception e){
return 0;
}
}

public static int getPrice(int customerType,int packType,int businessType){
return getNumber(makePrefix(customerType,packType,businessType)+".price");
}

public static int getFree(int customerType,int packType,int businessType){
return getNumber(makePrefix(customerType,packType,businessType)+".free");
}

public static int getRent(int customerType,int packType,int businessType){
return getNumber(makePrefix(customerType,packType,businessType)+".rent");
}

public static int getNewCustomerFree(int customerType,int businessType){
String[] businesses = {"phone","message","data"};
return getNumber((customerType==0?"common":"vip")+".new." + businesses[businessType] + ".free");
}
}


面向对象的分析和设计:

在进行面向对象设计之前,必须具备和把握了一个重要的经验:谁拥有数据,谁就对外提供操作这些数据的方法。大家可能会说,刚开始看到需求时,只知道某一个用户要使用很多各种各样的数据,而想不到要延伸出哪些对象,其实,只要你把所有数据和使用这些数据的方法归纳起来形成对象,自然就可以发掘出这些对象了。

(一)移动公司里面有两类客户,移动公司里的客户可以打电话、发短信、数据通信,还可以订购和退订套餐;移动公司每月要为其中所有客户生成计费清单,还要模拟各种客户的行为。据此,可以分析出如下一些类和方法:

(1) MobileCorporation类:simulationBusiness方法(模拟一个月的业务,内部随机做500件事情和结算每个用户的计费情况,随机做的事情就是挑选一个用户做其中任何一件事情:打电话/发短信/数据通信/定套餐/退订套餐/新用户入网)

(2) Customer、CommonCustomer、VipCustomer等类:普通用户和VIP用户都可以打电话/发短信/数据通信/定套餐/退订套餐/结算费用等方法。普通用户和VIP用户的区别在于定套餐、退订套餐、结算费用的策略对象不同。

(二)凭借积累的面向对象设计的经验,可以把计算电话、短信、数据费用的功能各封装成一个策略对象,这些策略对象内部根据当前的用户类别、当月适用的套餐和计费的功能项目来计算费用。策略对象在计算费用时,要从Properties文件中读取相应的数据值,为此可以专门设计一个类来读取配置文件,策略对象调用该类的方法。据此,可以分析出如下一些类和方法:

(1) ComputeStrategy类:包含computeMoney方法

(2) ConfigManager类: 包含getPrice、getFree、getRent、getNewCustomerFree等方法。

(三)另外,应该有一个总的策略存储对象来管理当前用户的各个功能项目的策略对象以及VIP用户的月租费或月基本费,所谓订购某项功能套餐,就是选用哪个策略对象,所以,订购某个功能套餐和退订某个功能套餐的方法应分配给这个总的策略存储对象。这个总的策略存储对象内部既要存储各个功能项的当前的套餐对应的策略对象、又要存储下月订购的套餐,还要在下个月时将订购的套餐“设置”为当前的套餐,这个“设置”不一定是真的变量赋值操作,可以是通过日期比较的方式来达到,这需要设计一个辅助类把某月和从该月开始订购的功能套餐进行关联存储。据此,可以分析出如下一些类和方法:

(1) PackStrategy类:包含orderRent、cancelRent、getValidRent、orderPack、cancelPack、getValidPack等方法

(2)OrderedStrategyHolder类:包含order、getValidStrategy等方法

(3)Rent类:包含computeRent方法

(四)类图:



(五)计算账单费用的各个对象的作用与关系



类的编码实现

(一)MobileCorporation类(代表移动公司)

1.在MobileCorporation类内部定义一个List集合的成员变量,用于存储移动公司的所有用户,这里不需要区分普通用户和VIP用户,而是把他们抽象成用户这个父类,这样就可以采用统一的方式来调用他们各自的行为,普通用户和VIP用户的行为差异,在它们各自的方法内部体现,这正是充分利用了面向对象的抽象和多态特性。

2.在构造方法中,向用于存储所有用户的List集合中增加15名普通用户和5名VIP用户,每名用户需要有自己的用户名和入网日期。

3.在MobileCorporation类中定义一个simulationBusiness(Date month)方法来模拟某个月的业务活动,首先清除用户上月的记录信息,然后再模拟发生如下一些事情:某个用户打电话、某个用户发短信、某个用户传输数据、某个用户订购套餐、某个用户退订套餐、新用户入网,最后再统计出各个用户在本月的账单信息。模拟发生的事情随机发生,总共模拟发生500次,并让打电话、发短信、传输数据等事情发生的概率为订购套餐、退订套餐、新用户入网的20倍。

4.将模拟随机发生一件事情的过程封装成一个私有方法randDoOneThing(Date month),这个方法内部调用的某个用户打电话、某个用户发短信、某个用户传输数据、某个用户订购套餐、某个用户退订套餐、新用户入网等功能又各自封装成一个私有方法。

源码如下:

public class IdGenerator {
private IdGenerator(){

}
private static IdGenerator instance = new IdGenerator();
public static IdGenerator getInstance(){
return instance;
}

private int lastCommonId = 15;
private int lastVipId = 5;

public synchronized int nextCommonId(){
return ++lastCommonId;
}

public synchronized int nextVipId(){
return ++lastVipId;
}
}


(三)MainClass类(整个程序的主运行类)

1.调用MobileCorporation类的simulationBusiness(Date date)方法,总共模拟15个连续的月份。

源码如下:

public abstract class Customer {
protected String name;
//用户入网的时间
private Date joinTime;
private int customerType = 0;
protected List<ActionRecord> actionRecords = new ArrayList<ActionRecord>();

//积累的结果只表示当月的所有通话记录,不代表所有历史记录
private ArrayList phoneRecords = new ArrayList();
//积累的结果只表示当月的所有短信记录,不代表所有历史记录
private ArrayList messageRecords = new ArrayList();
//积累的结果只表示当月的所有数据传送记录,不代表所有历史记录
private ArrayList dataRecords = new ArrayList();
protected PackStrategy packStrategy;

public void monthBegin(){
phoneRecords.clear();
messageRecords.clear();
dataRecords.clear();
actionRecords.clear();
}

public Customer(String name,Date joinTime,int customerType){
this.name = name;
this.joinTime = joinTime;
this.customerType = customerType;
}

public String toString(){
return name;
}

public void callPhone(int times){
phoneRecords.add(times);
actionRecords.add(new ActionRecord("打电话",times + "分钟"));
}
public void sendMessage(int numbers){
messageRecords.add(numbers);
actionRecords.add(new ActionRecord("发短信",numbers + "条"));
}
public  void transferData(int size){
dataRecords.add(size);
actionRecords.add(new ActionRecord("传数据",size + "k"));
}

/**
*
* @param currentMonth 正在被计费处理的当月的日期,
* 注意:日字段设置为1,以便于方法内部计算是否是新用户
*/
public int countMonthMoney(Date currentMonth){
boolean newcome = !joinTime.before(currentMonth);//joinTime.after(currentMonth);

int totalPhone = gatherRecords(phoneRecords);
int totalMessage = gatherRecords(messageRecords);
int totalData = gatherRecords(dataRecords);

int freePhone = 0;
int freeMessage = 0;
int freeData = 0;
if(newcome){
freePhone = ConfigManager.getNewCustomerFree(customerType,0);
freeMessage = ConfigManager.getNewCustomerFree(customerType,1);
freeData = ConfigManager.getNewCustomerFree(customerType,2);
}

int chargePhone = totalPhone>freePhone?totalPhone-freePhone:0;
int chargeMessage = totalMessage>freeMessage?totalMessage-freeMessage:0;
int chargeData = totalData>freeData?totalData-freeData:0;

//汇总打印:包括姓名,入网日期,统计月份,通话清单,费用清单,总费用。
System.out.println(name + "," + DateUtil.formatDateToDay(joinTime) + "入网.");
System.out.println(" 操作清单如下-----");
for(int i=0;i<actionRecords.size();i++){
System.out.println("  " + actionRecords.get(i));
}
System.out.println(" 统计清单如下-----");
System.out.println("  通话:" + phoneRecords + "分钟:短信" + messageRecords + "条:数据" + dataRecords + "k");
System.out.println("  通话累计:" + totalPhone + "分钟,减除新开户" + freePhone + "分钟,实际收费" + chargePhone + "分钟");
System.out.println("  短信累计:" + totalMessage + "条,减除新开户" + freeMessage + "条,实际收费" + chargeMessage + "条");
System.out.println("  数据累计:" + totalData + "k,减除新开户" + freeData + "k,实际收费" + chargeData + "k");

ComputeStrategy phoneStrategy = packStrategy.getValidPhonePack(currentMonth);
ComputeStrategy messageStrategy = packStrategy.getValidMessagePack(currentMonth);
ComputeStrategy dataStrategy = packStrategy.getValidDataPack(currentMonth);

int sum = 0;
//VIP用户才有月租金或基本费
Rent rent = packStrategy.getValidRent(currentMonth);
if(rent != null){
int rentMoney = rent.coputeRent(joinTime,currentMonth);
sum +=  rentMoney;
System.out.println("  月租费或基本费:" + rentMoney + "厘钱");
}

sum += phoneStrategy.computeMoney(chargePhone);
sum += messageStrategy.computeMoney(chargeMessage);
sum += dataStrategy.computeMoney(chargeData/10);
System.out.println("  总计:" + sum/1000f + "元钱");
return sum;
}

private int gatherRecords(ArrayList records){
int sum = 0;
for(int i=0;i<records.size();i++){
sum +=  (Integer)(records.get(i));
}
return sum;
}

public abstract void randomCancelPack(Date month);
public abstract void randomOrderPack(Date month);

}


(五)ActionRecord类(代表用户的一项操作)

1.代表用户的一项操作信息,包含操作名称和数量。

源码如下:

public class CommonCustomer extends Customer {

public CommonCustomer(String name,Date joinTime){
super(name,joinTime,0);
packStrategy = new PackStrategy(0,0,null);
actionRecords.add(new ActionRecord(name, DateUtil.formatDateToDay(joinTime)+"入网"));
}

public void randomCancelPack(Date orderedMonth){
int rand = new Random().nextInt(3);
switch(rand){
case 0:
/*if(packStrategy.getOrderedPhonePack() == null || packStrategy.getOrderedPhonePack().getPackType() == 0){
System.out.println(name + "试图退订根本就没有订过的电话套餐");
return;
}*/
packStrategy.cancelPhonePack(orderedMonth);
System.out.println(name + "退订了" + "电话套餐" + "(从" + DateUtil.formatDateToMonth(orderedMonth) + "开始)");
actionRecords.add(new ActionRecord("退订电话套餐",""));
break;
case 1:
/*if(packStrategy.getOrderedMessagePack() ==null || packStrategy.getOrderedMessagePack().getPackType() == 0){
System.out.println(name + "试图退订根本就没有订过的短信套餐");
return;
}*/
packStrategy.cancelMessagePack(orderedMonth);
System.out.println(name + "退订了" + "短信套餐" + "(从" + DateUtil.formatDateToMonth(orderedMonth) + "开始)");
actionRecords.add(new ActionRecord("退订短信套餐",""));
break;
case 2:
/*			if(packStrategy.getOrderedDataPack()==null || packStrategy.getOrderedDataPack().getPackType() == 0){
System.out.println(name + "试图退订根本就没有订过的数据套餐");
return;
}	*/
packStrategy.cancelDataPack(orderedMonth);
System.out.println(name + "退订了" + "数据套餐" + "(从" + DateUtil.formatDateToMonth(orderedMonth) + "开始)");
actionRecords.add(new ActionRecord("退订数据套餐",""));
break;
}
}

public void randomOrderPack(Date month){
int rand = new Random().nextInt(3);
switch(rand){
case 0:
//if(packStrategy.getOrderedPhonePack()==null || packStrategy.getOrderedPhonePack().getPackType() == 0){
packStrategy.orderPhonePack(month,1);
System.out.println(name + "订购了" + "电话套餐" + "(从" + DateUtil.formatDateToMonth(month) + "开始)");
actionRecords.add(new ActionRecord("定电话套餐",""));
//}
break;
case 1:
//if(packStrategy.getOrderedMessagePack() == null || packStrategy.getOrderedMessagePack().getPackType() == 0){
packStrategy.orderMessagePack(month,1);
System.out.println(name + "订购了" + "短信套餐" + "(从" + DateUtil.formatDateToMonth(month) + "开始)");
actionRecords.add(new ActionRecord("定短信套餐",""));
//}
break;
case 2:
//if(packStrategy.getOrderedDataPack() == null || packStrategy.getOrderedDataPack().getPackType() == 0){
packStrategy.orderDataPack(month,1);
System.out.println(name + "订购了" + "数据套餐" + "(从" + DateUtil.formatDateToMonth(month) + "开始)");
actionRecords.add(new ActionRecord("定数据套餐",""));
//}
break;
}
}
}


(七)VIPCustomer类(代表普通用户)

1.在构造方法中为策略存储对象赋初始值,即用户类型为1、各项功能的套餐类别为0,月租金或月基本费对象为按天计算月租金。

2.实现订购套餐和退订套餐的功能,通过产生随机数来决定是订购和退订套餐1或套餐2。订购套餐1或套餐2就是将各个功能套餐相应的策略对象的类型都设置为1或2,并设置相应的Rent对象;退订套餐就是将各个功能套餐相应的策略对象的类型都设置为0,并设置相应的Rent对象为按天计算月租金。其中的代码省略了一些琐碎的细节,包括阻止用户再次订购已经订购了的功能套餐、用户退订根本没有订购过的套餐。

源码如下:

public class DateUtil {
public static String formatDateToMonth(Date date){
SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月");
String result = sdf.format(date);
return result;
}

public static String formatDateToDay(Date date){
SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日");
String result = sdf.format(date);
return result;
}
}


(九)ComputeStrategy类(计算某项功能费用的策略类)

1.该类用于根据用户类型、套餐类型、业务功能类别来计算当月的某项业务功能的费用,其内部采用同一种算法公式来处理各种情况,仅仅参数值不同,所以,该中定义了三个属性来分别记住是哪种客户的哪种套餐下的哪种服务项,这样,要计算某种用户类型的某种套餐的某种业务功能的当月费用,只需要将这三个属性设置为相应的值即可。

2.如果VIP客户和common客户的不同套餐的每种服务费的算法公式区别很大,那就需要为每种客户的每种套餐的每种服务方式各设计一个子类,并将它们的共同之处抽象成为父类,譬如,定义PhoneComputeStrategy、MessageComputeStrategy、DataComputeStrategy等子类继承ComputeStragtegy。由于本系统可以采用同一种算法公式来处理各种情况,仅仅参数值不同,所以不需要采用多个子类的方式来做。

源码如下:

public class Rent {
private int price;
private RentUnit unit = RentUnit.MONTH;

public Rent(int price,RentUnit unit){
this.price = price;
this.unit = unit;
}

public int coputeRent(Date startTime,Date currentMonth){
//首先应该想到去找开源的日期运算类
if(unit == RentUnit.DAY){
Calendar start = Calendar.getInstance();
start.setTime(startTime);

Calendar end = Calendar.getInstance();
end.setTime(currentMonth);
//将日期设置为当月的最后一天
end.set(Calendar.DAY_OF_MONTH, 1);
end.add(Calendar.MONTH, 1);
end.add(Calendar.DAY_OF_MONTH, -1);

int days =  end.get(Calendar.DAY_OF_MONTH) ;
if(end.get(Calendar.MONTH) == start.get(Calendar.MONTH)){
days -= start.get(Calendar.DAY_OF_MONTH) + 1;
}

return  price*days;
}
else{
return price;
}
}
}


Rent类中所使用的枚举类RentUnit非常简单,其中仅仅是定义了MONTH和DAY两个成员, 其源码如下:

public class PackStrategy {
private int customerType;
private int packType = 0;
private ComputeStrategy currentStrategies[] = new ComputeStrategy[3];
private Rent rent;
private  OrderedStrategyHolder<ComputeStrategy> orderedStrategies[] = new OrderedStrategyHolder[]{
new OrderedStrategyHolder<ComputeStrategy>(),
new OrderedStrategyHolder<ComputeStrategy>(),
new OrderedStrategyHolder<ComputeStrategy>(),
};
private OrderedStrategyHolder<Rent> orderedRent = new OrderedStrategyHolder<Rent>();

public PackStrategy(int customerType,int packType,Rent rent){
this.customerType = customerType;
this.packType = packType;
this.rent = rent;
for(int i=0;i<3;i++){
currentStrategies[i] = new ComputeStrategy(customerType, packType,i);
}

}

public Rent getValidRent(Date month){
Rent validRent = orderedRent.getValidComputeStrategy(month);
return validRent==null?rent:validRent;

}

public void orderRent(Date orderedMonth,Rent rent){
Rent oldRent = orderedRent.order(orderedMonth, rent);
if(oldRent != null){
this.rent = oldRent;
}
}

public void cancelRent(Date orderedMonth,Rent rent){
orderRent(orderedMonth,null);
}

public ComputeStrategy getValidPhonePack(Date month){
ComputeStrategy computeStrategy = (ComputeStrategy)orderedStrategies[0].getValidComputeStrategy(month);
return computeStrategy==null?currentStrategies[0]:computeStrategy;
}

public ComputeStrategy getValidMessagePack(Date month){
ComputeStrategy computeStrategy = (ComputeStrategy)orderedStrategies[1].getValidComputeStrategy(month);
return computeStrategy==null?currentStrategies[1]:computeStrategy;
}

public ComputeStrategy getValidDataPack(Date month){
ComputeStrategy computeStrategy = (ComputeStrategy)orderedStrategies[2].getValidComputeStrategy(month);
return computeStrategy==null?currentStrategies[2]:computeStrategy;
}

public void orderPhonePack(Date orderedMonth,int packType){
ComputeStrategy oldComputeStrategy = orderedStrategies[0].order(orderedMonth, new ComputeStrategy(customerType, packType, 0));
if(oldComputeStrategy != null){
this.currentStrategies[0] = oldComputeStrategy;
}

}

public void orderMessagePack(Date orderedMonth, int packType){
ComputeStrategy oldComputeStrategy = orderedStrategies[1].order(orderedMonth, new ComputeStrategy(customerType, packType, 1));
if(oldComputeStrategy != null){
this.currentStrategies[1] = oldComputeStrategy;
}
}

public void orderDataPack(Date orderedMonth, int packType){
ComputeStrategy oldComputeStrategy = orderedStrategies[2].order(orderedMonth, new ComputeStrategy(customerType, packType, 2));
if(oldComputeStrategy != null){
this.currentStrategies[2] = oldComputeStrategy;
}
}

public void cancelPhonePack(Date orderedMonth){
orderPhonePack(orderedMonth, 0);
}

public void cancelMessagePack(Date orderedMonth){
orderMessagePack(orderedMonth, 0);
}

public void cancelDataPack(Date orderedMonth){
orderDataPack(orderedMonth, 0);
}

/*	public PhoneComputeStrategy getOrderedPhonePack(){
PhoneComputeStrategy phoneHolderStrategy = (PhoneComputeStrategy)phoneOrderedStrategyHolder.getOrderedStrategy();
return phoneHolderStrategy;
}

public MessageComputeStrategy getOrderedMessagePack(){
MessageComputeStrategy messageHolderStrategy = (MessageComputeStrategy)messageOrderedStrategyHolder.getOrderedStrategy();
return messageHolderStrategy;
}

public DataComputeStrategy getOrderedDataPack(){
DataComputeStrategy dataHolderStrategy = (DataComputeStrategy)dataOrderedStrategyHolder.getOrderedStrategy();
return dataHolderStrategy;
}	*/
}


(十二)OrderedStrategyHolder类(用于存储一项业务套餐订购信息)

1.用户订购一项业务功能套餐时需要记录生效的年月份和相应的业务功能套餐策略等两个信息,为此,需要设计一个订购记录类来管理这些信息,该类包含两个字段:套餐生效的年月份和订购的业务功能套餐策略。由于VIP用户订购套餐时,除了要记录各项业务功能的计费策略外,还要记录月租金或月基本费,月租金或月基本费的记录方式与记录一项业务功能的套餐方式完全一样,只是记录的参数类型不同,一个是ComputeStrategy策略对象,一个是Rent对象,所以,可以采用泛型技术来设计订购记录类,让它可以记录业务功能套餐策略的订购,也可以记录月租金或月基本费的订购。

2.本订购记录类中提供了一个order()方法来更新其中记录的套餐订购信息。前面讲解PackStrategy类的设计原理时已经提到过:因为原来订购的套餐策略的订购日期是存储在订购记录类中的,根据面向对象的封装特性,当我们订购新的计费策略时,原来订购的计费策略是否对当前月有效,即是否将原来订购的套餐信息赋值给PackStrategy中的用于记录原先或当前的套餐计费策略的变量, 这个功能不是在PackStrategy类中完成,而是在订购记录对象中完成。要防止设置下个月的计费策略时把当前月的计费策略冲掉,这个逻辑是在order方法中完成的,order方法中要根据原来记录的计费策略的生效年月份来判断是否要把原来记录的计费策略返回出去,如果原来记录计费策略的生效年月份已经是下个月了,则不能将此计费策略返回出去,这时候返回null,表示订购记录中原来存储的计费策略对当前月无效,PackStrategy类应使用其中的原先或当前的套餐计费策略的变量来计算当前月的费用,否则,
PackStrategy类应使用order方法返回的计费策略对象来计算当前月的费用。

3. 在计算某月的账单费用时,是否采用订购的从某个月开始的套餐计费策略,同样还是根据面向对象的封装特性,这一功能也应由订购记录类来提供。所以,本订购记录类中提供了一个getValidStrategy()方法来判断其中记录的套餐是否对某月有效,如果有效则返回该套餐策略对象,否则返回null。

源码如下:

public class OrderedStrategyHolder<T> {
private Date orderedMonth;
private T computeStrategy;

public T order(Date orderedMonth,T computeStrategy){
T oldComputeStrategy = null;
if(this.orderedMonth!=null && this.orderedMonth.before(orderedMonth)){
oldComputeStrategy = this.computeStrategy;
}
this.orderedMonth = orderedMonth;
this.computeStrategy = computeStrategy;

return oldComputeStrategy;
}

public T getValidComputeStrategy(Date month){
if(this.orderedMonth!=null && !month.before(orderedMonth)){
return computeStrategy;
}
return null;
}
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: