实验二 Java面向对象程序设计
(由于网速不好,实验楼的光标反映过慢,所以代码的调适与运行都是在电脑的Eclipse里面完成的,没在实验楼里截图)
实验内容
- 初步掌握单元测试和TDD
- 理解并掌握面向对象三要素:封装,继承,多态
- 初步掌握UML建模
- 熟悉S.O.L.I.D
- 了解设计模式
先在实验楼中的~/Code目录中用自己学号建立一个目录,用于存放代码和UML图
实验步骤
(一) 单元测试
(1) 三种代码
想用程序解决问题时,要会写三种代码
l 伪代码
l 产品代码
l 测试代码
举例:
需求:我们要在一个MyUtil
类中解决一个百分制成绩转成“优、良、中、及 格、不及格”五级制成绩的功能
l 伪代码(伪代码与具体编程语言无关,不要写与具体编程语言语法相关的语句,伪代码
从意图层面来解决问题,最终,伪代码
是产品代码
最自然的、最好的注释)
l 产品代码(把伪代码用Java语言翻译一下)
l 测试代码(写了产品代码
,我们还要写测试代码
,证明自己的代码没有问题。Java编程时,程序员对类实现的测试叫单元测试
。类XXXX
的单元测试
,一般写建一个XXXXTest
的类,针对MyUtil
类写一个MyUtilTest.java
的测试模块,代码如下)
这里我们设计了一个测试用例(Test Case)
,测试用例
是为某个特殊目标而编制的一组测试输入、执行条件以及预期结果,以便测试某个程序路径或核实是否满足某个特定需求
测试一般情况:
测试异常情况,如输入为负分或大于100的成绩,代码如下:
运行程序发现负分时与期望不一致,找到了一个bug,原因是判断不及格时没有要求成绩大于零。修改MyUtil.java
,增加对负分的判断,代码如下:
为保证单元测度充分,一般要求测试代码
比产品代码
多。《单元测试之道》提出的Right-BICEP
方法可用于写测试
(2)TDD(Test Driven Devlopment,测试驱动开发)
测试驱动开发(TDD):先写测试代码
,再写产品代码
,从而写出来的代码就是正确的
步骤如下:
l 明确当前要完成的功能,记录成一个测试列表
l 快速完成编写针对此功能的测试用例
l 测试代码编译不通过(无产品代码?)
l 编写产品代码
l 对代码进行重构,并保证测试通过(重构下次实验练习)
l 循环完成所有功能的开发
基于TDD,我们不会出现过度设计的情况,需求通过测试用例表达出来了,我们的产品代码
只要让测试通过就可以
Java中有单元测试工具JUnit来辅助进行TDD
用TDD的方式把前面的例子重写一次
Eclipse
->File->New->Java Project
新建一个TDDDemo
的Java项目
TDDDemo->右键->New->Source Folder
新建一个测试目录test
Test
->右键->New->JUnit Test Case
新建一个测试用例类MyUtilTest
我们增加第一个测试用例testNormal
(测试用例前一定要有注解@Test)
在TDDDemo
的src
目录中新建MyUtil
的类,并实现percentage2fivegrade方法
TDD的编码节奏是:
l 增加测试代码,Junit出现红条
l 修改产品代码
l Junit出现绿条,任务完成
(二)面向对象三要素
(1)抽象
抽象是指人在认识思维活动中对事物表象因素的舍弃和对本质因素的抽取。
抽象就是抽出事物的本质特征而暂时不考虑他们的细节。对于复杂系统问题人们借助分层次抽象的方法进行问题求解;
在抽象的最高层,可以使用问题环境的语言,以概括的方式叙述问题的解。
在抽象的较低层,则采用过程化的方式进行描述。
在描述问题解时,使用面向问题和面向实现的术语。 程序设计中,抽象包括两个方面,一是过程抽象,二是数据抽象。
举例:
如图可以打印出“1,2,3”,想打引“1,2,3,4”怎么办?大多做法是把上面的代码拷贝下来,再加一行:
想打印出“1..100"怎么办?这两段代码有三行重复的代码,违反了常见的一个编程原则DRY(Don‘t Repeat Yourself),解决的方法是进行过程抽象,写一个函数printn:
之前的两段代码就可以用
(2) 封装,继承与多态
面向对象三要素:封装、继承、多态
面向对象的思想涉及到软件开发的各个方面,如面向对象分析(OOA)、面向对象设计(OOD)、面向对象编程实现(OOP)。OOA根据抽象关键的问题域来分解系统。OOD是一种提供符号设计系统的面向对象的实现过程,用非常接近问题域术语的方法把系统构造成“现实世界”的对象,通过模型来实现功能规范
OOP则在设计的基础上用编程语言(如Java)编码。贯穿OOA、OOD和OOP的主线正是抽象
过程抽象的结果是函数,数据抽象的结果是抽象数据类型(Abstract Data Type,ADT),类可以作具有继承和多态机制的ADT。数据抽象才是OOP的核心和起源
封装:封装就是将数据与相关行为包装在一起以实现信息就隐藏
比如:
封装实际上使用方法(method)将类的数据隐藏起来,控制用户对类的修改和访问数据的程度,从而带来模块化(Modularity)和信息隐藏(Information hiding)的好处;接口(interface)是封装的准确描述手段
用UML中的类图来描述类Dog
shell->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属性
UML类图中继承的表示法,是用一个带三角的直线指向父类,通过继承,我们消除了Dog
类和Cat
类中的重复代码,符合DRY
的要求
继承是实现软件可重用的根基,是提高软件系统的可扩展性与可维护性的主要途径。以封装为基础,继承可以实现代码复用,继承更重要的作用是实现多态。 面向对象中允许不同类的对象对同一消息做出响应,即同一消息可以根据发送对象的不同而采用多种不同的行为方式,我们称此现象为多态性
多态是面向对象程序设计的灵活性和可扩展性的基础
(三)设计模式初步
(1)S.O.L.I.D
面向对象三要素是“封装、继承、多态”,任何面向对象编程语言都会在语法上支持这三要素。如何借助抽象思维用好三要素特别是多态还是非常困难的,S.O.L.I.D类设计原则是一个很好的指导
基于OCP
,利用面向对象中的多态性(Polymorphic),更灵活地处理变更拥抱变化,OCP
可以用以下手段实现:(1)抽象和继承,(2)面向接口编程
LSP
的核心思想是父类型对象可以被子类型对象所取代
(2)模式与设计模式
模式是某外在环境下﹐对特定问题的惯用解决之道,其中最重要的是设计模式
(3)设计模式实示例
设计模式提供一个用于细化软件系统的子系统或组件,或它们之间的关系图,它描述通信组件的公共再现结构,通信组件可以解决特定语境中的一个设计问题
(四)练习
使用TDD方式设计并实现复数类Complex
(1) 伪代码
设计一个复数类complex,分别将复数的实部和虚部作为属性
定义两个成员方法计算两个复数的和与差
定义一个print()方法输出复数的值,当虚部为0时不输出虚部
定义一个song类使用complex类,在这个类的主方法中创建两个复数对象,计算并输出
(2) 产品代码
class complex {
double re,im;
complex(){
this.re=0;
this.im=0;}
complex(double re){
this.re=re;
this.im=0;}
complex(double re,double im){
this.re=re;
this.im=im;
}
complex add(complex p1,complex p2){
complex p=new complex(p1.re+p2.re,p1.im+p2.im);
return p;
}
complex minus(complex p1,complex p2){
complex p=new complex(p1.re-p2.re,p1.im-p2.im);
return p;
}
void print(){
System.out.println("复数的值为:");
if(this.im!=0)
System.out.println(this.re+"+"+this.im+"i");
else
System.out.println(this.re);
}
}
(3) 测试代码
public class song{
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.add(c1,c2).re+"+"+c.add(c1, c2).im+"i").toString());
System.out.println("这两个复数的差为:");
System.out.println((c.minus(c1,c2).re+"+"+c.minus(c1, c2).im+"i").toString());
}
}
步骤 |
耗时 |
百分比 |
需求分析 |
20min |
20% |
设计 |
25min |
25% |
代码实现 |
40min |
40% |
测试 |
2min |
15% |
分析总结 |
10min |
单元测试的好处:
保障了单元自身功能的正确性
单元测试是随着代码自然发展的,不会因为代码发生改变而过期
单元测试为每个单元都提供了一个必须遵守的,严格的和书面的契约