20145226夏艺华 《Java程序设计》实验报告二
实验二 Java面向对象程序设计
实验内容
-
初步掌握单元测试和TDD
-
理解并掌握面向对象三要素:封装、继承、多态
-
初步掌握UML建模
-
熟悉S.O.L.I.D原则
-
了解设计模式
实验步骤
(一)单元测试
(1)三种代码
例子的需求:我们要在一个MyUtil类中解决一个百分制成绩转成“优、良、中、及格、不及格”五级制成绩的功能。
- 伪代码:与具体编程语言无关,是从意图层面来解决问题的,是产品代码最自然的、最好的注释。上述例子的伪代码如下:
百分制转五分制:
如果成绩小于60,转成“不及格”
如果成绩在60与70之间,转成“及格”
如果成绩在70与80之间,转成“中等”
如果成绩在80与90之间,转成“良好”
如果成绩在90与100之间,转成“优秀”
其他,转成“错误”
- 产品代码:用特定编程语言翻译。下面选定Java作为编程语言,产品代码如下:
public class MyUtil{
public static String percentage2FiveGrade(int grade){
//如果成绩小于60,转成“不及格”
if (grade < 60)
return "不及格";
//如果成绩在60与70之间,转成“及格”
else if (grade < 70)
return "及格";
//如果成绩在70与80之间,转成“中等”
else if (grade < 80)
return "中等";
//如果成绩在80与90之间,转成“良好”
else if (grade < 90)
return "良好";
//如果成绩在90与100之间,转成“优秀”
else if (grade < 100)
return "优秀";
//其他,转成“错误”
else
return "错误";
}
}
- 测试代码:
测试正常情况的测试代码如下:
public class MyUtilTest {
public static void main(String[] args) {
//测试正常情况
if(MyUtil.percentage2FiveGrade(58) != "不及格")
System.out.println("test failed!");
else if(MyUtil.percentage2FiveGrade(68) != "及格")
System.out.println("test failed!");
else if(MyUtil.percentage2FiveGrade(78) != "中等")
System.out.println("test failed!");
else if(MyUtil.percentage2FiveGrade(88) != "良好")
System.out.println("test failed!");
else if(MyUtil.percentage2FiveGrade(98) != "优秀")
System.out.println("test failed!");
else
System.out.println("test passe```````
!");
}
}
运行结果如下:
测试不正常情况,例如输入为负分或大于100的成绩,测试代码如下:
public class MyUtilTest1 {
public static void main(String[] args) {
//测试出错情况
if(MyUtil.percentage2FiveGrade(-18) != "错误")
System.out.println("test failed 1!");
else if(MyUtil.percentage2FiveGrade(118) != "错误")
System.out.println("test failed 2!");
else
System.out.println("test passed!");
}
}
运行结果如下:
程序运行后发现错误,所以要修改代码,增加了边界定义和异常的定义,即增加了小于零和大于一百的判断语句,代码修改后如下:
public class MyUtil {
public static String percentage2fivegrade(int grade){
//如果成绩小于0,转成“错误”
if ((grade < 0))
return "错误";
//如果成绩小于60,转成“不及格”
else if (grade < 60)
return "不及格";
//如果成绩在60与70之间,转成“及格”
else if (grade < 70)
return "及格";
//如果成绩在70与80之间,转成“中等”
else if (grade < 80)
return "中等";
//如果成绩在80与90之间,转成“良好”
else if (grade < 90)
return "良好";
//如果成绩在90与100之间,转成“优秀”
else if (grade < 100)
return "优秀";
//如果成绩大于100,转成“错误”
else
return "错误";
}
}
再次运行MyUtilTest1.java的运行结果如下所示:
测试边界情况,例如输入0,60,70,80,90,100这些边界成绩进行测试,测试代码如下:
public class MyUtilTest2 {
public static void main(String[] args) {
//测试边界情况
if(MyUtil.percentage2FiveGrade(0) != "不及格")
System.out.println("test failed 1!");
else if(MyUtil.percentage2FiveGrade(60) != "及格")
System.out.println("test failed 2!");
else if(MyUtil.percentage2FiveGrade(70) != "中等")
System.out.println("test failed 3!");
else if(MyUtil.percentage2FiveGrade(80) != "良好")
System.out.println("test failed 4!");
else if(MyUtil.percentage2FiveGrade(90) != "优秀")
System.out.println("test failed 5!");
else if(MyUtil.percentage2FiveGrade(100) != "优秀")
System.out.println("test failed 6!");
else
System.out.println("test passed!");
}
}
运行结果如下所示:
运行结果显示当输入边界成绩100时出现了一个Bug。所以修改代码,将成绩100添加到优秀的判断结果中,修改后的代码为:
public class MyUtil {
public static String percentage2FiveGrade(int grade){
//如果成绩小于0,转成“错误”
if ((grade < 0))
return "错误";
//如果成绩小于60,转成“不及格”
else if (grade < 60)
return "不及格";
//如果成绩在60与70之间,转成“及格”
else if (grade < 70)
return "及格";
//如果成绩在70与80之间,转成“中等”
else if (grade < 80)
return "中等";
//如果成绩在80与90之间,转成“良好”
else if (grade < 90)
return "良好";
//如果成绩在90与100之间,转成“优秀”
else if (grade <= 100)
return "优秀";
//如果成绩大于100,转成“错误”
else
return "t"
}
}
再次运行MyUtilTest2.java的运行结果如下所示:
测试结果显示代码符合预期要求,测试通过。
(2)TDD(Test Driven Devlopment, 测试驱动开发)
上述的例子是先写产品代码,再写测试代码,从测试过程中发现一些Bug,然后完善代码。而TDD则是先写测试代码,然后再写产品代码的开发方法。
TDD的一般步骤如下:
- 明确当前要完成的功能,记录成一个测试列表
- 测试代码编译不通过(没产品代码呢)
- 测试通过
- 对代码进行重构,并保证测试通过(重构下次实验练习)
- 循环完成所有功能的开发
Java中有单元测试工具JUnit来辅助进行TDD,下面用TDD的方式把前面百分制转五分制的例子重写一次,体会一下有测试工具支持的开发的好处。
1.打开Eclipse,单击File->New->Java Project新建一个TDDDemo的Java项目。
2.在TDDDemo项目中,把鼠标放到项目名TDDDemo上,单击右键,在弹出的菜单中选定New->Source Folder新建一个测试目录test。如下图所示:
3.我们把鼠标放到test目录上,单击右键,在弹出的菜单中选定New->JUnit Test Case新建一个测试用例类MyUtilTest。
4.增加第一个测试用例testNormal,注意测试用例前一定要有注解@Test,测试用例方法名任意,并将上面的修改完成的代码MyUtil.java拷贝到src文件夹中,输入以下代码:
public class MyUtilTest extends TestCase {
@Test
public void testNormal() {
assertEquals("不及格", MyUtil.percentage2FiveGrade(58));
assertEquals("及格", MyUtil.percentage2FiveGrade(68));
assertEquals("中等", MyUtil.percentage2FiveGrade(78));
assertEquals("良好", MyUtil.percentage2FiveGrade(88));
assertEquals("优秀", MyUtil.percentage2FiveGrade(98));
}
}
5.把鼠标放到MyUtilTest.java上,单击右键,选择Run as->JUnit Test,运行结果如下:
(二)面向对象三要素
(1)抽象
抽象是抽出事物的本质特征而暂时不考虑他们的细节。程序设计中,抽象包括两个方面,一是过程抽象,二是数据抽象。"去粗取精、化繁为简、由表及里、异中求同"。
(2)封装、继承与多态
面向对象(Object-Oriented)的三要素包括:封装、继承、多态。面向对象的思想涉及到软件开发的各个方面,如面向对象分析(OOA)、面向对象设计(OOD)、面向对象编程实现(OOP)。OOA根据抽象关键的问题域来分解系统,关注是什么(what)。OOD是一种提供符号设计系统的面向对象的实现过程,用非常接近问题域术语的方法把系统构造成“现实世界”的对象,关注怎么做(how),通过模型来实现功能规范。OOP则在设计的基础上用编程语言(如Java)编码。贯穿OOA、OOD和OOP的主线正是抽象。
- 封装
封装就是将数据与相关行为包装在一起以实现信息就隐藏。在Java中用类进行封装,比如一个Dog类:
public class Dog {
private String color;
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
public String bark(){
return "汪汪";
}
public String toString(){
return "The Dog's color is " + this.getColor() +", and it shouts "+ this.bark() + "!";`
`
}
}
封装实际上使用方法将类的数据隐藏起来,控制用户对类的修改和访问数据的程度,从而带来模块化和信息隐藏的好处。例如Dog类通过使用类和访问控制隐藏了属性color,开放了接口setColor(),getColor(),bark()和toString。可以使用下面的代码来使用Dog类:
public class DogTest {
public static void main(String[] args){
Dog g = new Dog();
g.setColor("Blue");
getInfo(g);
}
public static void getInfo(Dog d){
System.out.println(d.toString());
}
}
代码的运行结果如下图所示:
下面来使用StarUML工具来进行UML建模,首先创建一个Dog类,添加其对应的属性和方法,然后依创建一个Cat类,同时创建一个AnimalTest类来使用Dog类和Cat类。具体的UML图如下所示:
但是根据观察,Dog类和Cat类都有Color属性和相应的setter和getter方法,这明显违反了前面提到的DRY原则,可以通过继承解决这个问题,把Color属性和相应的setter和getter方法放到父类Animal中,修改后的UML如图所示:
继承指一个类的定义可以基于另外一个已经存在的类,即子类基于父类,从而实现父类代码的重用。继承是实现软件可重用的根基,是提高软件系统的可扩展性与可维护性的主要途径。
面向对象中允许不同类的对象对同一消息做出响应,即同一消息可以根据发送对象的不同而采用多种不同的行为方式,我们称此现象为多态性。Java中,多态是指不同的类对象调用同一个签名的成员方法时将执行不同代码的现象。多态是面向对象程序设计的灵活性和可扩展性的基础。
所以针对上面获得的类图,可以进一步抽象,把Dog类中的bark()和Cat类中的meow()抽象成一个抽象方法shout(),Dog类和Cat类中覆盖这个方法,如以下UML图所示:
(三)设计模式
(1)S.O.L.I.D原则
-
SRP(Single Responsibility Principle,单一职责原则):决不要有一个以上的理由修改一个类。对象的改变仅仅依赖于单一职责的改变,基于软件设计中的高内聚性定义。
-
OCP(Open-Closed Principle,开放-封闭原则) :软件实体(类,模块,函数等)应该对扩充开放,对修改封闭。OCP可以用抽象和继承、面向接口编程手段实现。
-
LSP(Liskov Substitusion Principle,Liskov替换原则):子类必须可以被其基类所代
。使用指向基类的指针或引用的函数,必须能够在不知道具体派生类对象类型的情况下使用它。核心思想是父类型对象可以被子类型对象所取代。 -
ISP(Interface Segregation Principle,接口分离原则):客户不应该依赖他们并未使用的接口。
-
DIP(Dependency Inversionsion Principle,依赖倒置原则):高层模块不应该依赖于低层模块,二者都应该依赖于抽象
。抽象不应该依赖于细节,细节应该依赖于抽象。在应用中通过依赖注入的方式实现解耦,重用低级模块,重用实现,解除依赖。
(2)模式与设计模式
模式是某外在环境下﹐对特定问题的惯用解决之道。模式必须使得问题明晰,阐明为什么用它来求解问题,以及在什么情况下有用,什么情况下不能起作用,每个模式因其重复性从而可被复用。模式中最重要的是设计模式。
(3)设计模式实示例
设计模式(design pattern)提供一个用于细化软件系统的子系统或组件,或它们之间的关系图,它描述通信组件的公共再现结构,通信组件可以解决特定语境中的一个设计问题。
设计模式有四个基本要素:
Pattern name:描述模式,便于交流,存档
Problem:描述何处应用该模式
Solution:描述一个设计的组成元素,不针对特例对特例
Consequence:应用该模式的结果权衡
同时也要了解设计模式可能会存在的过度设计问题以及如何避免它。
(四)练习:使用TDD的方式设计关实现复数类Complex。
(1)伪代码:
复数类Complex
复数 = 实部 + 虚部 i
属性:实部-shibu,虚部-xubu。
方法:
getShiBu(int shibu); 获取实部
getXuBu(int xubu);返回虚部
add(Complex c);实现复数相加
minus(Complex c);实现复数相减
print(Complex c);实现复数打印
复数相加 = (实部 + 实部)+(虚部 + 虚部)i
复数相减 = (实部 - 实部)+(虚部 - 虚部)i复数打印:
虚部 > 0:"实部" + "+" + "虚部" + "i"
虚部 < 0:"实部" + "虚部" + "i"
虚部为0:"实部"
(2)测试代码:
public class ComplexTest extends TestCase{
Complex a=new Complex();
Complex b=new Complex(1);
Complex c=new Complex(19,4);
Complex d=new Complex(5,-3);
@Test
public void getXuBu() throws Exception {
assertEquals(0.0, a.getXuBu());
assertEquals(0.0, b.getXuBu());
assertEquals(4.0, c.getXuBu());
assertEquals(-3.0, d.getXuBu());
}
@Test
public void getShiBu() throws Exception {
assertEquals(0.0, a.getShiBu());
assertEquals(1.0, a.getShiBu());
assertEquals(19.0, a.getShiBu());
assertEquals(5.0, a.getShiBu());
}
@Test
public void add() throws Exception {
assertEquals("(1.0)", a.add(b).print());
assertEquals("(19.0+4.0i)", a.add(c).print());
assertEquals("(5.0-3.0i)", a.add(d).print());
}
@Test
public void minus() throws Exception {
assertEquals("(-1.0)", a.minus(b).print());
assertEquals("(-19.0-4.0i)", a.minus(c).print());
assertEquals("(-5.0+3.0i)", a.minus(d).print());
}
}
测试结果如下图所示:
(3)产品代码:
public class Complex {
private double shibu;
private double xubu;
public Complex() {
this.shibu = 0.0;
this.xubu = 0.0;
}
public Complex(double m) {
this.shibu = m;
this.xubu = 0.0;
}
public Complex(double m, double n) {
this.shibu = m;
this.xubu = n;
}
public double getShiBu(){
return shibu;
}
public double getXuBu(){
return xubu;
}
public Complex add(Complex c2) {
return new Complex(shibu + c2.shibu, xubu + c2.xubu);
}
public Complex minus(Complex c2) {
return new Complex(shibu - c2.shibu, xubu - c2.xubu);
}
public String print(){
if(this.xubu>0){
return "("+this.shibu+"+"+this.xubu+"i)";
}
else if(this.xubu<0){
return "("+this.shibu+this.xubu+"i)";
}
else{
return "("+this.shibu+")";
}
}
}
本次实验的 PSP(Personal Software Process) 时间:
步骤 | 耗时 | 百分比 |
---|---|---|
需求分析 | 20min | 14.2% |
设 计 | 20min | 14.2% |
代码实现 | 40min | 28.5% |
测 试 | 40min | 28.5% |
分析总结 | 21min | 14.6% |