重构Java代码的既有设计-影片出租店
案例:计算每位顾客的消费金额并打印详细信息。顾客租赁了哪些影片,租期多长,根据租赁时间和影片类型计算出费用。影片分为3类:儿童片,新片,普通片。此外需计算该顾客的积分。
Movie:
- public class Movie {
- //电影类型
- public static final int CHILD = 2;
- public static final int NEW = 3;
- public static final int REGULAR = 1;
- private String _title;
- private int _priceCode;
- public Movie(String title,int priceCode) {
- this._title = title;
- this._priceCode = priceCode;
- }
- public String get_title() {
- return _title;
- }
- /**
- * 获取影片类型
- * @return
- */
- public int get_priceCode() {
- return _priceCode;
- }
- }
Resume:该顾客租赁了一部影片
- public class Resume {
- private Movie _movie;
- private int _daysRented;
- public Resume(Movie movie,int daysRented) {
- this._movie = movie;
- this._daysRented = daysRented;
- }
- public Movie get_movie() {
- return _movie;
- }
- public int get_daysRented() {
- return _daysRented;
- }
- }
Customer:
租赁费用计算:
影片类型为儿童片,两天以内费用为2,超出两天的时间,每天的费用为1.5
影片类型为新片,每天的费用为3
影片类型为普通片,三天以内费用为1.5,超出三天,每天的费用为1.5
积分计算:
每次租赁影片,积分加一,如果影片为新片且租赁时间大于1天,则多加一分
- import java.util.Enumeration;
- import java.util.Scanner;
- import java.util.Vector;
- public class Customer {
- private String _name;
- private Vector<Resume> _resume = new Vector<Resume>(); //all resume by this customer
- public Customer(String name){
- this._name = name;
- }
- /**
- * add resume info
- * @param arg
- */
- public void addRental(Resume arg){
- this._resume.addElement(arg);
- }
- public String getName(){
- return this._name;
- }
- /**
- * get all result(include time,movie type,fee of each resume and all fee)
- * @return result
- */
- public String statement(){
- double totalAmount = 0;
- int frequentRenterPoints = 0; //the all collectPoint;
- Enumeration<Resume> resumes = this._resume.elements(); //all record of resumes
- String result = "Rental Record for" +"\t" + this.getName() + "\n";
- while(resumes.hasMoreElements()){
- double thisAmount = 0; // fee of this record
- Resume each = (Resume) resumes.nextElement();
- // the movie's type
- switch(each.get_movie().get_priceCode()){
- case Movie.CHILD:
- thisAmount += 2; //the basic fee is 2
- if(each.get_daysRented() > 2){
- //the day is more than 2
- thisAmount += (each.get_daysRented() - 2) * 1.5;
- }
- break;
- case Movie.NEW:
- thisAmount += each.get_daysRented() * 3;
- break;
- case Movie.REGULAR:
- thisAmount += 1.5; //the basic fee is 1.5
- if(each.get_daysRented() > 3){
- //the day is more than 3
- thisAmount += (each.get_daysRented() - 3) * 1.5;
- }
- break;
- }
- frequentRenterPoints ++;
- if((each.get_movie().get_priceCode() == Movie.NEW)&&(each.get_daysRented() > 1)){
- frequentRenterPoints ++;
- }
- result += "\t" + each.get_movie().get_title() + "\t" + String.valueOf(thisAmount) + "\n";
- totalAmount += thisAmount;
- }
- result += "Amount owed is" + "\t" + String.valueOf(totalAmount) + "\n";
- result += "You earned "+ String.valueOf(frequentRenterPoints) + " frequent renter points";
- return result;
- }
- @SuppressWarnings("resource")
- public static void main(String arg[]){
- Scanner sc = new Scanner(System.in);
- System.out.println("please input your name:"+"\n");
- String c_name = sc.nextLine();
- Customer c1 = new Customer(c_name);
- System.out.println("please input the movie name:"+"\n");
- String m_name = sc.nextLine();
- System.out.println("please input the movie type:"+ "\n");
- System.out.println("1.regular movie"+"\n"+"2.child movie"+"\n"+"3.new movie"+"\n");
- int type = sc.nextInt();
- Movie m1 = new Movie(m_name,type);
- System.out.println("please input the time you have rent:"+"\n");
- int day = sc.nextInt();
- Resume r1 = new Resume(m1,day);
- c1.addRental(r1);
- String ans= c1.statement();
- System.out.println(ans);
- }
- }
现在的代码可以实现基本的功能,当租赁策略、积分策略发生改变时,需要仔细查找statement策略,这时很容易引入bug。那么就很有必要重构之前写的代码。
第一步:为即将修改的代码建立一个可靠的测试环境。
MovieTest
- import static org.junit.Assert.*;
- import org.junit.After;
- import org.junit.Before;
- import org.junit.Test;
- public class MovieTest {
- Movie m0 = new Movie("fall in love",3);
- @Before
- public void setUp() throws Exception {
- }
- @After
- public void tearDown() throws Exception {
- }
- @Test
- public void testGet_title() {
- assertEquals("fall in love",m0.get_title());
- }
- @Test
- public void testGet_priceCode() {
- assertEquals(3,m0.get_priceCode());
- }
- }
ResumeTest
- import static org.junit.Assert.*;
- import org.junit.After;
- import org.junit.Before;
- import org.junit.Test;
- public class ResumeTest {
- Movie m2 = new Movie("three children and their mother",2);
- Resume r2 = new Resume(m2,3);
- @Before
- public void setUp() throws Exception {
- }
- @After
- public void tearDown() throws Exception {
- }
- @Test
- public void testGet_movie() {
- Movie m3 = new Movie("three children and their mother",2);
- assertEquals(m3.get_title(),r2.get_movie().get_title());
- }
- @Test
- public void testGet_daysRented() {
- assertEquals(r2.get_daysRented(),3);
- }
- }
CustomerTest
- import static org.junit.Assert.*;
- import org.junit.Test;
- public class CustomerTest {
- Movie m1 = new Movie("123435",1);
- Resume r1 = new Resume(m1,4);
- Customer c1 = new Customer("abby");
- @Test
- public void testAddRental() {
- c1.addRental(r1);
- }
- @Test
- public void testGetName() {
- String testname = "abby";
- assertEquals(testname, c1.getName());
- }
- @Test
- public void testStatement() {
- String testResult = "Rental Record for abby"+"\n\t"+
- "123435 3.0"+"\n"+
- "Amount owed is 3.0"+"\n"+
- "You earned 1 frequent renter points";
- c1.addRental(r1);
- String realResult = c1.statement();
- assertEquals(testResult,realResult);
- }
- }
第二步:分解重组代码块
statement函数太长了,我们需要分解它,首先将switch语句包装到另外一个函数AmountFor中去,并更改名称使代码更加容易理解
- /**
- * calculate amount fee for this resume
- * @param resume
- * @return
- */
- private double AmountFor(Resume resume){
- double result = 0; // fee of this record
- switch(resume.get_movie().get_priceCode()){
- case Movie.CHILD:
- result += 2; //the basic fee is 2
- if(resume.get_daysRented() > 2){
- //the day is more than 2
- result += (resume.get_daysRented() - 2) * 1.5;
- }
- break;
- case Movie.NEW:
- result += resume.get_daysRented() * 3; //the basic fee is 2
- break;
- case Movie.REGULAR:
- result += 1.5; //the basic fee is 1.5
- if(resume.get_daysRented() > 3){
- //the day is more than 3
- result += (resume.get_daysRented() - 3) * 1.5;
- }
- break;
- }
- return result;
- }
AmountFor
原来的statement函数改为下面的代码
- /**
- * get all result(include time,movie type,fee of each resume and all fee)
- * @return result
- */
- public String statement(){
- double totalAmount = 0;
- int frequentRenterPoints = 0; //the all collectPoint;
- Enumeration<Resume> resumes = this._resume.elements(); //all record of resumes
- String result = "Rental Record for" +"\t" + this.getName() + "\n";
- while(resumes.hasMoreElements()){
- Resume each = resumes.nextElement();
- // get amount for each resume
- double thisAmount = this.AmountFor(each);
- frequentRenterPoints ++;
- if((each.get_movie().get_priceCode() == Movie.NEW)&&(each.get_daysRented() > 1)){
- frequentRenterPoints ++;
- }
- result += "\t" + each.get_movie().get_title() + "\t" + String.valueOf(thisAmount) + "\n";
- totalAmount += thisAmount;
- }
- result += "Amount owed is" + "\t" + String.valueOf(totalAmount) + "\n";
- result += "You earned "+ String.valueOf(frequentRenterPoints) + " frequent renter points";
- return result;
- }
Statement
在AmountFor中我们发现它只使用了Resume类,并没有使用到Movie,所以我们将AmountFor函数放在Resume类中,并将函数名改为GetCharge
- public class Resume {
- ......
- /**
- * calculate charge for this resume
- * @return
- */
- public double GetCharge(){
- double result = 0; // fee of this record
- switch(get_movie().get_priceCode()){
- case Movie.CHILD:
- result += 2; //the basic fee is 2
- if(get_daysRented() > 2){
- //the day is more than 2
- result += (get_daysRented() - 2) * 1.5;
- }
- break;
- case Movie.NEW:
- result += get_daysRented() * 3; //the basic fee is 2
- break;
- case Movie.REGULAR:
- result += 1.5; //the basic fee is 1.5
- if(get_daysRented() > 3){
- //the day is more than 3
- result += (get_daysRented() - 3) * 1.5;
- }
- break;
- }
- return result;
- }
- }
GetCharge
同时添加新的函数测试代码
- public class ResumeTest {
- ......
- @Test
- public void testGetCharge() {
- assertEquals(String.valueOf(r2.GetCharge()),String.valueOf(3.5));
- }
- }
testGetCharge
然后在原来的程序中找到旧函数的所有引用点,然后再用新函数去代替他们
接下来类似“费用计算”我们处理“积分计算”,直接显示修改后的代码
- public class Resume {
- ......
- /**
- * get FrequentRenterPoints for this resume
- * @return
- */
- public int GetFrequentRenterPoints(){
- int result = 0;
- result ++;
- if((get_movie().get_priceCode() == Movie.NEW)&&(get_daysRented() > 1)){
- result ++;
- }
- return result;
- }
- }
GetFrequentRenterPoints
- /**
- * get all result(include time,movie type,fee of each resume and all fee)
- * @return result
- */
- public String statement(){
- double totalAmount = 0;
- int frequentRenterPoints = 0; //the all collectPoint;
- Enumeration<Resume> resumes = this._resume.elements(); //all record of resumes
- String result = "Rental Record for" +"\t" + this.getName() + "\n";
- while(resumes.hasMoreElements()){
- Resume each = resumes.nextElement();
- frequentRenterPoints += each.GetFrequentRenterPoints();
- totalAmount += each.GetCharge();
- result += "\t" + each.get_movie().get_title() + "\t" + String.valueOf(each.GetCharge()) + "\n";
- }
- result += "Amount owed is" + "\t" + String.valueOf(totalAmount) + "\n";
- result += "You earned "+ String.valueOf(frequentRenterPoints) + " frequent renter points";
- return result;
- }
statement
然后接着提取totalAmount和totalFrequentRenterPoints
- public class Customer {
- ......
- /**
- * get total charge
- * @return
- */
- private double GetTotalCharge(){
- Enumeration<Resume> resumes = this._resume.elements(); //all record of resumes
- double result = 0;
- while(resumes.hasMoreElements()){
- Resume each = resumes.nextElement();
- result += each.GetCharge();
- }
- return result;
- }
- /**
- * get total frequentRenterPoints
- * @return
- */
- private int GetTotalFrequentRenterPoints(){
- Enumeration<Resume> resumes = this._resume.elements(); //all record of resumes
- int result = 0;
- while(resumes.hasMoreElements()){
- Resume each = resumes.nextElement();
- result += each.GetFrequentRenterPoints();
- }
- return result;
- }
- /**
- * get all result(include time,movie type,fee of each resume and all fee)
- * @return result
- */
- public String statement(){
- Enumeration<Resume> resumes = this._resume.elements(); //all record of resumes
- String result = "Rental Record for" +"\t" + this.getName() + "\n";
- while(resumes.hasMoreElements()){
- Resume each = resumes.nextElement();
- result += "\t" + each.get_movie().get_title() + "\t" + String.valueOf(each.GetCharge()) + "\n";
- }
- result += "Amount owed is" + "\t" + String.valueOf(GetTotalCharge()) + "\n";
- result += "You earned "+ String.valueOf(GetTotalFrequentRenterPoints()) + " frequent renter points";
- return result;
- }
- }
Customer
最后测试一下修改后的代码
现在你会发现statement函数所做的功能全部是字符串拼接,即界面显示工作,如果需要将结果显示成HTML或者是其他形式,直接添加相同功能函数即可。
第三步:使用类的特性(分装,继承,多态)和设计模式对程序继续重构
switch部分很容易发生修改,因为在修改影片费用策略时就会修改到switch部分,我们现在来重构switch部分
switch部分最好是在自己对象上使用,尽可能的避免在别人的对象上使用。所以这就提示我们需要把switch部分移到movie类中
- public class Movie {
- ......
- /**
- * calculate charge for resume
- * @return
- */
- public double GetCharge(int dayRent){
- double result = 0; // fee of this record
- switch(this.get_priceCode()){
- case Movie.CHILD:
- result += 2; //the basic fee is 2
- if(dayRent > 2){
- //the day is more than 2
- result += (dayRent - 2) * 1.5;
- }
- break;
- case Movie.NEW:
- result += dayRent * 3; //the basic fee is 2
- break;
- case Movie.REGULAR:
- result += 1.5; //the basic fee is 1.5
- if(dayRent > 3){
- //the day is more than 3
- result += (dayRent - 3) * 1.5;
- }
- break;
- }
- return result;
- }
- /**
- * get FrequentRenterPoints for resume
- * @return
- */
- public int GetFrequentRenterPoints(int dayRent){
- if((get_priceCode() == Movie.NEW)&&(dayRent > 1))
- return 2;
- else
- return 1;
- }
- }
Movie
- public class Resume {
- ......
- /**
- * calculate charge for this resume
- * @return
- */
- public double GetCharge(){
- return _movie.GetCharge(this._daysRented);
- }
- /**
- * get FrequentRenterPoints for this resume
- * @return
- */
- public int GetFrequentRenterPoints(){
- return _movie.GetFrequentRenterPoints(_daysRented);
- }
- }
Resume
影片类型有三种,而这三种影片的租赁价格都有其各自的计算方法,所以使用的是策略模式
下面是重构以后关于movie修改和新加的内容:
- public class Movie {
- //电影类型
- public static final int CHILD = 2;
- public static final int NEW = 3;
- public static final int REGULAR = 1;
- private String _title;
- private int _priceCode; //影片类型
- private Price _price;
- public Movie(String title,int priceCode) {
- this._title = title;
- this._priceCode = priceCode;
- set_priceCode();
- }
- public String get_title() {
- return _title;
- }
- public int get_priceCode() {
- return _price.getPriceCode();
- }
- public void set_priceCode() {
- switch(_priceCode){
- case Movie.CHILD:
- _price = new ChildPrice();
- break;
- case Movie.NEW:
- _price = new NewPrice();
- break;
- case Movie.REGULAR:
- _price = new RegularPrice();
- break;
- }
- }
- /**
- * calculate charge for resume
- * @return
- */
- public double GetCharge(int dayRent){
- return _price.getCharge(dayRent);
- }
- /**
- * get FrequentRenterPoints for resume
- * @return
- */
- public int GetFrequentRenterPoints(int dayRent){
- if((get_priceCode() == Movie.NEW)&&(dayRent > 1))
- return 2;
- else
- return 1;
- }
- }
Movie
- public abstract class Price {
- abstract int getPriceCode();
- abstract double getCharge(int dayRent);
- }
Price
- public class NewPrice extends Price {
- @Override
- int getPriceCode() {
- // TODO Auto-generated method stub
- return Movie.NEW;
- }
- @Override
- double getCharge(int dayRent) {
- return dayRent * 3;
- }
- }
NewPrice
- public class RegularPrice extends Price {
- @Override
- int getPriceCode() {
- // TODO Auto-generated method stub
- return Movie.REGULAR;
- }
- @Override
- double getCharge(int dayRent) {
- double result = 1.5; //the basic fee is 1.5
- if(dayRent > 3){ //the day is more than 3
- result += (dayRent - 3) * 1.5;
- }
- return result;
- }
- }
RegularPrice
- public class ChildPrice extends Price {
- @Override
- int getPriceCode() {
- // TODO Auto-generated method stub
- return Movie.CHILD;
- }
- @Override
- double getCharge(int dayRent) {
- double result = 2; //the basic fee is 2
- if(dayRent > 2){ //the day is more than 2
- result += (dayRent - 2) * 1.5;
- }
- return result;
- }
- }
ChildPrice
其实重构就是不断的测试修改的过程。
重构Java代码的既有设计-影片出租店的更多相关文章
- 怎样编写高质量的java代码
代码质量概述 怎样辨别一个项目代码写得好还是坏?优秀的代码和腐化的代码区别在哪里?怎么让自己写的代码既漂亮又有生命力?接下来将对代码质量的问题进行一些粗略的介绍.也请有过代码质量相关经验的朋友 ...
- 敏捷开发中高质量 Java 代码开发实践
Java 项目开发过程中,由于开发人员的经验.代码风格各不相同,以及缺乏统一的标准和管理流程,往往导致整个项目的代码质量较差,难于维护,需要较大的测试投入 和周期等问题. 这些问题在一个项目组初建.需 ...
- 如何在Android上编写高效的Java代码
转自:http://www.ituring.com.cn/article/177180 作者/ Erik Hellman Factor10咨询公司资深移动开发顾问,曾任索尼公司Android团队首席架 ...
- 老司机告诉你高质量的Java代码是怎么练成的?
一提起程序员,首先想到的一定是"码农",对,我们是高产量的优质"码农",我们拥有超跃常人的逻辑思维以及不走寻常路的分析.判别能力,当然,我们也有良好的编码规范, ...
- 写出优质Java代码的4个技巧(转)
http://geek.csdn.net/news/detail/238243 原文:4 More Techniques for Writing Better Java 作者:Justin Alban ...
- java代码分析及分析工具
一个项目从搭建开始,开发的初期往往思路比较清晰,代码也比较清晰.随着时间的推移,业务越来越复杂.代码也就面临着耦合,冗余,甚至杂乱,到最后谁都不敢碰. 作为一个互联网电子商务网站的业务支撑系统,业务复 ...
- 高效重构 C++ 代码
引言 Martin Fowler的<重构:改善既有代码的设计>一书从2003年问世至今已有十几年时间了,按照计算机领域日新月异的变化速度,重构已经算是一门陈旧的技术了.但是陈旧并不代表不重 ...
- 怎样编写高质量的 Java 代码
代码质量概述 怎样辨别一个项目代码写得好还是坏?优秀的代码和腐化的代码区别在哪里?怎么让自己写的代码既漂亮又有生命力?接下来将对代码质量的问题进行一些粗略的介绍.也请有过代码质量相关经验的朋友提出宝贵 ...
- 适用于Java开发人员的SOLID设计原则简介
看看这篇针对Java开发人员的SOLID设计原则简介.抽丝剥茧,细说架构那些事——[优锐课] 当你刚接触软件工程时,这些原理和设计模式不容易理解或习惯.我们都遇到了问题,很难理解SOLID + DP的 ...
随机推荐
- javascript对象继承
一.实例化和继承的区别 构造函数.原型和实例的关系:每 个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型 对象的内部指针. 类(Class)和实例(Insta ...
- 一.复习GCC编译器的用法
1.复习GCC编译器的用法 欲善其工,那么要先利其器.在这个C语言巩固与提高的阶段中,如果想要更好的达成预期目标,首先就要熟练掌握GCC编译器的用法.以下是GCC相关知识: GCC使用语法 gcc 选 ...
- ipythons 使用攻略
ipython是一个 python 的交互式 shell,比默认的 python shell 好用得多,支持变量自动补全,自动缩进,支持 bash shell 命令,内置了许多很有用的功能和函数. 安 ...
- [随记][asp.net基础]Page_Load和OnLoad
标题:[随记][asp.net基础]Page_Load和OnLoad 一.前言 东西好久不用.不想,就会忘,所以没办法,只好记下来. 二.正文 aspx页面加载的时候会自动执行Page_Load,也会 ...
- opkg 不能更新和安装openwrt软件的方法
首先,将所有的IPK 放在自己的虚拟HTTP服务器上.2,用Telnet进入路由器,使用VI编辑器,编程Opkg.conf,命令: vi /etc/opkg.conf3,修改文件,将第一行 ...
- 20145329《Java程序设计》实验四总结
实验四 Android环境搭建 实验内容 1.搭建Android环境 2.运行Android 3.修改代码,能输出学号 实验步骤 1.搭建Android环境 2.安装Android,核心是配置JDK. ...
- Calling Convention的总结
因为经常需要和不同的Calling Convention打交道,前段时间整理了一下它们之间的区别,如下: 清理堆栈 参数压栈顺序 命名规则 (MSVC++) 备注 Cdecl 调用者 (Caller) ...
- Linux禁止普通用户使用crontab命令
cron计划任务默认root用户与非root用户都可以执行,当然如果在安全方面想禁用这部分用户,则可以通过两个文件来解决: cron.allow cron.deny cron.allow:定义允许使用 ...
- 第八篇:Spark SQL Catalyst源码分析之UDF
/** Spark SQL源码分析系列文章*/ 在SQL的世界里,除了官方提供的常用的处理函数之外,一般都会提供可扩展的对外自定义函数接口,这已经成为一种事实的标准. 在前面Spark SQL源码分析 ...
- rsync | scp文件同步命令使用
现在有一台服务器A,目录/data2/abc下存在若干文件夹和文件,需要复制到服务器B中.这时,可以在服务器A上执行rsync或者scp命令,将文件夹或文件复制到服务器B中. SCP: scp /da ...