实验二 Java面对对象程序设计
一、实验内容
1. 初步掌握单元测试和TDD
2. 理解并掌握面向对象三要素:封装、继承、多态
3. 初步掌握UML建模
4. 熟悉S.O.L.I.D原则
5. 了解设计模式
二、实验要求
1.没有Linux基础的同学建议先学习《Linux基础入门(新版)》《Vim编辑器》 课程
2.完成实验、撰写实验报告,注意实验报告重点是运行结果,遇到的问题、解决办法以及分析。
3. 严禁抄袭,有该行为者实验成绩归零,并附加其他惩罚措施。
4. 在实验楼中的~/Code目录中用自己的学号建立一个目录,代码和UML图要放到这个目录中。
三、实验步骤
(一)单元测试
(1) 三种代码
伪代码、产品代码和测试代码
需求:在一个MyUtil类中解决一个百分制成绩转成“优、良、中、及格、不及格”五级制成绩的功能。
伪代码: 伪代码与具体编程语言无关,不要写与具体编程语言语法相关的语句伪代码从意图层面来解决问题,是产品代码最自然的、最好的注释:
(百分制转五分制):
如果成绩小于60,转成“不及格”
如果成绩在60与70之间,转成“及格”
如果成绩在70与80之间,转成“中等”
如果成绩在80与90之间,转成“良好”
如果成绩在90与100之间,转成“优秀”
其他,转成“错误”
产品代码:
一般写建一个XXXXTest的类:
public class MyUtilTest {
public static void main(String[] args) {
// 百分制成绩是50时应该返回五级制的“不及格”
if(MyUtil.percentage2fivegrade(50) != "不及格")
System.out.println("test failed!");
else
System.out.println("test passed!");
}
}
测试用例(Test Case):测试用例是为某个特殊目标而编制的一组测试输入、执行条件以及预期结果,以便测试某个程序路径或核实是否满足某个特定需求。
对一般情况进行测试,代码如下:
加以考虑负数和边界情况(50、60、100等),完整代码及测试如下:
(2) TDD(Test Driven Devlopment, 测试驱动开发)
TDD:先写测试代码,然后再写产品代码的开发方法叫“测试驱动开发”(TDD)。一般步骤如下:
• 明确当前要完成的功能,记录成一个测试列表
• 快速完成编写针对此功能的测试用例
• 测试代码编译不通过(没产品代码呢)
• 编写产品代码
• 测试通过
• 对代码进行重构,并保证测试通过(重构下次实验练习)
• 循环完成所有功能的开发
Java中有单元测试工具JUnit来辅助进行TDD。
用TDD的方式完成百分制转五分制:
1.打开Eclipse,新建Java项目TDDDemo;
2.我们在TDDDemo项目中,新建测试目录test;
3.test目录中,新建测试用例类MyUtilTest;
4.增加第一个测试用例testNormal,输入以下代码:
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));
}
}
!但MyUtil类还不存在,类中的percentage2fivegrade方法也不存在
5.在TDDDemo的src目录中新建MyUtil类,并实现percentage2fivegrade方法,然互右键MyUtilTest.java,选择Run as->JUnit Test,如下图:
发现不成功
6.修改代码,再次运行测试,成功:
总结:TDD的目标是"Clean Code That Works",TDD的slogan是"Keep the bar green, to Keep the code clean",大家体会一下。
TDD的编码节奏是:
• 增加测试代码,JUnit出现红条
• 修改产品代码
• JUnit出现绿条,任务完成
(二)面向对象三要素
(1)抽象
抽象包括两个方面,一是过程抽象,二是数据抽象。
(2)封装、继承与多态
原理:面向对象(Object-Oriented)的三要素包括:封装、继承、多态。面向对象的思想涉及到软件开发的各个方面,如面向对象分析(OOA)、面向对象设计(OOD)、面向对象编程实现(OOP)。OOA根据抽象关键的问题域来分解系统,关注是什么(what)。OOD是一种提供符号设计系统的面向对象的实现过程,用非常接近问题域术语的方法把系统构造成“现实世界”的对象,关注怎么做(how),通过模型来实现功能规范。OOP则在设计的基础上用编程语言(如Java)编码。贯穿OOA、OOD和OOP的主线正是抽象。过程抽象的结果是函数,数据抽象的结果是抽象数据类型(Abstract Data Type,ADT),类可以作具有继承和多态机制的ADT。数据抽象才是OOP的核心和起源。封装实际上使用方法(method)将类的数据隐藏起来,控制用户对类的修改和访问数据的程度,从而带来模块化(Modularity)和信息隐藏(Information hiding)的好处;接口(interface)是封装的准确描述手段。
具体步骤:
1. Java中用类进行封装,比如一个Dog类:
2.测试代码与运行结果如下:
3.用UML中的类图来描述类Dog,打开shell,在命令行中输入umbrello,打开UML建模软件umbrello
4.创建类Dog,Cat,Animal Test
5. +表示public,#表示 protected,-表示 private
这时的测试类如以下UML图所示:
6.把Dog类和Cat类中属性和相应的setter和getter方法放到父类Animal中,进一步优化:
7.进一步抽象,把Dog类中的bark()和Cat类中的meow()抽象成一个抽象方法shout(),Dog类和Cat类中覆盖这个方法,如以下UML图所示:
!注意:在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)面向接口编程。
SRP的内容是:
- There should never be more than one reason for a class to change
- 决不要有一个以上的理由修改一个类
对象提供单一职责的高度封装,对象的改变仅仅依赖于单一职责的改变,它基于软件设计中的高内聚性定义。
LSP的内容是:
- Subtypes must be substitutable for their base types
- Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it
- 子类必须可以被其基类所代
- 使用指向基类的指针或引用的函数,必须能够在不知道具体派生类对象类型的情况下使用它
ISP的内容是:
- Clients should not be forced to depend upon interfaces that they do not use
- 客户不应该依赖他们并未使用的接口
DIP的内容是:
- High level modules should not depend upon low level modules. Both should depend upon abstractions
- Abstractions should not depend upon details. Details should depend upon abstractions
- 高层模块不应该依赖于低层模块。二者都应该依赖于抽象
- 抽象不应该依赖于细节。细节应该依赖于抽象
(2)模式与设计模式
模式是某外在环境(Context) 下﹐对特定问题(Problem)的惯用解决之道(Solution)。模式必须使得问题明晰,阐明为什么用它来求解问题,以及在什么情况下有用,什么情况下不能起作用,每个模式因其重复性从而可被复用,本身有自己的名字,有可传授性,能移植到不同情景下。模式可以看作对一个问题可复用的专家级解决方法。 计算机科学中有很多模式:
- GRASP模式
- 分析模式
- 软件体系结构模式
- 设计模式:创建型,结构型,行为型
- 管理模式: The Manager Pool 实现模式
- 界面设计交互模式
- …
这里面最重要的是设计模式,在面向对象中设计模式的地位可以和面向过程编程中的数据结构的地位相当。
(3)设计模式实示例
设计模式(design pattern)提供一个用于细化软件系统的子系统或组件,或它们之间的关系图,它描述通信组件的公共再现结构,通信组件可以解决特定语境中的一个设计问题。 如图,随着系统中对象的数量增多,对象之间的交互成指数增长,设计模式可以帮我们以最好的方式来设计系统。设计模式背后是抽象和SOLID原则。 设计模式有四个基本要素:
- Pattern name:描述模式,便于交流,存档
- Problem:描述何处应用该模式
- Solution:描述一个设计的组成元素,不针对特例
- Consequence:应用该模式的结果和权衡(trade-offs)
Java类库中大量使用设计模式:
- Factory:java.util.Calendar
- Compsite:java.awt.Container
- Decorator:java I/0
- Iterator:java.util.Enumeration
- Strategy:java.awt.LayoutManager
- …
实例——一个设计:
class Integer {
int value;
public Integer(){
value=100;
}
public void DisplayValue(){
System.out.println(value);
}
}
class Document {
Integer pi;
public Document(){
pi = new Integer();
}
public void DisplayData(){
pi.DisplayValue();
}
}
public class MyDoc{
static Document d;
public static void main(String [] args) {
d = new Document();
d.DisplayData();
}
}
客户如果要求系统支持Float类,则使用多态解决,对应的代码如下:
abstract class Data{
public abstract void DisplayValue();
}
class Integer extends Data {
int value;
Integer(){
value=100;
}
public void DisplayValue(){
System.out.println(value);
}
}
class Document {
Data pd;
Document() {
pd=new Integer();
}
public void DisplayData(){
pd.DisplayValue();
}
}
public class MyDoc {
static Document d;
public static void main(String[] args) {
d = new Document();
d.DisplayData();
}
}
要支持Float类,Document类要修改构造方法,这还违反了OCP原则。封装、继承、多态解决不了问题了,这时需要设计模式了:
// Server Classes
abstract class Data {
abstract public void DisplayValue();
}
class Integer extends Data {
int value;
Integer() {
value=100;
}
public void DisplayValue(){
System.out.println (value);
}
}
// Pattern Classes
abstract class Factory {
abstract public Data CreateDataObject();
}
class IntFactory extends Factory {
public Data CreateDataObject(){
return new Integer();
}
}
//Client classes
class Document {
Data pd;
Document(Factory pf){
pd = pf.CreateDataObject();
}
public void DisplayData(){
pd.DisplayValue();
}
}
//Test class
public class MyDoc {
static Document d;
public static void main(String[] args) {
d = new Document(new IntFactory());
d.DisplayData();
}
}
(四)练习
1. 使用TDD的方式设计关实现复数类Complex。
2. 实验报告中统计自己的PSP(Personal Software Process)时间
步骤 |
耗时 |
百分比 |
需求分析 |
10 |
5.6% |
设计 |
50 |
27.8% |
代码实现 |
70 |
38.8% |
测试 |
30 |
16.7% |
分析总结 |
20 |
11.1% |
3. 实现要有伪代码,产品代码,测试代码。
伪代码:
设计一个类Complex,用于封装对复数的下列操作:
(1)一个带参数的构造函数,用于初始化复数成员
(2)一个不带参数的构造函数,调用代参数的构造函数完成对复数成员的初始化。
(3)实现两个复数的加法、减法、乘法、除法。
(4)以复数的标准形式:x+iy 输出此复数
(5) 写两个函数,分别获得复数的实部getReal(),getImage()和虚部。
产品代码:
测试代码:
4.总结单元测试的好处
(1)它是一种验证行为。
程序中的每一项功能都是测试来验证它的正确性。它为以后的开发提供支缓。就算是开发后期,我们也可以轻松的增加功能或更改程序结构,而不用担心这个过程中会破坏重要的东西。而且它为代码的重构提供了保障。这样,我们就可以更自由的对程序进行改进。
(2)它是一种设计行为。
编写单元测试将使我们从调用者观察、思考。特别是先写测试(test-first),迫使我们把程序设计成易于调用和可测试的,即迫使我们解除软件中的耦合。
(3)它是一种编写文档的行为。
单元测试是一种无价的文档,它是展示函数或类如何使用的最佳文档。这份文档是可编译、可运行的,并且它保持最新,永远与代码同步。
(4)它具有回归性。
自动化的单元测试避免了代码出现回归,编写完成之后,可以随时随地的快速运行测试。
理解:
一般认为软件的用户就是使用软件的人,而用户需求通常表现为一纸文档。然而,这个人在哪里?他今天能来吗?这份文档是谁在维护?今天下午他能对着文档帮你看看实现吗?昨天有人改过这份文档吗?想想这些问题的答案,你都能感觉到事情有多么飘乎……在软件开发中,或者说至少在软件实现中,不够具体的东西作用总是有限的,有时甚至仅有“想象”中的作用。
而且,从更细的粒度来看,“软件模块”的用户跟“软件”的用户不是一个概念。对一个模块来说,它的直接“用户”是调用它的其它模块,而不是用户界面之外的人。从这个角度,写下单元测试的时候,不仅模块的需求得到了明确表达,模块还有了独立的、具体的、有效的第一个“用户”。说它是独立的,至少意味着远在其它模块完成之前你的模块就已经有“用户”了;说它是具体的,是因为它真正“使用”你的模块,而不只是扔给你一份文档看看;说它是有效的,是因为它随时可以运行,一运行就用遍模块的所有接口。
五. 心得体会
Java是面向对象程序设计是一门实践性比较强的课程,感觉本次实验比上次难度增大。通过这次实验,我了解了单元测试,初步掌握了UML建模,熟悉了熟悉S.O.L.I.D原则和设计原则。但是也感到有些力不从心,特别是对于最后的练习,感觉能够设计好,但是却无法很顺畅的运用代码来实现。通过对Java的学习和实验体验,我发现它确实有很多方便之处,它集抽象性、封装性、继承性和多态性于一体,实现了代码重用和代码扩充,提高了软件开发的效率。对于我们这个专业来说学好Java语言是很重要的。所以在实际中,我们必须把理论和实践结合起来。在实验中,我们理解理论课上的知识,然后运用到实际的操作中,我们必须在现有的理论的基础上,进行实践。多次实验后,也让我看到了现在学习的一个很大弱点:只听不练,永远不会熟练运用;空记技巧,忽略思想,会在多变的习题中十分无奈。在以后的Java学习中,我一定要多与老师、同学交流,多培养编写代码的感觉,也多多提高代码质量。