实验二 Java面向对象程序设计
课程:Java实验 班级:201352 姓名:程涵 学号:20135210
成绩: 指导教师:娄佳鹏 实验日期:15.05.05
实验密级: 预习程度: 实验时间:
仪器组次: 必修/选修:选修 实验序号:2
实验名称: Java面向对象程序设计
实验目的与要求:
1. 初步掌握单元测试和TDD
2. 理解并掌握面向对象三要素:封装、继承、多态
3. 初步掌握UML建模
4. 熟悉S.O.L.I.D原则
5. 了解设计模式
实验要求
1.没有Linux基础的同学建议先学习《Linux基础入门(新版)》《Vim编辑器》 课程
2.完成实验、撰写实验报告,实验报告以博客方式发表在博客园,注意实验报告重点是运行结果,遇到的问题(工具查找,安装,使用,程序的编辑,调试,运行等)、解决办法(空洞的方法如“查网络”、“问同学”、“看书”等一律得0分)以及分析(从中可以得到什么启示,有什么收获,教训等)。报告可以参考范飞龙老师的指导
3. 严禁抄袭,有该行为者实验成绩归零,并附加其他惩罚措施。
实验仪器:
名称 |
型号 |
数量 |
PC |
|
1 |
|
|
|
实验内容、步骤与体会(附纸):
(一)单元测试
(1) 三种代码
编程是智力活动,不是打字,编程前要把干什么、如何干想清楚才能把程序写对、写好。与目前不少同学一说编程就打开编辑器写代码不同,我希望同学们养成一个习惯,当你们想用程序解决问题时,要会写三种码:
- 伪代码
- 产品代码
- 测试代码
我们通过一个例子说明如何写这三种代码。
需求:我们要在一个
MyUtil
类中解决一个百分制成绩转成“优、良、中、及格、不及格”五级制成绩的功能。
我们设计了一个测试用例(Test Case)
,测试用例
是为某个特殊目标而编制的一组测试输入、执行条件以及预期结果,以便测试某个程序路径或核实是否满足某个特定需求。这里我们的测试输入是“50”,预期结果是“不及格”。在Eclipse中运行结果如下,测试结果符合预期:
只有一组输入的测试是不充分的,我们把一般情况都测试一下,
在Eclipse中运行结果如下,测试结果符合预期:
我们不能只测试正常情况,下面看看异常情况如何,比如输入为负分或大于100的成绩,
运行程序发现负分时与期望不一致,终于找到了一个bug,原因是判断不及格时没有要求成绩大于零。我们修改MyUtil.java
,增加对负分的判断再次运行测试,测试结果符合预期,如下图所示:
测试够了吗?还不够,一般代码在边界处最容易出错,我们还没有测试边界情况,我们对输入为“0,60,70,80,90,100”这些边界情况进行测试,
测试结果如下:
我们发现边界情况中输入100时有一个Bug。我们修改MyUtil.java
,把判断优秀的条件中包含输入为100的情况,
这时测试都符合预期了,我们把MyUtil.java
提供给别人使用时,心里比较有底气了。那如何保证单元测度是充分的呢?我们的一般要求是测试代码
要比产品代码
多。如何写测试,《单元测试之道》提出了Right-BICEP
的方法,大家可以参考一下。
软件是由多人合作完成的,不同人员的工作相互有依赖关系。软件的很多错误都来源于程序员对模块功能的误解、疏忽或不了解模块的变化。如何能让自己负责的模块功能定义尽量明确,模块内部的改变不会影响其他模块,而且模块的质量能得到稳定的、量化的保证?单元测试就是一个很有效的解决方案
(2) TDD(Test Driven Devlopment, 测试驱动开发)
先写测试代码
,然后再写产品代码
的开发方法叫“测试驱动开发”(TDD)。
TDD的一般步骤如下:
- 明确当前要完成的功能,记录成一个测试列表
- 快速完成编写针对此功能的测试用例
- 测试代码编译不通过(没产品代码呢)
- 编写产品代码
- 测试通过
- 对代码进行重构,并保证测试通过(重构下次实验练习)
- 循环完成所有功能的开发
基于TDD,我们不会出现过度设计的情况,需求通过测试用例表达出来了,我们的产品代码
只要让测试通过就可以了。
Java中有单元测试工具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("不及格", MyUtil.percentage2fivegrade(55)); assertEquals("及格", MyUtil.percentage2fivegrade(65)); assertEquals("中等", MyUtil.percentage2fivegrade(75)); assertEquals("良好", MyUtil.percentage2fivegrade(85)); assertEquals("优秀", MyUtil.percentage2fivegrade(95)); } }
输入完毕,Eclipse
中如下图所示:
图中的红叉说明代码存在语法错误,原因很简单,MyUtil
类还不存在,类中的percentage2fivegrade方法也不存在,我们在TDDDemo
的src
目录中新建一个MyUtil
的类,并实现percentage2fivegrade方法,如下图所示:
大家可以看到现在测试代码没有语法错误了,我们把鼠标放到MyUtilTest.java
上,单击右键,选择Run as->JUnit Test
,如下图:
测试结果出现了一个红条(red bar),说明测试没通过,红条上面汇总了测试情况,运行了一个测试,没有错误,一个测试没通过。下面原因说的也很清楚:测试代码第十行传入55时,期望结果是“不及格”,代码返回了“错误”,修改MyUtil.Java
吧,输入以下代码:
再运行测试,如下图所示:
测试结果出现了一个绿条(green bar),说明测试通过了。
TDD的目标是"Clean Code That Works",TDD的slogan是"Keep the bar green, to Keep the code clean",大家体会一下。
TDD的编码节奏是:
- 增加测试代码,JUnit出现红条
- 修改产品代码
- JUnit出现绿条,任务完成
我们增加一个测试异常情况的用例testException,
我们增加一个测试边界情况的用例testBoundary
,
如何让JUnit的gree bar
出来,动手实验一下,如下图:
不管用不用TDD,写出高质量的测试用例才是最重要的,如何进行单元测试,大家可参考一下《单元测试之道》这本书。另外,《Agile Java 中文版》展示了如何将Java和TDD进行有效的整合,通过TDD驱动项目开发,有兴趣的可以参考。
(2)封装、继承与多态
面向对象(Object-Oriented)的三要素包括:封装、继承、多态。面向对象的思想涉及到软件开发的各个方面,如面向对象分析(OOA)、面向对象设计(OOD)、面向对象编程实现(OOP)。OOA根据抽象关键的问题域来分解系统,关注是什么(what)。OOD是一种提供符号设计系统的面向对象的实现过程,用非常接近问题域术语的方法把系统构造成“现实世界”的对象,关注怎么做(how),通过模型来实现功能规范。OOP则在设计的基础上用编程语言(如Java)编码。贯穿OOA、OOD和OOP的主线正是抽象。
OOD中建模会用图形化的建模语言UML(Unified Modeling Language),UML是一种通用的建模语言,我们实验中使用umbrello进行建模,Windows中推荐大家使用 StarUML。
过程抽象的结果是函数,数据抽象的结果是抽象数据类型(Abstract Data Type,ADT),类可以作具有继承和多态机制的ADT。数据抽象才是OOP的核心和起源。
封装实际上使用方法(method)将类的数据隐藏起来,控制用户对类的修改和访问数据的程度,从而带来模块化(Modularity)和信息隐藏(Information hiding)的好处;接口(interface)是封装的准确描述手段。
- +表示public
- #表示 protected
- -表示 private
继承指一个类的定义可以基于另外一个已经存在的类,即子类基于父类,从而实现父类代码的重用。既存类称作基类、超类、父类(base class、super class、parent class),新类称作派生类、继承类、子类(derived class、inherited class、child class)。继承关系表达了”Is a kind of“的关系,称为“ISA”关系。继承的关键在于确认子类为父类的一个特殊类型
。继承是实现软件可重用的根基,是提高软件系统的可扩展性与可维护性的主要途径。
如上面所示,以封装为基础,继承可以实现代码复用,需要注意的是,继承更重要的作用是实现多态。
面向对象中允许不同类的对象对同一消息做出响应,即同一消息可以根据发送对象的不同而采用多种不同的行为方式,我们称此现象为多态性。Java中,多态是指不同的类对象调用同一个签名的成员方法时将执行不同代码的现象。多态是面向对象程序设计的灵活性和可扩展性的基础。
(三)设计模式初步
(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,依赖倒置原则)
OCP
是OOD中最重要的一个原则,OCP
的内容是:
- software entities (class, modules, function, etc.) should open for extension,but closed for modification.
- 软件实体(类,模块,函数等)应该对扩充开放,对修改封闭。
对扩充开放(Open For Extension )要求软件模块的行为必须是可以扩充的,在应用需求改变或需要满足新的应用需求时,我们要让模块以不同的方式工作; 对修改封闭(Closed for Modification )要求模块的源代码是不可改动的,任何人都不许修改已有模块的源代码。 基于OCP
,利用面向对象中的多态性(Polymorphic),更灵活地处理变更拥抱变化,OCP
可以用以下手段实现:(1)抽象和继承,(2)面向接口编程。
-
(2)模式与设计模式
模式是某外在环境(Context) 下﹐对特定问题(Problem)的惯用解决之道(Solution)。模式必须使得问题明晰,阐明为什么用它来求解问题,以及在什么情况下有用,什么情况下不能起作用,每个模式因其重复性从而可被复用,本身有自己的名字,有可传授性,能移植到不同情景下。模式可以看作对一个问题可复用的专家级解决方法。 计算机科学中有很多模式:
- GRASP模式
- 分析模式
- 软件体系结构模式
- 设计模式:创建型,结构型,行为型
- 管理模式: The Manager Pool 实现模式
- 界面设计交互模式
- …
这里面最重要的是设计模式,在面向对象中设计模式的地位可以和面向过程编程中的数据结构的地位相当。
-
(3)设计模式实示例
设计模式(design pattern)提供一个用于细化软件系统的子系统或组件,或它们之间的关系图,它描述通信组件的公共再现结构,通信组件可以解决特定语境中的一个设计问题。
我们看到通过增加了一层抽象层使代码符合了OCP原则。代码有良好的可扩充性、可维护性,代价是代码多了,效率变低下了。
设计模式初学者容易过度使用它们,导致过度设计,也就是说,遵守DRY和OCP当然好,但会出现YAGNI(You aren't gonna need it, 你不会需要它)问题。
DRY原则和YAGNI原则并非完全兼容。前者追求"抽象化",要求找到通用的解决方法;后者追求"快和省",意味着不要把精力放在抽象化上面,因为很可能"你不会需要它"。怎么平衡呢?有一个Rule of three (三次原则):第一次用到某个功能时,你写一个特定的解决方法;第二次又用到的时候,你拷贝上一次的代码(违反了DRY);第三次出现的时候,你才着手"抽象化",写出通用的解决方法。
设计模式学习先参考一下《深入浅出设计模式》,这本书可读性非常好。
除SOLID原则外还有很多其它的面向对象原则。如:
- "组合替代继承":这是说相对于继承,要更倾向于使用组合;
- "笛米特法则":这是说"你的类对其它类知道的越少越好";
- "共同封闭原则":这是说"相关类应该打包在一起";
- "稳定抽象原则":这是说"类越稳定,越应该由抽象类组成";
当然,这些原则并不是孤立存在的,而是紧密联系的,遵循一个原则的同时也就遵循了另外一个或多个原则;反之,违反了其中一个原则也很可能同时就违反了另外一个或多个原则。 设计模式是这些原则在一些特定场景的应用结果。因此,可以把设计模式看作"框架",把OOD原则看作"规范"。 在学习设计模式的过程中,要经常性的反思,这个设计模式体现了面向对象设计原则中的哪个或哪一些原则。
(四)练习
1使用TDD的方式设计关实现复数类Complex。
伪代码:
无输入 --- 则复数的实部为0,虚部为0
仅输入实部 --- 复数的实部为所输入的实部,虚部为0
仅输入虚部 --- 复数的实部为0,虚部为所输入的虚部
实部虚部都输入 --- 复数的实部与虚部对应相应输入的实部虚部。
加法 --- 复数的实部与实部相加,虚部与虚部相加 ,即输出p1.rePart+p2.rePart,p1.imPart+p2.imPart
减法 --- 复数的实部与实部相减,虚部与虚部相减 , 即输出 p1.rePart-p2.rePart,p1.imPart-p2.imPart
产品代码:
package Exercise;
public class Complex
{
double rePart,imPart;
Complex()
{
this.rePart=0;
this.imPart=0;
}
Complex(double rePart)
{
this.rePart=rePart;
this.imPart=0;
}
Complex(double rePart,double imPart)
{
this.rePart=rePart;
this.imPart=imPart;
}
Complex Jia(Complex p1,Complex p2)
{
Complex p =new Complex(p1.rePart+p2.rePart,p1.imPart+p2.imPart);
return p;
}
Complex Jian(Complex p1,Complex p2)
{
Complex p =new Complex(p1.rePart-p2.rePart,p1.imPart-p2.imPart);
return p;
}
void Print()
{
System.out.println("复数的值为:");
if(this.imPart!=0)
System.out.println(this.rePart+"+"+this.imPart+"i");
else System.out.println(this.rePart);
}
}
测试代码:
package Exercise;
public class Test
{
public static void main(String[] args)
{
Complex c=new Complex();
Complex c1=new Complex(2,7);
Complex c2=new Complex(5,2);
c1.Print();
c2.Print();
System.out.println("这两复数和为:");
System.out.println((c.Jia(c1, c2).rePart+"+"+c.Jia(c1, c2).imPart+"i").toString());
System.out.println("这两复数差为:");
System.out.println(c.Jian(c1, c2).rePart+"+"+c.Jian(c1, c2).imPart+"i");
}
}
2.实验报告中统计自己的PSP(Personal Software Process)时
步骤 |
耗时 |
百分比 |
需求分析 |
5 | 6.25% |
设计 |
10 | 12.5% |
代码实现 |
50 | 62.5% |
测试 |
10 | 12.5% |
分析总结 |
5 | 6.25% |
3. 实现要有伪代码,产品代码,测试代码。
4.总结单元测试的好处
1.加快项目进程,并且是程序的设计更加完善。
2.通过简单的事务回滚功能在生产环境上做基于真实数据的测试而不用担心会产生不必要的数据。
3. 让自己负责的模块功能定义明确,模块与模块之间内部的改变不会产生影响。