OOP题目集7-9总结

前言

这三次题目集的题量很少,每次题目集基本上就只有1-2题,难度在加大,每题的代码量也上去了,做题时间给的相当充裕,所以有足够时间去思考。题目集7蕴含了继承与多态的应用、ArrayList泛型的应用方法、Comparable接口及泛型的应用,还有单一职责原则与开闭原则的应用,从题目集8开始,完全要自己设计类的结构与类间关系,真正的考验开始了!另外,在题目集9开始前,老师给出了题目集8的参考源码,而题目集9其实就是题目集8的延申,所以在开始题目集9前,读懂老师给出的题目集8参考源码很重要,主要是要去理解参考源码体现的思路与设计,然后在参考源码的基础上进行重构来完成题目集9。

题目集07

7-1 图形卡片排序游戏

输入格式:

  • 首先,在一行上输入一串数字(1~4,整数),其中,1代表圆形卡片,2代表矩形卡片,3代表三角形卡片,4代表梯形卡片。各数字之间以一个或多个空格分隔,以“0”结束。例如: 1 3 4 2 1 3 4 2 1 3 0
  • 然后根据第一行数字所代表的卡片图形类型,依次输入各图形的相关参数,例如:圆形卡片需要输入圆的半径,矩形卡片需要输入矩形的宽和长,三角形卡片需要输入三角形的三条边长,梯形需要输入梯形的上底、下底以及高。各数据之间用一个或多个空格分隔。

输出格式:

  • 如果图形数量非法(小于0)或图形属性值非法(数值小于0以及三角形三边不能组成三角形),则输出Wrong Format
  • 如果输入合法,则正常输出,所有数值计算后均保留小数点后两位即可。输出内容如下:
  1. 排序前的各图形类型及面积,格式为图形名称1:面积值1图形名称2:面积值2 …图形名称n:面积值n ,注意,各图形输出之间用空格分开,且输出最后存在一个用于分隔的空格;
  2. 排序后的各图形类型及面积,格式同排序前的输出;
  3. 所有图形的面积总和,格式为Sum of area:总面积值

首先,这题主要是考察了类的继承、多态性使用方法以及接口的应用。我建立了8个类,分别是Card、Circle、DealCardList、Rectangle、Shape、Trapezoid、Triangle和Main类,

其中Shape类作为抽象类,也是Circle、Rectangle、Trapezoid、Triangle的父类,该类只包含了2个属性,分别为:

private String shapeName;
private double shapeArea;

然后就是两个抽象方法:

public abstract boolean validate();
public abstract double getArea();

另外,在Shape类中,需要重写toString( )方法:

@Override
public String toString() {
	return getShapeName()+":"+String.format("%.2f ",getArea());
}

由于图形需要排序,所以Card类实现了Comparable接口,Comparable接口中有一个方法为compareTo( );

compareTo( )方法介绍:

int compare(T o1, T o2);
用来比较两个对象,如果o1小于o2,返回负数;等于o2,返回0;大于o2返回正数
compareTo是按照按字典顺序去比较两个字符串的,一般常用的字符串类型无非3个,阿拉伯数字格式,英文字母格式以及中文格式。其中数字格式的顺序最简单,英文字母格式次之。

DealCardList就相当于一个中介类,在这个中介类中将进行对图形排序的方法、对图形属性的非法属性检验、输出图形信息等。

下面对代码进行分析:

Card、Circle、Rectangle、Shape、Trapezoid、Triangle类中都只是包含了一些构造方法和getter、setter方法,所以复杂度并不高。核心代码主要集中在DealCardList这个中介类中,所以复杂度自然就比其他类要高一些,DealCardList复杂度为7。

主要来分析DealCardList类:

复杂度其实并不高,此类中,我觉得需要注意的一个就是输入问题,在Main类中定义一个静态Scanner对象,这样在其它类中如果想要使用该对象进行输入,则直接使用Main.input.next…即可(避免采坑)

/*
 *这里给出部分代码示例
 */
public DealCardList(ArrayList<Integer> list) {
		//遍历list
		for(int i = 0;i<list.size();i++) {
			switch(list.get(i)) {
			case 1://圆形卡片
				//输入半径
				double radius = Main.input.nextDouble();
				Circle circle = new Circle(radius);
//				Card card = new Card(circle);
				cardList.add(new Card(circle));
				break;

7-2 图形卡片分组游戏

输入格式:

  • 在一行上输入一串数字(1~4,整数),其中,1代表圆形卡片,2代表矩形卡片,3代表三角形卡片,4代表梯形卡片。各数字之间以一个或多个空格分隔,以“0”结束。例如:1 3 4 2 1 3 4 2 1 3 0
  • 根据第一行数字所代表的卡片图形类型,依次输入各图形的相关参数,例如:圆形卡片需要输入圆的半径,矩形卡片需要输入矩形的宽和长,三角形卡片需要输入三角形的三条边长,梯形需要输入梯形的上底、下底以及高。各数据之间用一个或多个空格分隔。

输出格式:

  • 如果图形数量非法(<=0)或图形属性值非法(数值<0以及三角形三边不能组成三角形),则输出Wrong Format
  • 如果输入合法,则正常输出,所有数值计算后均保留小数点后两位即可。输出内容如下:
  1. 排序前的各图形类型及面积,格式为[图形名称1:面积值1图形名称2:面积值2 …图形名称n:面积值n ],注意,各图形输出之间用空格分开,且输出最后存在一个用于分隔的空格,在结束符“]”之前;
  2. 输出分组后的图形类型及面积,格式为[圆形分组各图形类型及面积][矩形分组各图形类型及面积][三角形分组各图形类型及面积][梯形分组各图形类型及面积],各组内格式为图形名称:面积值。按照“Circle、Rectangle、Triangle、Trapezoid”的顺序依次输出;
  3. 各组内图形排序后的各图形类型及面积,格式同排序前各组图形的输出;
  4. 各组中面积之和的最大值输出,格式为The max area:面积值

与上一题不同的是,这题需要给图形卡片分组,输出分组后的图形类型和面积。所以在DealCardList类中,我准备用4个ArrayList来分别存储Circle、Rectangle、Triangle、Trapezoid类型卡片,如下:

	ArrayList<Card> cardList = new ArrayList<Card>();//存储所有类型的图形卡片
	ArrayList<Card> circleList = new ArrayList<Card>();//存储Circle类图形卡片
	ArrayList<Card> rectangleList = new ArrayList<Card>();//存储Rectangle类图形卡片
	ArrayList<Card> triangleList = new ArrayList<Card>();//存储Triangle类图形卡片
	ArrayList<Card> trapezoidList = new ArrayList<Card>();//存储Trapezoid里图形卡片

然后就需要写一个分组功能的方法,这4个ArrayList将在该方法中用上,当检验到图片类型为Circle、Rectangle、Triangle或Trapezoid类型时,就分别放入相应的ArrayList中。

其实,关于分组的方法这块儿,我写的就很重复啰嗦了,因为我对检验图片类型为Circle、Rectangle、Triangle或Trapezoid分别都写了一个方法:

private void seperateCircle() {
		// TODO Auto-generated method stub
		System.out.print("[");
		for(Card item : this.cardList) {
			if(item.getShape().getClass().getName().equals("Circle")) {
				this.circleList.add(item);
				System.out.print(item.getShape().toString());
			}
		}
		System.out.print("]");
	}

除了seperateCircle()方法,我还有seperateRectangle()、seperateTriangle()、seperateTrapezoid()方法,代码基本类似,只有一句代码需要改动,即:

if(item.getShape().getClass().getName().equals("Circle"))

equals()方法中的参数需要改动为Rectangle、Triangle、Trapezoid。

改进:

其实这四个方法完全可以合成写为一个方法,可以考虑把类名Circle、Rectangle、Triangle和Trapezoid放进一个String数组,把该String数组作为分组方法的参数,遍历该数组,当cardList里的元素类名与String数组中的元素匹配上时,就把cardList中的该元素添加到相应的图形list,完成图形卡片分组。

而关于输出面积之和的最大值,可以将图形卡片面积list用list.toArray()方法将ArrayList转换成Array数组,然后采用Array类中的sort()方法对数组元素进行排序,最大值就为排序后数组的最后一个元素。

**用sourceMonitor对代码进行分析:

大体上和7-1题类似,也就只有DealCardList类中做了很大改动。

下面是DealCardList类的代码分析情况:

该类包含了11个方法,这与我上面说到的在给图形卡片进行分组时,方法写得太啰嗦了的后果,如果再根据我后来提出的改进再重写,方法数量会减少3。所幸的是,DealCardList类的最大复杂度为7,这个复杂度属于清晰了,毕竟该类需要用到的方法也并不复杂,只要再稍加注意减少分支结构和把每个功能模块化就能适当降低圈复杂度。

题目集08

终于进入到自己设计类的时候了!

7-1 ATM机类结构设计(一)

设计ATM仿真系统,具体要求参见作业说明。

输入格式:

每一行输入一次业务操作,可以输入多行,最终以字符#终止。具体每种业务操作输入格式如下:

  • 存款、取款功能输入数据格式: 卡号 密码 ATM机编号 金额(由一个或多个空格分隔), 其中,当金额大于0时,代表取款,否则代表存款。
  • 查询余额功能输入数据格式: 卡号

输出格式:

①输入错误处理

  • 如果输入卡号不存在,则输出Sorry,this card does not exist.
  • 如果输入ATM机编号不存在,则输出Sorry,the ATM's id is wrong.
  • 如果输入银行卡密码错误,则输出Sorry,your password is wrong.
  • 如果输入取款金额大于账户余额,则输出Sorry,your account balance is insufficient.
  • 如果检测为跨行存取款,则输出Sorry,cross-bank withdrawal is not supported.

②取款业务输出

输出共两行,格式分别为:

[用户姓名]在[银行名称]的[ATM编号]上取款¥[金额]
当前余额为¥[金额]

其中,[]说明括起来的部分为输出属性或变量,金额均保留两位小数。

③存款业务输出

输出共两行,格式分别为:

[用户姓名]在[银行名称]的[ATM编号]上存款¥[金额]
当前余额为¥[金额]

其中,[]说明括起来的部分为输出属性或变量,金额均保留两位小数。

④查询余额业务输出

¥[金额]

金额保留两位小数。

和以往不同,这次题目集没有给出类图,要设计多少个类、类间关系又该是如何,这些全部都要自己去思考。

按照我自己的想法,我一共建了10个类,其中包括中国银联(China UnionPay)、银行(Bank)、银行用户(User)、银行账户(Account)、银行卡(Card)、ATM这6个实体类,接着是Initialization(用户初始化数据)、Check(检验数据)、Agent(用于进行各项银行业务),这3个类应该是算是业务类吧,最后是Main类,用于启动程序。

下面是用powerdesigner生成的类图:

中国银联(China UnionPay)、银行(Bank)、银行用户(User)、银行账户(Account)、银行卡(Card)、ATM这6个实体类的代码行数相比其他类较少,但是这几个类间的关系却需要我们好好思考一下。

  1. 中国银联是中国银行卡联合组织,包含多家银行机构,所以China UnionPay类中会需要声明类型为Bank的私有属性,而中国银联与银行机构为一对多关系,所以应如下声明:

    private ArrayList<Bank> list = new ArrayList<Bank>();//中国银联包含多家银行机构
    
  2. 银行具有名称,一个银行机构可以包含多个银行用户,一个银行机构也可以包含多个银行账户,并且ATM机隶属于银行,所以说,银行和用户、账户、ATM机都是一对多关系:

    private String name;//银行名称
    private ArrayList<User> userList;//一个银行机构可以拥有多个银行用户
    private ArrayList<Account> acountList = new ArrayList<Account>();//一个银行机构可以拥有多个银行账户
    private ArrayList<ATM> atmList;//ATM隶属于银行机构
    
  3. 账户需要具有的属性有String类型的银行账号、double类型的余额(这里要注意的是余额应该是在账户上,而不是在银行卡上),另外,一个账户可以拥有多张银行卡,所以账户与银行卡又是一对多的关系:

    private	String account;//银行账号
    private double balance;//余额
    private ArrayList<Card> cardList;//一个银行账户可以拥有多张银行卡
    
  4. Card类,即银行卡需具有的属性就简单多了,只要有一个银行卡号的属性就行,银行卡号声明为String类型:

    private String ID;//银行卡号
    
  5. ATM类也很简单,只需有ATM机编号即可。

    private int num;//ATM机编号
    

Initialization(用户初始化数据)类最大圈复杂度为1,但是代码行数很多……是我写的太冗长了,后面看了老师给出的源码就觉得自己当初写的这源码真垃圾啊……。

Check(检验数据)类的圈复杂度最大,达到了16:

Check类共包含12个方法,圈复杂度最高的方法是checkvality(),checkvality方法中我对卡号、ATM机编号、卡密码、取款金额是否大于账户余额以及是否为跨行取款作了判断,这就需要写很多if分支语句来进行判断,其中也不免需要嵌套if语句。虽然我分别把对卡号、ATM机编号、卡密码、取款金额是否大于账户余额以及是否为跨行取款的判断都单独写成了方法checkCardNumber()、checkATMnumber()、checkPassword()、checkMoney()、isCrossBank(),但是在checkvality()方法中还是需要使用一连串的if语句来进行判断。

在判断是否为跨行取款时,需要根据银行卡号找出该银行卡属于哪家银行,之后再遍历找出的卡所在银行中的ATM机编号,如果能找到取款时输入的ATM机编号,则不是跨行取款,反之,为跨行取款。

根据卡号找银行时,就涉及到“从下往上找”:

/*
 * 通过银行卡号判断出该银行卡属于哪家银行
 * 返回银行的名称
 */
	private String cardBelong(String card) {
		Iterator<Bank> bankItr = this.initial.getUnionPay().getList().iterator();
		String str = "cardWrong";
		while(bankItr.hasNext()) {
			Bank bank = bankItr.next();
			Iterator<Account> accountItr = bank.getAcountList().iterator();
			String bankName = bank.getName();
			while(accountItr.hasNext()) {
				Iterator<Card> cardItr = accountItr.next().getCardList().iterator();
				while(cardItr.hasNext()) {
					if(cardItr.next().getID().equals(card)){
						str = bankName;
						return str;
					}
					else {
						str = card;
					}
				}
			}
		}
		return str;
	}

也就是先遍历银行,然后遍历账户,再遍历银行卡,当在银行卡里找到了输入的银行卡号时,返回此时相应的银行名称。

题目集09

放上根据代码生成的类图:

题目集9就是在题目集8上做了扩展,增加了信用卡功能与跨行取款功能,贷记账户需添加透支最大额度与透支手续费的属性,跨行需添加跨行手续费的属性。

由于在开始写这次题目集前,老师已经给出了题目集8的参考源码,所以我这次题目的完成是在给出的参考源码上重构的。其实阅读完参考源码,我还有点小确幸,因为参考源码的设计思路和我自己想的差不多,所以我在读懂参考源码过程中完全没有困难。阅读完后也发现了自己题目集8写的源码中能进一步改进的地方,果然,在自己独立思考完成源码后再来看看参考源码就是一种乐事。

好了,开始分析这题。

首先,需求增加了信用卡功能。

需要注意的是,增加信用卡功能并不意味着是在Card类上进行扩展,而是应在Account类上扩展,正是因为账户分为借记账户与贷记账户,所以才有借记卡与信用卡的说法产生。因此,需要创建一个名叫CreditAccount(贷记账户)的类作为Account的子类,我觉得没有必要再额外创建一个借记账户的类作为Account的子类,我认为贷记账户就是借记账户的延申,并且目前账户只分贷记账户和借记账户两类,日后应该也不会有什么新的类型账户产生,所以完全可以让借记账户作为父类Account,再使CreditAccount继承自Account。

CreditAccount继承Account后,也就继承了Account中包含的属性,但是需求中明显需要我们再另外添加属性:

private double overdraft = 0.0;//透支额度
private double rate = 0.0;//透支取款手续费

然后是跨行取款功能。

跨行取款就需要设置跨行手续费,跨行取款是在ATM机上操作的,但跨行取款的手续费是银行设定的,手续费也是由ATM机所隶属的银行收取的,所以跨行手续费这一属性应该给Bank类,即Bank类包含的属性变为了:

private String bankNO;
private String bankName;
private ArrayList<Account> accountList = new ArrayList<Account>();
private ArrayList<ATM> ATMList = new ArrayList<ATM>();
private double serviceFee = 0.0;//手续费

下面来看看用sourcemonitor生成的分析结果:

Account、ATM、Bank、Card、CreditAccount、UnionPay、User这几个实体类和题目集8设计的差不多,无非是个别类增加了几个需要用到的属性,整体复杂度也并不高。Withdraw类复杂度最高,Withdraw类主要是用来进行一些银行业务,例如取款、存款等,另外Withdraw类还包含了检验操作合法性的方法以及打印最终结果的方法。Withdraw类的最大圈复杂度来自于检验操作合法性的方法:

Withdraw类中的用来检验操作合法性的方法为checkVality(),

需要检验的有:

  1. 校验该卡是否存在
  2. 校验ATM是否存在
  3. 校验卡密码是否正确
  4. 校验取款金额是否大于余额或者透支金额超过额度
  5. 校验是否为跨行取款

尽管在检验这些东西的时候是通过调用方法来判定,但是就是不可避免得需要使用if分支语句。不过在我题目集8中的检验合法性方法的对比下,圈复杂度由16降到了11,这点还是让我很欣慰了。

另外,我觉得参考源码中有段代码让我顿悟了:

	public void withdraw(Account account,double balance) {
		
		account.setBalance(balance - amount);//取款更新余额操作

		if(amount >= 0) {
			showResult(account,1);
		}else {
			showResult(account,0);
		}
		
	}
	
	public void showResult(Account account,int flag) {
		String type = "";
		if(flag == 1) {
			type = "取款";			
		}else {
			type = "存款";
			amount *= -1;
		}
		String userName = account.getUser().getName();
		String bankName = account.getBank().getBankName();	
		System.out.println("业务:" + type + " " + userName + "在" +
				ValidateData.getATMbyATMID(unionPay, ATMID).getBank().getBankName() + "的" + ATMID + "号ATM机上" + type + String.format("¥%.2f", amount));
		System.out.println("当前余额为" + String.format("¥%.2f", account.getBalance()));
	}

就是在输出结果的处理上让我悟了。因为最后输出时,可能是输出显示取款或者存款,而判断是输出取款还是存款是根据输入钱数的正负来判断,所以我在题目集8中的处理就是直接对输入钱数的正负写了if语句,如果amount<0则为存款,然后就输出题目要求输出的信息,否则为取款,再输出题目要求输出的信息,这两种输出信息唯一的差别就是取款/存款:

if(money2 <= 0) {//存款
	System.out.printf(userName + "在" + bankName + "的 " + ATM + " ATM 机上" + "存款%.2f\n",Math.abs(money2));	System.out.printf("当前余额为¥%.2f\n", balance);
	return;
}
else {
	System.out.printf(userName + "在" + bankName + "的 " + ATM + " ATM 机上" + "取款¥%.2f\n",money2);	
	System.out.printf("当前余额为¥%.2f\n", balance);
	return;
}

这样就需要写四行输出语句,显得很累赘。

再看看参考代码中又是怎么处理的:

System.out.println("业务:" + type + " " + userName + "在" +ValidateData.getATMbyATMID(unionPay, ATMID).getBank().getBankName() + "的" + ATMID + "号ATM机上" + type + String.format("¥%.2f", amount));
System.out.println("当前余额为" + String.format("¥%.2f", account.getBalance()));

只有两行输出语句。其中type会根据判断赋值为“取款”或者“存款”。参考源码真是打开人思维啊。

踩坑心得:

1.在初始化数据这里有点卡住,写完后觉得自己代码写得太冗长了,很不满意。

看我这个类中声明的属性就能感觉到后面的代码也是多么的繁杂。还是向参考源码学习吧。

2.在检验卡是否存在时,用上了迭代器,虽然之前作业中已经使用过迭代器,但是还没有对迭代器有很深的认识,这次用迭代器的时候就出现了代码抛出NoSuchElementException异常,经过查询,得知出现这个异常的原因之一是因为线程访问越界,Iterator类中的next()方法使用了多次, ArrayList的源码if (i >= size) throw new NoSuchElementException();,在使用迭代器的时候注意next()方法在同一循环中不能出现2次。

改进建议:

  • 在题目集7-2中关于对图形卡片进行分组的那四个方法,

其实这四个方法完全可以合成写为一个方法,可以考虑把类名Circle、Rectangle、Triangle和Trapezoid放进一个String数组,把该String数组作为分组方法的参数,遍历该数组,当cardList里的元素类名与String数组中的元素匹配上时,就把cardList中的该元素添加到相应的图形list,完成图形卡片分组。

  • 题目集8与题目集9中对输出信息的处理建议参考题目集9中的处理方式,可以减少if分支语句与代码行数,让代码更加简洁。

总结:

这个月的题目集是对能力的大考验,因为终于要求完全由自己设计类了,这就需要我们下功夫去思考整个布局,得有个大体的方向。

这就涉及到面向对象的设计原则了:

最常见的7个面向对象原则如表1所示:

设计原则名称 定 义 使用频率
单一职责原则
(Single Responsibility Principle,SRP)
一个对象应该只包含单一的职责,并且该职责被完整地封装在一个类中 ★★★★☆
开闭原则(Open-Closed Principle,OCP) 软件实体应当对扩展开放,对修改关闭 ★★★★★
里氏代换原则
(Liskov Substitution Principle,LSP)
所有引用基类的地方必须能透明地使用其子类的对象 ★★★★★
依赖倒转原则
(Dependence Inversion Principle,DIP)
高层模块不应该依赖低层模块,它们都应该依赖抽象。抽象不应该依赖于细节,细节应该依赖于抽象 ★★★★★
接口隔离原则
(Interface Segregation Principle,ISP)
客户端不应该依赖那些它不需要的接口 ★★☆☆☆
合成复用原则
(Composite Reuse Principle,CRP)
★★★★☆
迪米特法则
(Law of Demeter,LoD)
每一个软件单位对其他单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位 ★★★☆☆

另外,我觉得自己在java的一些常用接口方面还不是很熟悉,接下来也需要在这方面加强一下。

posted @ 2021-06-08 21:01  N3D2Y  阅读(88)  评论(0)    收藏  举报