20135234mqy 实验二 Java面向对象程序设计
实 验 报 告
课程:Java程序设计 班级:1352 姓名:mqy 学号:20135234
成绩: 指导教师:娄嘉鹏 实验日期:2015.5.7
实验密级: 预习程度: 实验时间:15:50--22:50
仪器组次:34 必修/选修: 选修 实验序号:02
实验名称:实验二 Java面向对象程序设计
实验内容
1. 初步掌握单元测试和TDD
2. 理解并掌握面向对象三要素:封装、继承、多态
3. 初步掌握UML建模
4. 熟悉S.O.L.I.D原则
5. 了解设计模式
实验要求
1.没有Linux基础的同学建议先学习《Linux基础入门(新版)》《Vim编辑器》 课程
2.完成实验、撰写实验报告,实验报告以博客方式发表在博客园,注意实验报告重点是运行结果,遇到的问题(工具查找,安装,使用,程序的编辑,调试,运行等)、解决办法(空洞的方法如“查网络”、“问同学”、“看书”等一律得0分)以及分析(从中可以得到什么启示,有什么收获,教训等)。报告可以参考范飞龙老师的指导
3. 严禁抄袭,有该行为者实验成绩归零,并附加其他惩罚措施。
4. 请大家先在实验楼中的~/Code目录中用自己的学号建立一个目录,代码和UML图要放到这个目录中,截图中没有学号的会要求重做,然后跟着下面的步骤练习。
实验步骤
(一)单元测试
百分制转五分制:
如果成绩小于60,转成“不及格”
如果成绩在60与70之间,转成“及格”
如果成绩在70与80之间,转成“中等”
如果成绩在80与90之间,转成“良好”
如果成绩在90与100之间,转成“优秀”
其他,转成“错误”
具体代码:
public class MyUtil{
public static String percentage2fivegrade(int grade){
if (grade < 60)
return "bujige";
else if (grade < 70)
return "jige";
else if (grade < 80)
return "zhongdeng";
else if (grade < 90)
return "lianghao";
else if (grade < 100)
return "youxiu";
else
return "cuow";
}
}
选用Java,翻译好的MyUtil.java
如下:
写一个MyUtilTest.java
的测试模块,代码如下:
public class MyUtilTest {
public static void main(String[] args) {
if(MyUtil.percentage2fivegrade(50) != "bujige")
System.out.println("test failed!");
else
System.out.println("test passed!");
}
}
只有一组输入的测试是不充分的,我们把一般情况都测试一下,代码如下:
在Eclipse中运行结果如下,测试结果符合预期:
不能只测试正常情况,下面看看异常情况如何,比如输入为负分或大于100的成绩,代码如下:
运行程序发现负分时与期望不一致,找到了一个bug,原因是判断不及格时没有要求成绩大于零。修改MyUtil.java
,增加对负分的判断,代码如下:
public class MyUtil{
public static String percentage2fivegrade(int grade){
if ((grade < 0))//对错误判断
return "cuowu";
else if (grade < 60)
return "bujige";
else if (grade < 70)
return "jige";
else if (grade < 80)
return "zhongdeng";
else if (grade < 90)
return "lianghao";
else if (grade < 100)
return "youxiu";
else//对错误判断
return "cuowu";
}
}
一般代码在边界处最容易出错,我们还没有测试边界情况,我们对输入为“0,60,70,80,90,100”这些边界情况进行测试的代码如下:
public class MyUtilTest {
public static void main(String[] args) {
if(MyUtil.percentage2fivegrade(0) != "bujige")
System.out.println("test failed 1!");
else if(MyUtil.percentage2fivegrade(60) != "jige")
System.out.println("test failed 2!");
else if(MyUtil.percentage2fivegrade(70) != "zhongdeng")
System.out.println("test failed 3!");
else if(MyUtil.percentage2fivegrade(80) != "lianghao")
System.out.println("test failed 4!");
else if(MyUtil.percentage2fivegrade(90) != "youxiu")
System.out.println("test failed 5!");
else if(MyUtil.percentage2fivegrade(100) != "youxiu")
System.out.println("test failed 6!");
else
System.out.println("test passed!");
}
}
测试结果如下:
发现边界情况中输入100时有一个Bug。修改MyUtil.java
,把判断优秀的条件中包含输入为100的情况,代码如下:
测试结果都符合预期
(2) TDD(Test Driven Devlopment, 测试驱动开发)
先写测试代码
,然后再写产品代码
的开发方法叫“测试驱动开发”(TDD)。TDD的一般步骤如下:
- 明确当前要完成的功能,记录成一个测试列表
- 快速完成编写针对此功能的测试用例
- 测试代码编译不通过(没产品代码呢)
- 编写产品代码
- 测试通过
- 对代码进行重构,并保证测试通过(重构下次实验练习)
- 循环完成所有功能的开发
ava中有单元测试工具JUnit来辅助进行TDD,我们用TDD的方式把前面百分制转五分制的例子重写一次,体会一下有测试工具支持的开发的好处。
打开Eclipse
,单击File->New->Java Project
新建一个TDDDemo
的Java项目,
在TDDDemo
项目中,把鼠标放到项目名TDDDemo
上,单击右键,在弹出的菜单中选定New->Source Folder
新建一个测试目录test
把鼠标放到test
目录上,单击右键,在弹出的菜单中选定New->JUnit Test Case
新建一个测试用例类MyUtilTest
,
增加第一个测试用例testNormal
,注意测试用例前一定要有注解@Test
,测试用例方法名任意,输入以下代码:
import org.junit.Test;
import junit.framework.TestCase;
public class MyUtilTest extends TestCase {
@Test
public void testNormal() {
assertEquals("bujige", MyUtil.percentage2fivegrade(55));
assertEquals("jige", MyUtil.percentage2fivegrade(65));
assertEquals("zhongdeng", MyUtil.percentage2fivegrade(75));
assertEquals("lianghao", MyUtil.percentage2fivegrade(85));
assertEquals("youxiu", MyUtil.percentage2fivegrade(95));
}
}
出现红叉说明代码存在语法错误,原因很简单,MyUtil
类还不存在,类中的percentage2fivegrade方法也不存在,我们在TDDDemo
的src
目录中新建一个MyUtil
的类,并实现percentage2fivegrade方法,如下图所示
测试代码没有语法错误了,我们把鼠标放到MyUtilTest.java
上,单击右键,选择Run as->JUnit Test
,如下图:
测试结果出现了一个红条(red bar),说明测试没通过,红条上面汇总了测试情况,运行了一个测试,没有错误,一个测试没通过。下面原因说的也很清楚:测试代码第十行传入55时,期望结果是“不及格”,代码返回了“错误”,修改MyUtil.Java
吧,输入以下代码:
将grad改为grade
public class MyUtil{
public static String percentage2fivegrade(int grade){
if ((grade < 0))
return "cuowu";
else if (grade < 60)
return "bujige";
else if (grade < 70)
return "jige";
else if (grade < 80)
return "zhongdeng";
else if (grade < 90)
return "lianghao";
else if (grade < 100)
return "youxiu";
else
return "cuowu";
}
}
再运行测试,如下图所示:
测试结果出现了一个绿条(green bar),说明测试通过了。
TDD的编码节奏是:
- 增加测试代码,JUnit出现红条
- 修改产品代码
- JUnit出现绿条,任务完成
增加一个测试异常情况的用例testException和testBoundary
,如下图:
(二)面向对象三要素
(1)抽象
抽象包括两个方面,一是过程抽象,二是数据抽象。
(2)封装、继承与多态
Dog类:
测试代码与运行结果如下:
我们可以用UML中的类图来描述类Dog
,首先我们在实验楼的环境中打开shell,在命令行中输入umbrello
,打开UML建模软件umbrello
先单击工具栏上的类图标,再在class diagram(类图)
中单击一下,会弹出一个圣诞框,输入类名Dog
把鼠标放到Dog
类上,单击右键,选择Properties
,在弹出的对话框中的Display
中去掉Public Only
选项
把鼠标放到Dog
类上,单击右键,选择New->Attribute
,在弹出的对话框中的填好Type
,Name
,并选好Visibility
,
把鼠标放到Dog
类上,单击右键,选择New->Operation
,在弹出的对话框中的填好Type
,Name
,并选好Visibility
,
在UML 里,一个类的属性能显示它的名字,类型,初始化值,属性也可以显示private,public,protected。 类的方法能显示它们的方法名,参数,返回类型,以及方法的private,public,protected属性。其中:
- +表示public
- #表示 protected
- -表示 private
同样,我们可以建立一个Cat
类
这时的测试类如以下UML图所示:
注意:UML类图要展示类之间的静态关系,AnimalTest
类依赖Dog
类和Cat
类,UML中依赖用带箭头的直线表示。
对应的测试代码和运行结果如下图所示:
看到Dog
类和Cat
类都有Color
属性和相应的setter和getter方法,明显违反了前面提到的DRY
原则,我们可以通过继承解决这个问题,把Color
属性和相应的setter和getter方法放到父类Animal
中,如以下UML较图所示:
UML类图中继承的表示法,是用一个带三角的直线指向父类,通过继承,消除了Dog
类和Cat
类中的重复代码,符合DRY
的要求。
继承指一个类的定义可以基于另外一个已经存在的类,即子类基于父类,从而实现父类代码的重用。既存类称作基类、超类、父类(base class、super class、parent class),新类称作派生类、继承类、子类(derived class、inherited class、child class)。继承关系表达了”Is a kind of“的关系,称为“ISA”关系。继承的关键在于确认子类为父类的一个特殊类型
。继承是实现软件可重用的根基,是提高软件系统的可扩展性与可维护性的主要途径。
如上面所示,以封装为基础,继承可以实现代码复用,需要注意的是,继承更重要的作用是实现多态。
面向对象中允许不同类的对象对同一消息做出响应,即同一消息可以根据发送对象的不同而采用多种不同的行为方式,我们称此现象为多态性。Java中,多态是指不同的类对象调用同一个签名的成员方法时将执行不同代码的现象。多态是面向对象程序设计的灵活性和可扩展性的基础。
可以进一步抽象,把Dog
类中的bark()
和Cat
类中的meow()
抽象成一个抽象方法shout()
,Dog
类和Cat
类中覆盖这个方法,如以下UML图所示:
UML类图中的Animal
类中的shout()
方法是抽象方法,是斜体的,Animal
类是抽象类,也是斜体的。
对应的代码如下:
public abstract class Animal {
private String color;
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
public abstract String shout();
}
public class Dog extends Animal{
public String shout(){
return "wang";
}
public String toString(){
return "The Dog's color is " + this.getColor() +", and it shouts "+ this.shout() + "!";
}
}
public class Cat extends Animal{
public String shout(){
return "miao";
}
public String toString(){
return "The Cat's color is " + this.getColor() +", and it shouts "+ this.shout() + "!";
}
}
测试代码和运行结果如下:
测试结果都符合预期
(三)设计模式初步
(1)S.O.L.I.D原则
面向对象三要素是“封装、继承、多态”,任何面向对象编程语言都会在语法上支持这三要素。如何借助抽象思维用好三要素特别是多态还是非常困难的,S.O.L.I.D
类设计原则是一个很好的指导:
- SRP(Single Responsibility Principle,单一职责原则)
- OCP(Open-Closed Principle,开放-封闭原则)
- LSP(Liskov Substitusion Principle,Liskov替换原则)
- ISP(Interface Segregation Principle,接口分离原则)
- DIP(Dependency Inversion Principle,依赖倒置原则)
(2)模式与设计模式
模式是某外在环境(Context) 下﹐对特定问题(Problem)的惯用解决之道(Solution)。模式必须使得问题明晰,阐明为什么用它来求解问题,以及在什么情况下有用,什么情况下不能起作用,每个模式因其重复性从而可被复用,本身有自己的名字,有可传授性,能移植到不同情景下。模式可以看作对一个问题可复用的专家级解决方法。
计算机科学中有很多模式:
- GRASP模式
- 分析模式
- 软件体系结构模式
- 设计模式:创建型,结构型,行为型
- 管理模式: The Manager Pool 实现模式
- 界面设计交互模式
- …
这里面最重要的是设计模式,在面向对象中设计模式的地位可以和面向过程编程中的数据结构的地位相当。
(3)设计模式实示例
(1)设计模式(design pattern)提供一个用于细化软件系统的子系统或组件,或它们之间的关系图,它描述通信组件的公共再现结构,通信组件可以解决特定语境中的一个设计问题。
设计模式背后是抽象和SOLID原则。
(2)设计模式有四个基本要素:
• Pattern name:描述模式,便于交流,存档
• Problem:描述何处应用该模式
• Solution:描述一个设计的组成元素,不针对特例
• Consequence:应用该模式的结果和权衡(trade-offs)
(3)了解设计模式可能会存在的过度设计问题以及如何避免它。
例:
文档系统:
客户如果要求系统支持Float
类,这是一个合理的要求,要支持Float
类,Document
类要修改两个地方,这违反了OCP
原则,使用多态可以解决部分问题:
要支持Float
类,Document
类要修改构造方法,这还违反了OCP
原则。封装、继承、多态解决不了问题了,这时需要设计模式了:
(四)练习
1使用TDD的方式设计关实现复数类Complex。
//伪代码
首先设计一个复数类complex,定义三个构造方法:
①没有参数时默认为实部和虚部都为0;
②一个参数时默认为实数,即虚部为0,
③两个参数时分别为实部和虚部
再定义两个成员方法计算两个复数的和与差.定义一个print()方法输出复数的值,当虚部为0时不输出虚部.
最后定义一个song类使用complex类,在这个类的主方法中创建两个复数对象,分别计算这两个复数的和与差并输出.
//产品代码
public class Complex {
private int sh,xu;
Complex(){
this.sh=0;
this.xu=0;
}
Complex(int sh){
this.sh=sh;
this.xu=0;
}
Complex(int sh,int xu){
this.sh=sh;
this.xu=xu;
}
public void addFu(Complex p1,Complex p2){
System.out.println("这两个复数的和为:");
this.sh=p1.sh+p2.sh;
this.xu=p1.xu+p2.xu;
print();
}
public void minusFu(Complex p1,Complex p2){
System.out.println("这两个复数的差为:");
this.sh=p1.sh-p2.sh;
this.xu=p1.xu-p2.xu;
print();
}
public void outputFu(){
System.out.println("复数的值为:");
print();
}
public void print(){
if(this.xu>0){
System.out.println(this.sh+"+"+this.xu+"i");
}else if(this.xu<0){
System.out.println(this.sh+""+this.xu+"i");
}else{
System.out.println(this.sh);
}
}
}
2.实验报告中统计自己的PSP(Personal Software Process)时间
步骤 |
耗时 |
百分比 |
需求分析 |
1h |
14.3% |
设计 |
2h |
28.6% |
代码实现 |
2h |
28.6% |
测试 |
1.5h |
21.4% |
分析总结 |
0.5h |
7.1% |
(五)实验中遇到的问题及解决方法
出现的问题:在单元测试的编写中,网络信号不稳定,经常出现断网、卡机等问题,只能等待网络恢复,或者重新开启虚拟机重新进行实验,
测试代码的编写过程中因为不太熟悉程序的编写,只能自己对照着产品代码来写的测试代码,所以测试代码出现了很多问题,花费了很多时间去修改。有的编写代码最终还是无法顺利运行。
(六)实验总结及体会
这次实验尽管花了很多时间,但是我也收获了很多。首先,通过这次实验,我对虚拟机的使用更加熟悉,也更加适应这种实验模式。单元测试也帮助我提升了自己的能力,一步一步地引导我学会处理可能出现的种种问题,同时也教会我以后在编写程序的时候要考虑到各种可能性,以提高代码的安全性。
本次实验让我了解到想用程序解决实际问题时,不仅是只要写出一个代码就可以了,而是最好写出三种代码:伪代码、产品代码、测试代码,这样的程序才有实际意义,方便使用。