北京电子科技学院(BESTI)
实 验 报 告
课程:Java程序设计 班级:1352 姓名:潘俊洋 学号:20135230
成绩: 指导教师:娄嘉鹏 实验日期:2015.5.4
实验密级: 预习程度: 实验时间:
仪器组次: 必修/选修: 实验序号:2
实验名称: Java面向对象程序设计
实验仪器:
名称 | 型号 | 数量 |
PC |
| 1 |
|
|
|
实验内容
1. 初步掌握单元测试和TDD
2. 理解并掌握面向对象三要素:封装、继承、多态
3. 初步掌握UML建模
4. 熟悉S.O.L.I.D原则
5. 了解设计模式
实验要求
1.没有Linux基础的同学建议先学习《Linux基础入门(新版)》《Vim编辑器》 课程
2.完成实验、撰写实验报告,实验报告以博客方式发表在博客园,注意实验报告重点是运行结果,遇到的问题(工具查找,安装,使用,程序的编辑,调试,运行等)、解决办法(空洞的方法如“查网络”、“问同学”、“看书”等一律得0分)以及分析(从中可以得到什么启示,有什么收获,教训等)。报告可以参考范飞龙老师的指导
3. 严禁抄袭,有该行为者实验成绩归零,并附加其他惩罚措施。
4. 请大家先在实验楼中的~/Code
目录中用自己的学号建立一个目录,代码和UML图要放到这个目录中,截图中没有学号的会要求重做,然后跟着下面的步骤练习。
实验步骤
(一)单元测试
编程是智力活动,不是打字,编程前要把干什么、如何干想清楚才能把程序写对、写好。与目前不少同学一说编程就打开编辑器写代码不同,我希望同学们养成一个习惯,当你们想用程序解决问题时,要会写三种码:
- 伪代码
- 产品代码
- 测试代码
我们先写伪代码
,伪代码可以用汉语写,推荐大家用英语写,伪代码与具体编程语言无关,不要写与具体编程语言语法相关的语句(如用malloc分配内存
,这样只能用C语言编程了),伪代码
从意图层面来解决问题,最终,伪代码
是产品代码
最自然的、最好的注释。想用编程来解决问题,首先要用伪代码
表明自己想明白了。 有了伪代码
,我们用特定编程语言翻译一下,就是可用的产品代码
了。产品代码
写完了,如果别人要使用这个代码,把MyUtil.java
拷给他就可以了。但是作为负责任的你,肯定会担心自己的程序会有Bug。如果别人用自己的代码发现一堆Bugs,那多没面子!怎么办?写了产品代码
,我们还要写测试代码
,证明自己的代码没有问题。Java编程时,程序员对类实现的测试叫单元测试
。类XXXX
的单元测试
,我们一般写建一个XXXXTest
的类,针对MyUtil
类我们写一个MyUtilTest.java
的测试模块。
这时测试都符合预期了,我们把MyUtil.java
提供给别人使用时,心里比较有底气了。那如何保证单元测度是充分的呢?我们的一般要求是测试代码
要比产品代码
多。如何写测试,《单元测试之道》提出了Right-BICEP
的方法,大家可以参考一下。 软件是由多人合作完成的,不同人员的工作相互有依赖关系。软件的很多错误都来源于程序员对模块功能的误解、疏忽或不了解模块的变化。如何能让自己负责的模块功能定义尽量明确,模块内部的改变不会影响其他模块,而且模块的质量能得到稳定的、量化的保证?单元测试就是一个很有效的解决方案。
(2) TDD(Test Driven Devlopment, 测试驱动开发)
我们先写产品代码
,然后再写测试代码
,通过测试发现了一些Bugs,提高了代码质量。这有问题吗?软件开发从建筑中吸取了很多营养
工人是“先把墙砌好的,再用绳子测一下墙平不平,直不直,如果不平或不直拆了重砌”,还是“先用绳子给出平和直的标准,然后靠着绳子砌墙,从而保证了墙砌出来就是又平又直的”呢?答案是不言而喻的了。 拿编程做对比,我们是该“先写产品代码
,然后再写测试代码
,通过测试发现了一些Bugs,修改代码”,还是该“先写测试代码
,然后再写产品代码
,从而写出来的代码就是正确的”呢?当然先写测试代码了。这种先写测试代码
,然后再写产品代码
的开发方法叫“测试驱动开发”(TDD)。TDD的一般步骤如下:
- 明确当前要完成的功能,记录成一个测试列表
- 快速完成编写针对此功能的测试用例
- 测试代码编译不通过(没产品代码呢)
- 编写产品代码
- 测试通过
- 对代码进行重构,并保证测试通过(重构下次实验练习)
- 循环完成所有功能的开发
基于TDD,我们不会出现过度设计的情况,需求通过测试用例表达出来了,我们的产品代码
只要让测试通过就可以了。
需求:我们要在一个MyUtil
类中解决一个百分制成绩转成“优、良、中、及格、不及格”五级制成绩的功能。
图中的红叉说明代码存在语法错误,原因很简单,MyUtil
类还不存在,类中的percentage2fivegrade方法也不存在,我们在TDDDemo
的src
目录中新建一个MyUtil
的类,并实现percentage2fivegrade方法
测试结果出现了一个绿条(green bar),说明测试通过了。TDD的目标是"Clean Code That Works",TDD的slogan是"Keep the bar green, to Keep the code clean"
(二)面向对象三要素
(1)抽象
抽象一词的本意是指人在认识思维活动中对事物表象因素的舍弃和对本质因素的抽取。抽象是人类认识复杂事物和现象时经常使用的思维工具,抽象思维能力在程序设计中非常重要,"去粗取精、化繁为简、由表及里、异中求同"的抽象能力很大程度上决定了程序员的程序设计能力。
抽象就是抽出事物的本质特征而暂时不考虑他们的细节。对于复杂系统问题人们借助分层次抽象的方法进行问题求解;在抽象的最高层,可以使用问题环境的语言,以概括的方式叙述问题的解。在抽象的较低层,则采用过程化的方式进行描述。在描述问题解时,使用面向问题和面向实现的术语。 程序设计中,抽象包括两个方面,一是过程抽象,二是数据抽象。
(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的核心和起源。
OO三要素的第一个要素是封装,封装就是将数据与相关行为包装在一起以实现信息就隐藏。Java中用类进行封装
我们可以用UML中的类图来描述类Dog
,首先我们在实验楼的环境中打开shell,在命令行中输入umbrello
,打开UML建模软件umbrello
我们可以看到,在UML 里,一个类的属性能显示它的名字,类型,初始化值,属性也可以显示private,public,protected。 类的方法能显示它们的方法名,参数,返回类型,以及方法的private,public,protected属性。其中:
- +表示public
- #表示 protected
- -表示 private
使用UML可以让我们不必关注细节。同样,我们可以建立一个Cat
类
请大家注意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
类中覆盖这个方法
(三)设计模式初步
(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)面向接口编程。 比如,在一个图形系统中,已经存在三个模块Shape
,Square
,Circle
,如下图所示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
- 子类必须可以被其基类所代
- 使用指向基类的指针或引用的函数,必须能够在不知道具体派生类对象类型的情况下使用它请大家想一想,
Square
类为何不能继承Rectangle
类,在数学上好像是没有什么问题的呀。LSP
告诉大家的一点是不要滥用继承,LSP
原则清楚地指出,OOD中“ISA关系”是就行为功能而言。行为功能(behavior)不是内在的、私有的,而是外在、公开的,是客户程序所依赖的接口。(2)模式与设计模式
模式是某外在环境(Context) 下﹐对特定问题(Problem)的惯用解决之道(Solution)。模式必须使得问题明晰,阐明为什么用它来求解问题,以及在什么情况下有用,什么情况下不能起作用,每个模式因其重复性从而可被复用,本身有自己的名字,有可传授性,能移植到不同情景下。模式可以看作对一个问题可复用的专家级解决方法。 计算机科学中有很多模式:
- GRASP模式
- 分析模式
- 软件体系结构模式
- 设计模式:创建型,结构型,行为型
- 管理模式: The Manager Pool 实现模式
- 界面设计交互模式
- …
这里面最重要的是设计模式,在面向对象中设计模式的地位可以和面向过程编程中的数据结构的地位相当。
(3)设计模式实示例
设计模式(design pattern)提供一个用于细化软件系统的子系统或组件,或它们之间的关系图,它描述通信组件的公共再现结构,通信组件可以解决特定语境中的一个设计问题。如图,随着系统中对象的数量增多,对象之间的交互成指数增长,设计模式可以帮我们以最好的方式来设计系统。设计模式背后是抽象和SOLID原则。 设计模式有四个基本要素:
- Pattern name:描述模式,便于交流,存档
- Problem:描述何处应用该模式
- Solution:描述一个设计的组成元素,不针对特例
- Consequence:应用该模式的结果和权衡(trade-offs)
-
我们设计了一个文档系统,如下图UML类图所示:
-
对应的代码如下:
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();
}
} -
抽象工厂模式应用如下:
对应代码如下:
// 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(); } }
-
我们看到通过增加了一层抽象层使代码符合了OCP原则。代码有良好的可扩充性、可维护性,代价是代码多了,效率变低下了。 设计模式初学者容易过度使用它们,导致过度设计,也就是说,遵守DRY和OCP当然好,但会出现YAGNI(You aren't gonna need it, 你不会需要它)问题。 DRY原则和YAGNI原则并非完全兼容。前者追求"抽象化",要求找到通用的解决方法;后者追求"快和省",意味着不要把精力放在抽象化上面,因为很可能"你不会需要它"。怎么平衡呢?有一个Rule of three (三次原则):第一次用到某个功能时,你写一个特定的解决方法;第二次又用到的时候,你拷贝上一次的代码(违反了DRY);第三次出现的时候,你才着手"抽象化",写出通用的解决方法。 设计模式学习先参考一下《深入浅出设计模式》,这本书可读性非常好。
除SOLID原则外还有很多其它的面向对象原则。如:
- "组合替代继承":这是说相对于继承,要更倾向于使用组合;
- "笛米特法则":这是说"你的类对其它类知道的越少越好";
- "共同封闭原则":这是说"相关类应该打包在一起";
- "稳定抽象原则":这是说"类越稳定,越应该由抽象类组成";
当然,这些原则并不是孤立存在的,而是紧密联系的,遵循一个原则的同时也就遵循了另外一个或多个原则;反之,违反了其中一个原则也很可能同时就违反了另外一个或多个原则。 设计模式是这些原则在一些特定场景的应用结果。因此,可以把设计模式看作"框架",把OOD原则看作"规范"。 在学习设计模式的过程中,要经常性的反思,这个设计模式体现了面向对象设计原则中的哪个或哪一些原则。
-
(四)练习
1使用TDD的方式设计关实现复数类Complex。
-
- 伪代码
-
复数类ComplexNumber的方法:
ComplexNumber():构造函数,将实部和虚部都置为0
ComplexNumber(double r,double i):构造函数,通过参数r,i初始化实部和虚部
complexAdd(ComplexNumber c):当前复数对象与参数对象相加,所得的结果也是复数,返回给此方法调用者
complexMinus(ComplexNumber c):当前复数对象与参数对象相减,所得的结果也是复数,返回给此方法调用者
complexMulti(ComplexNumber c):当前复数对象与参数对象相乘,所得的结果也是复数,返回给此方法调用者 - 测试代码
-
public class Test
{
public static void main(String args[])
{
ComplexNumber cNumber_1 =
new ComplexNumber(3,-5);
ComplexNumber
cNumber_2 =
new ComplexNumber(2,2);
double d = 10.0;
System.out.println(cNumber_1.toString() + " 加 "
+ cNumber_2.toString() + "
等于 "
+ cNumber_1.complexAdd(cNumber_2).toString());
System.out.println(cNumber_1.toString() + " 加 "
+ d + " 等于 "
+
cNumber_1.complexAdd(d).toString());
System.out.println();System.out.println(cNumber_1.toString() + " 减 "
+ cNumber_2.toString() + "
等于 "
+ cNumber_1.complexMinus(cNumber_2).toString());
System.out.println(cNumber_1.toString() + " 减 "
+ d + " 等于 "
+
cNumber_1.complexMinus(d).toString());
System.out.println();System.out.println(cNumber_1.toString() + " 乘 "
+ cNumber_2.toString() + "
等于 "
+ cNumber_1.complexMulti(cNumber_2).toString());
System.out.println(cNumber_1.toString() + " 乘 "
+ d + " 等于 "
+
cNumber_1.complexMulti(d).toString());
System.out.println();System.out.println(cNumber_1.toString() + " 除 "
+ cNumber_2.toString() +
"等于"
+ cNumber_1.complexDivision(cNumber_2).toString());
System.out.println(cNumber_1.toString() + " 除 "
+ d + " 等于 "
+
cNumber_1.complexDivision(d).toString());
}
}
class ComplexNumber
{
//复数运算方法
//加
ComplexNumber complexAdd(ComplexNumber
c)
{
return new ComplexNumber(
this.m_dRealPart +
c.getRealPart(),
this.m_dImaginPart +
c.getImaginaryPart());
}
ComplexNumber complexAdd(double c)
{
return new ComplexNumber(
this.m_dRealPart + c,
this.m_dImaginPart);
}
//减
ComplexNumber complexMinus(ComplexNumber
c)
{
return new ComplexNumber(
this.m_dRealPart -
c.getRealPart(),
this.m_dImaginPart -
c.getImaginaryPart());
}
ComplexNumber complexMinus(double c)
{
return new ComplexNumber(
this.m_dRealPart - c,
this.m_dImaginPart);
}
//乘
ComplexNumber complexMulti(ComplexNumber
c)
{
return new ComplexNumber(
this.m_dRealPart * c.getRealPart()- this.m_dImaginPart * c.getImaginaryPart(),
this.m_dRealPart *
c.getImaginaryPart()
+ this.m_dImaginPart *
c.getRealPart());
}
ComplexNumber complexMulti(double c)
{
return
new ComplexNumber(
this.m_dRealPart * c, this.m_dImaginPart *
c);
}
//除
ComplexNumber complexDivision(ComplexNumber c){
return
new ComplexNumber((this.m_dRealPart*c.getRealPart()
+this.m_dImaginPart*c.getImaginaryPart())
/(c.getRealPart()*c.getRealPart()+c.getImaginaryPart()*c.getImaginaryPart()),(this.m_dImaginPart*c.getRealPart()
-this.m_dRealPart*c.getImaginaryPart())
/(c.getRealPart()*c.getRealPart()+c.getImaginaryPart()*c.getImaginaryPart()));}
ComplexNumber complexDivision(double c){
return new
ComplexNumber(this.m_dRealPart/c,this.m_dImaginPart/c);
}
//toString()
public String toString()
{
return "(" + m_dRealPart + " + "
+
m_dImaginPart + " i" + ")";
}
}- 产品代码
-
public class Test
{
public static void main(String args[])
{
ComplexNumber cNumber_1 =
new ComplexNumber(3,-5);
ComplexNumber
cNumber_2 =
new ComplexNumber(2,2);
double d = 10.0;
System.out.println(cNumber_1.toString() + " 加 "
+ cNumber_2.toString() + "
等于 "
+ cNumber_1.complexAdd(cNumber_2).toString());
System.out.println(cNumber_1.toString() + " 加 "
+ d + " 等于 "
+
cNumber_1.complexAdd(d).toString());
System.out.println();System.out.println(cNumber_1.toString() + " 减 "
+ cNumber_2.toString() + "
等于 "
+ cNumber_1.complexMinus(cNumber_2).toString());
System.out.println(cNumber_1.toString() + " 减 "
+ d + " 等于 "
+
cNumber_1.complexMinus(d).toString());
System.out.println();System.out.println(cNumber_1.toString() + " 乘 "
+ cNumber_2.toString() + "
等于 "
+ cNumber_1.complexMulti(cNumber_2).toString());
System.out.println(cNumber_1.toString() + " 乘 "
+ d + " 等于 "
+
cNumber_1.complexMulti(d).toString());
System.out.println();System.out.println(cNumber_1.toString() + " 除 "
+ cNumber_2.toString() +
"等于"
+ cNumber_1.complexDivision(cNumber_2).toString());
System.out.println(cNumber_1.toString() + " 除 "
+ d + " 等于 "
+
cNumber_1.complexDivision(d).toString());
}
}
//复数运算方法
//加
ComplexNumber complexAdd(ComplexNumber
c)
{
return new ComplexNumber(
this.m_dRealPart +
c.getRealPart(),
this.m_dImaginPart +
c.getImaginaryPart());
}
ComplexNumber complexAdd(double c)
{
return new ComplexNumber(
this.m_dRealPart + c,
this.m_dImaginPart);
}
//减
ComplexNumber complexMinus(ComplexNumber
c)
{
return new ComplexNumber(
this.m_dRealPart -
c.getRealPart(),
this.m_dImaginPart -
c.getImaginaryPart());
}
ComplexNumber complexMinus(double c)
{
return new ComplexNumber(
this.m_dRealPart - c,
this.m_dImaginPart);
}
//乘
ComplexNumber complexMulti(ComplexNumber
c)
{
return new ComplexNumber(
this.m_dRealPart * c.getRealPart()- this.m_dImaginPart * c.getImaginaryPart(),
this.m_dRealPart *
c.getImaginaryPart()
+ this.m_dImaginPart *
c.getRealPart());
}
ComplexNumber complexMulti(double c)
{
return
new ComplexNumber(
this.m_dRealPart * c, this.m_dImaginPart *
c);
}
//除
ComplexNumber complexDivision(ComplexNumber c){
return
new ComplexNumber((this.m_dRealPart*c.getRealPart()
+this.m_dImaginPart*c.getImaginaryPart())
/(c.getRealPart()*c.getRealPart()+c.getImaginaryPart()*c.getImaginaryPart()),(this.m_dImaginPart*c.getRealPart()
-this.m_dRealPart*c.getImaginaryPart())
/(c.getRealPart()*c.getRealPart()+c.getImaginaryPart()*c.getImaginaryPart()));}
ComplexNumber complexDivision(double c){
return new
ComplexNumber(this.m_dRealPart/c,this.m_dImaginPart/c);
}
//toString()
public String toString()
{
return "(" + m_dRealPart + " + "
+
m_dImaginPart + " i" + ")";
}
} - 单元测试的好处
-
熟悉单元测试技术,了解相关的基本原理;
掌握代码,积累代码编写经验,积累调试经验,积累分析问题、解决问题的经验;
训练动手能力,单元测试代码不是业务代码,开发、维护过程中不需要特别关注质量要求,底限是达到验证业务代码逻辑性的目的,因而比修改代码要省心、省事;
不需要准备项目运行环境,单元测试代码在运行时的外部依赖比较少,执行验证、调试代码的代价会很低;
降低新手程序员进入项目的门槛,有助于积累信心。
项目过程中写单元测试的好处
一边写代码,一边检查代码中的小错误或者小疏忽,提前解决代码中可能存在的笔误;
为了让单元测试代码更好写,需要花点心思在思考类和方法的结构,好处是可以有效的提升代码的可测试性,否则设计和结构不理想时,单元测试代码写作时也会比较麻烦,需要打很多桩;
在集成测试前,有机会做验证模块内部的逻辑正确性,避免在联调时花费过多的时间来解决小问题,提高联调的效率;
单元测试代码对运行环境依赖小,不需要特别准备复杂的环境,外部模块的行为和表现是可控制的,易于模拟异常场景;
记录已实现的特性,代码即是最好的文档,设计良好的单元测试代码有助于后续维护,降低后期修改引入问题的可能性。
维护或者学习代码过程中写单元测试的好处
熟悉已有代码的逻辑,变量的变化方式,代码的运行轨迹,提高学习代码的效率,降低通过看代码来学习代码的负担;
帮助重构,提高重构的成功率;
辅助问题分析,遇到问题时,可以借助分析单元测试代码来了解模块的一般行为
-