移动用户资费统计系统
模拟实现简易的移动用户资费统计系统逻辑,具体需求如下:
- 移动运营商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的完整内容如下:
- 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) + "--------------");