•数据分析与统一计算公式:
分析本系统的业务,可以看到普通用户和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的完整内容如下:
common.normal.phone.price=600
common.normal.message.price=100
common.normal.data.price=50
common.pack1.phone.price=500
common.pack1.message.price=100
common.pack1.data.price=30
common.pack1.phone.rent=20000
common.pack1.message.rent=10000
common.pack1.data.rent=20000
common.pack1.phone.free=60
common.pack1.message.free=200
common.pack1.data.free=5000
vip.normal.phone.price=400
vip.normal.message.price=100
vip.normal.data.price=30
vip.pack1.phone.price=300
vip.pack1.message.price=100
vip.pack1.data.price=10
vip.pack1.phone.free=750
vip.pack1.message.free=200
vip.pack1.data.free=10000
vip.pack2.phone.price=200
vip.pack2.message.price=100
vip.pack2.data.price=5
vip.pack2.phone.free=2000
vip.pack2.message.free=500
vip.pack2.data.free=30000
common.new.phone.free=60
common.new.message.free=200
common.new.data.free=5000
vip.new.phone.free=200
vip.new.message.free=200
vip.new.data.free=10000
接着可编写一个读取上面的配置文件中的各项数据的ConfigManager类,该类根据用户类型、套餐类型、业务功能类别来读取相应功能套餐的单价、免费数量、功能费,以及新用户免费数量。源码如下:
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),这个方法内部调用的某个用户打电话、某个用户发短信、某个用户传输数据、某个用户订购套餐、某个用户退订套餐、新用户入网等功能又各自封装成一个私有方法。
源码如下:
package cn.itcast.mobilecounter.strategy;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.Random;
public class MobileCorporation {
private ArrayList<Customer> customers = new ArrayList<Customer>();
public MobileCorporation(){
for(int i=1;i<=15;i++){
customers.add(
new CommonCustomer(i+"号普通客户",new Date(108,10,1))
);
}
for(int i=1;i<=5;i++){
customers.add(
new VIPCustomer(i+"号VIP客户",new Date(108,10,1))
);
}
System.out.println("程序创建了运营商已有的5个VIP用户和15个普通用户,并设置他们的入网日期为2008年10月1日.");
}
//模拟某个月的业务活动
public void simulationBusiness(Date month){
for(Customer customer : customers){
customer.monthBegin();
}
System.out.println("--------being simulating " + DateUtil.formatDateToMonth(month) + "--------------");
for(int i=0;i<500/*30*/;i++){
randDoOneThing(month);
}
System.out.println(DateUtil.formatDateToMonth(month)+"的计费汇总清单:");
//汇总所有人的账单
for(int i=0;i<customers.size();i++){
customers.get(i).countMonthMoney(month);
}
}
/**
* 随机调用下面的某一个方法
* */
private void randDoOneThing(Date month){
Calendar calendar = Calendar.getInstance();
calendar.setTime(month);
calendar.add(Calendar.MONTH, 1);
Date monthOfOrderPack = calendar.getTime();
/*让orderPack、cancelPack、joinNewCustomer的出现概率是其他操作的1/20。*/
int rand = new Random().nextInt(63);
if(rand>=0 && rand<20){
callPhone();
}
else if(rand>=20 && rand<40){
sendMessage();
}
else if(rand>=40 && rand<60){
transferData();
}else{
switch(rand){
case 60:
orderPack(monthOfOrderPack);
break;
case 61:
cancelPack(monthOfOrderPack);
break;
case 62:
joinNewCustomer(month);
break;
}
}
}
/**
* 随机选中一个用户,让其随机拨打的电话时长为1至10分钟不等
*/
private void callPhone(){
int rand = new Random().nextInt(customers.size());
Customer customer = customers.get(rand);
int phoneTimes = new Random().nextInt(10) + 1;
customer.callPhone(phoneTimes);
System.out.println(customer + "打了" + phoneTimes + "分钟电话");
}
/**
* 随机选中一个用户,让其随机发送的短信数目为1至10条不等
*/
private void sendMessage(){
int rand = new Random().nextInt(customers.size());
Customer customer = customers.get(rand);
int messageNumbers = new Random().nextInt(10) + 1;
customer.sendMessage(messageNumbers);
System.out.println(customer + "发了" + messageNumbers + "条短信");
}
/**
* 随机选中一个用户,让其随机获取的数据流量为50K,100K,200K,500K,1M
*/
private void transferData(){
int rand = new Random().nextInt(customers.size());
Customer customer = customers.get(rand);
int [] dataSize = new int[]{50,100,200,500,1000};
int randSizeKey = new Random().nextInt(5);
customer.transferData(dataSize[randSizeKey]);
System.out.println(customer + "传送了" + dataSize[randSizeKey] + "k数据");
}
/**
* 随机选中一个用户,为其随机订购一款套餐
*/
private void orderPack(Date month){
int rand = new Random().nextInt(customers.size());
customers.get(rand).randomOrderPack(month);
}
/**
* 随机选中一个用户,并将其已经有的套餐取消
*/
private void cancelPack(Date month){
int rand = new Random().nextInt(customers.size());
customers.get(rand).randomCancelPack(month);
}
private void joinNewCustomer(Date month){
Calendar calendar = Calendar.getInstance();
calendar.setTime(month);
int maxDay = calendar.getMaximum(Calendar.DAY_OF_MONTH);
int randDay = new Random().nextInt(maxDay) + 1;
/*下面的复制过程很重要,不能直接修改date,当然最好是对Calendar直接操作
* 这里是为了演示要注意clone而保留的。
*/
Date joinTime = (Date)month.clone();
joinTime.setDate(randDay);
int randType = (new Random().nextInt(10))%2;
Customer customer = null;
if(randType == 0){
int commonId = IdGenerator.getInstance().nextCommonId();
customer = new CommonCustomer(commonId+"号普通客户",joinTime);
customers.add(customer);
}else{
int vipId = IdGenerator.getInstance().nextVipId();
customer = new VIPCustomer(vipId+"号VIP客户",joinTime);
customers.add(customer);
}
System.out.println(DateUtil.formatDateToDay(joinTime) + "新注册了" + customer);
}
}
(二)IdGenerator类(新用户生成新Id编号)
1.由于系统中原来已经有了15名普通用户和5名VIP用户,所以,新产生的普通用户的编号应从16开始,新产生的VIP用户的编号应该从6开始。
2.把该类写成单例,逻辑上更为严谨,在前面的博文中有人批评我写程序不必那么严谨、凑合即可,那就请思考为什么我们一直做不出奔驰这样精致的汽车的原因,因为凑合的想法在很多国人心中已经根深蒂固了,马马虎虎已经是我等中国人的优良传统了,但我并不想继承这一优良传统。
源码如下:
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 class MainClass {
public static void main(String[] args) {
MobileCorporation corp = new MobileCorporation();
//设置要模拟的起始月份
Date month = new Date(109,0,1);
System.out.println("程序开始模拟从2009年1月1日开始,连续15个月的运行情况.");
//总共模拟15个连续的月份
for(int i=0;i<15/*3*/;i++){
corp.simulationBusiness(month);
Calendar calendar = Calendar.getInstance();
calendar.setTime(month);
calendar.add(Calendar.MONTH, 1);
month = calendar.getTime();
}
}
}
(四)Customer类(普通用户与VIP用户的抽象父类)
1.在Customer类中定义了用户名称和入网时间;还定义了4个List集合的成员变量,分别用于存储用户每次拨打的电话时间、每次发送的短信数量和每次传输的数据量,以及用户的操作列表;此外, Customer类中还定义了一个用于计算当月账单费用的策略存储对象,它用于存储用户选择的各项功能套餐的策略对象。
2.在Customer类中定义打电话、发短信、传输数据的方法,这些方法分别为对应的List集合增加记录,并且都需要向操作列表集合中增加记录。此外,由于普通用户和VIP用户定套餐和退订套餐的方式有很大的区别,所以,这两个功能在Customer类中对应的方法被定义成了抽象的,这两个方法由普通用户和VIP用户根据自己的实际情况去具体实现。
3.在Customer类中定义了一个countMonthMoney方法来计算用户当前月的账单费用,首先从电话、短信、数据等集合中累加出各项功能的数量,对于新用户,则减去新用户要优惠的数量,然后再将这些数量分别传递给用户订购的相应的策略对象去计算费用,最后汇总和打印出总费用。
源码如下:
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 ActionRecord {
private String name;
private String value;
public ActionRecord(String name, String value) {
this.name = name;
this.value = value;
}
public String toString(){
return name + ":" + value;
}
}
(六)CommonCustomer类(代表普通用户)
1.在构造方法中为策略存储对象赋初始值,即用户类型为0、各项功能的套餐类别为0,月租金或月基本费对象为null。
2.实现订购套餐和退订套餐的功能,通过产生随机数来决定是订购和退订哪一项功能的套餐,订购某个功能套餐就是将相应的策略对象的类型设置为1,退订某个功能套餐就是将相应的策略对象的类型设置为0。其中的代码省略了一些琐碎的细节,包括阻止用户再次订购已经订购了的功能套餐、阻止用户退订根本没有订购过的套餐。
源码如下:
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 VIPCustomer extends Customer {
public VIPCustomer(String name,Date joinTime){
super(name,joinTime,1);
packStrategy = new PackStrategy(1,0,new Rent(200,RentUnit.DAY));
actionRecords.add(new ActionRecord(name, DateUtil.formatDateToDay(joinTime)+"入网"));
}
public void orderPack1(Date month){
/* if(packStrategy.getOrderedPhonePack()==null || packStrategy.getOrderedPhonePack().getPackType() != 1)
{ */
packStrategy.orderRent(month,new Rent(10000,RentUnit.MONTH));
packStrategy.orderPhonePack(month,1);
packStrategy.orderMessagePack(month,1);
packStrategy.orderDataPack(month,1);
System.out.println(name+ "订购了套餐1" + "(从" + DateUtil.formatDateToMonth(month) + "开始)" );
actionRecords.add(new ActionRecord("订购套餐1",""));
//}
}
public void orderPack2(Date month){
/* if(packStrategy.getOrderedPhonePack() ==null || packStrategy.getOrderedPhonePack().getPackType() != 2)
{*/
packStrategy.orderRent(month,new Rent(20000,RentUnit.MONTH));
packStrategy.orderPhonePack(month,2);
packStrategy.orderMessagePack(month,2);
packStrategy.orderDataPack(month,2);
System.out.println(name+ "订购了套餐2" + "(从" + DateUtil.formatDateToMonth(month) + "开始)" );
actionRecords.add(new ActionRecord("订购套餐2",""));
//}
}
public void randomCancelPack(Date orderedMonth){
/*
if(packStrategy.getOrderedPhonePack() ==null ||
packStrategy.getOrderedPhonePack().getPackType() == 0){
System.out.println(name + "试图退订根本就没有订过的套餐"); return; }
*/
packStrategy.orderRent(orderedMonth, new Rent(200, RentUnit.DAY));
packStrategy.orderPhonePack(orderedMonth, 0);
packStrategy.orderMessagePack(orderedMonth, 0);
packStrategy.orderDataPack(orderedMonth, 0);
System.out.println(name + "退订了" + "套餐" + "(从"
+ DateUtil.formatDateToMonth(orderedMonth) + "开始)");
actionRecords.add(new ActionRecord("退定套餐", ""));
}
public void randomOrderPack(Date month){
//如果以前订购过某套餐,现在仍然可以重新订购该套餐
int randType = (new Random().nextInt(10))%2;
if(randType == 0){
orderPack1(month);
}else if(randType == 1){
orderPack2(month);
}
}
}
(八)DateUtil类(将日期格式化成字符串的辅助类)
源码如下:
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 ComputeStrategy {
private int customerType;
private int packType;
private int businessType;
private String businessName = "";
public ComputeStrategy(int customerType, int packType, int businessType) {
this.customerType = customerType;
this.packType = packType;
this.businessType = businessType;
switch(businessType){
case 0:businessName = "电话";break;
case 1:businessName = "短信";break;
case 2:businessName = "数据";break;
}
}
public int computeMoney(int quantity){
int price = ConfigManager.getPrice(customerType, packType,businessType);
int freeQuantity = ConfigManager.getFree(customerType, packType,businessType);
int chargeQuantity = quantity - freeQuantity;
if(chargeQuantity < 0){
chargeQuantity = 0;
}
int phoneBaseMoney = ConfigManager.getRent(customerType, packType,businessType);
System.out.print(businessName + "功能费:" + phoneBaseMoney + "厘钱,");
int fee = price * chargeQuantity;
System.out.println(businessName + "计价费:" + quantity + "-" + freeQuantity + "=" + chargeQuantity + ","
+ chargeQuantity + "*" + price + "=" + fee +"厘钱");
return phoneBaseMoney + fee;
}
}
(十)Rent类(用于计算VIP用户月租费或月基本费)
1.VIP用户需要支付月租费或月基本费,如果是月租费,则还需要按照天来计算,这个计算相对来说也有一点复杂,为此,可以将计算月租费或月基本费的功能封装到一个Rent类中,这样对整个计算账单费用的程序模块来说,就不需要关心具体的计算细节了,只需要调用Rent类的实例对象即可得到用户要缴纳的月租费或月基本费。
源码如下:
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两个成员, 其源码如下:
view plaincopy to clipboardprint?
01.public enum RentUnit {
02. DAY,MONTH;
03.}
public enum RentUnit {
DAY,MONTH;
}
(十一)PackStrategy类(用于存储用户的各项业务套餐策略)
这个类中的程序代码的逻辑最为复杂,建议读者在阅读此类的代码之前,先思考清楚如下问题:
1.假如用户在2月时订购从3月以后的套餐,那么在结算2月份的费用时,不能使用订购的套餐计费方式,而应该使用原先的套餐计费方式;结算3月以后的费用时,就应采用订购的套餐计费方式了。显然,要处理某项业务功能的月账单费用,在PackStrategy中类中要为之存储两份ComputeStrategy对象,一份是原先或当前的套餐计费策略,另一份是订购的从某个月开始的套餐计费策略。
2.
问: 假设2月份的时候订3月以后的套餐, 2月底计算2月份的费用时用哪个套餐?3月底计算3月份的费用时用哪个套餐?
答: 2月底计算2月份的费用时用PackStrategy中的原先或当前的套餐计费策略, 3月底计算3月份的费用时用订购的套餐计费策略。
总结:当计算某月的账单费用时,是采用PackStrategy中的原先或当前的套餐计费策略,还是采用订购的从某个月开始的套餐计费策略呢?这就要看是在计算哪个月的账单费用了,如果“计算账单费用的当前年月份”大于或等于“订购的从某个月开始的套餐计费策略的起效年月份”,那么就要用订购的从某个月开始的套餐计费策略,否则,就采用PackStrategy中的原先或当前的套餐计费策略。
3.
问: 2月的初始计费策略为套餐0, 2月份的时候订3月以后的套餐1,3月份的时候又订4月以后的套餐2,请问,3月底计算3月份的费用时用哪个套餐?
答: 3月底计算3月份的费用时应该用套餐1。在程序代码中,在3月订4月的套餐2时,如果直接对订购变量进行赋值,它将冲掉 2月份订购的3月份以后的套餐1信息,而PackStrategy中的原先或当前的计费策略为套餐0,这样在3月底计算3月份的费用时,将得到错误的结果。所以,应该先将2月份订购的3月份以后的套餐1赋值给PackStrategy中的用于记录原先或当前的套餐计费策略的变量,再用3月订4月的套餐2对订购变量赋值,这样在3月底计算3月份的费用时,采用PackStrategy中的原先或当前的套餐计费策略,正好就是2月份订购的3月份以后的套餐1。
4.
问: 2月的初始计费策略为套餐0, 2月份的时候订了3月以后的套餐1,随后,还是在2月份的时候又重新订购3月以后的套餐,改订为套餐2,请问,2月底计算2月份的费用时用哪个套餐?
答:还应该用2月的初始计费策略套餐0。也就是说,在2月份的时候又重新订购3月以后的套餐2时,应该直接对订购变量进行赋值,让它冲掉 2月份订购的3月份以后的套餐1信息,而不要将2月份订购的3月份以后的套餐1赋值给PackStrategy中的用于记录原先或当前的套餐计费策略的变量,这一点与上面的情况正好不同。
总结:当重新订购新的套餐时,只有原来订购的套餐对当前的年月份起作用时(也就是 “订购变量中存储的日期”小于或等于当前的年月份,即“新订购的套餐的年月份”大于“订购变量中存储的日期”,因为订购的年月份总是当前月份基础上加1),原来订购的套餐信息才要赋值给PackStrategy中的用于记录原先或当前的套餐计费策略的变量,以便计算当前月的费用时使用,否则,如果原来订购的套餐对当前的年月份是不起作用,那么,则不能将原来订购的套餐信息才要赋值给PackStrategy中的用于记录原先或当前的套餐计费策略的变量, 让它直接被新订购的策略冲掉即可。
5.根据面向对象的封装特性,当我们订购新的计费策略时,原来订购的计费策略是否对当前月有效,即是否将原来订购的套餐信息赋值给PackStrategy中的用于记录原先或当前的套餐计费策略的变量, 这个功能不是在PackStrategy类中完成,而是在订购记录对象中完成,因为原来订购的套餐策略的订购日期是存储在订购记录类中的。同样的道理,在计算某月的账单费用时,是否采用订购的从某个月开始的套餐计费策略,这一功能也是由订购记录类来提供。
源码如下:
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;
}
}
以上笔记参照张孝祥老师讲义。