北京电子科技学院(BESTI)
实 验 报 告
课程:Java程序设计 | 班级:1353 | 姓名:吴子怡(20135313) |
成绩: | 指导教师:娄嘉鹏 | 实验日期:2015.5.5 |
实验密级: | 预习程度: | 实验时间:—— |
仪器组次: 13 | 必修/选修:选修 | 实验序号:2 |
实验名称: 实验二 Java面向对象程序设计 |
一、实验内容
1. 初步掌握单元测试和TDD
2. 理解并掌握面向对象三要素:封装、继承、多态
3. 初步掌握UML建模
4. 熟悉S.O.L.I.D原则
5. 了解设计模式
二、实验要求
1.没有Linux基础的同学建议先学习《Linux基础入门(新版)》《Vim编辑器》 课程
2.完成实验、撰写实验报告,实验报告以博客方式发表,注意实验报告重点是运行结果,遇到的问题(工具查找,安装,使用,程序的编辑,调试,运行等)、解决办法(空洞的方法如“查网络”、“问同学”、“看书”等一律得0分)以及分析(从中可以得到什么启示,有什么收获,教训等)。
3. 严禁抄袭,有该行为者实验成绩归零,并附加其他惩罚措施。
4. 在电脑的目录中用自己的学号建立一个目录,代码和UML图要放到这个目录中,截图中没有学号的会要求重做,然后跟着下面的步骤练习。
***如下为以学号建立的目录截图:
三、实验仪器
名称 |
型号 |
数量 |
PC |
Dell |
1 |
虚拟机 |
实验楼 |
1 |
四、实验步骤
(一)单元测试
(1) 三种代码:当用程序解决问题时,要写三种码:伪代码、产品代码、测试代码。
需求:我们要在一个MyUtil类中解决一个百分制成绩转成“优、良、中、及格、不及格”五级制成绩的功能。
1、伪代码(伪代码可用汉语写,推荐使用英语,伪代码与具体编程语言无关,不要写与具体编程语言语法相关的语句(如用malloc分配内存,这样只能用C语言编程了),伪代码从意图层面来解决问题,最终,伪代码是产品代码最自然的、最好的注释。):
百分制转五分制: 如果成绩小于60,转成“不及格” 如果成绩在60与70之间,转成“及格” 如果成绩在70与80之间,转成“中等” 如果成绩在80与90之间,转成“良好” 如果成绩在90与100之间,转成“优秀” 其他,转成“错误”
2、产品代码: 有了伪代码,再用特定编程语言翻译,就得到可用的产品代码。如用java语言编写的代码MyUtil.java。以下是我操作时的代码截图:
3、测试代码:用于测试证明代码没有问题。Java编程时,程序员对类实现的测试叫单元测试(类XXXX的单元测试,一般建一个XXXXTest的类)。
针对MyUtil类,测试模块为MyUtilTest.java,代码截图如下:
4、测试用例(Test Case):为某个特殊目标而编制的一组测试输入、执行条件以及预期结果,以便测试某个程序路径或核实是否满足某个特定需求。
如本例中,测试输入“50”,预期结果是“不及格”。在Eclipse中实验运行结果截图如下,测试结果符合预期:
只有一组输入的测试是不充分的,应将一般情况都测试一下。另外,
不能只测试正常情况,还应测试异常情况,比如输入为负分或大于100的成绩。
实验代码和运行结果截图如下:
运行程序发现负分时与期望运行结果不一致,原因是判断不及格时没有要求成绩大于零。通过修改MyUtil.java,增加对负分的判断,实验代码和运行结果如下:
再次运行测试,测试结果符合预期,如下图所示:
测试边界情况:对输入为“0,60,70,80,90,100”这些边界情况进行测试,实验代码及运行结果如下:
可发现边界情况中输入100时有一个Bug。修改MyUtil.java,把判断优秀的条件中加入输入为100的情况,修改后的代码及运行结果截图如下:
测试通过。
5、为保证单元测度是充分的,一般要求是测试代码要比产品代码多。为使模块功能定义尽量明确,模块内部的改变不会影响其他模块,且模块的质量能得到稳定的、量化的保证,单元测试就是一个很有效的解决方案。
(2) TDD(Test Driven Devlopment, 测试驱动开发):就是一种先写测试代码,然后再写产品代码的开发方法。
TDD的一般步骤如下:
- 明确当前要完成的功能,记录成一个测试列表
- 快速完成编写针对此功能的测试用例
- 测试代码编译不通过(没产品代码呢)
- 编写产品代码
- 测试通过
- 对代码进行重构,并保证测试通过(重构下次实验练习)
- 循环完成所有功能的开发
1、实验步骤:
打开Eclipse,单击File->New->Java Project新建一个TDDDemo的Java项目,在TDDDemo项目中,把鼠标放到项目名TDDDemo上,单击右键,在弹出的菜单中选定New->Source Folder新建一个测试目录test,把鼠标放到test目录上,单击右键,在弹出的菜单中选定New->JUnit Test Case新建一个测试用例类MyUtilTest,增加第一个测试用例testNormal(注意测试用例前一定要有注解@Test,测试用例方法名任意)。
当测试代码没有语法错误时,我们把鼠标放到MyUtilTest.java上,单击右键,选择Run as->JUnit Test。运行,若测试结果出现了一个红条(red bar),说明测试没通过,红条上面会汇总测试情况。若测试结果出现一个绿条(green bar),说明测试通过了。TDD的目标是"Clean Code That Works",TDD的slogan是"Keep the bar green, to Keep the code clean"。
2、TDD的编码节奏是:增加测试代码,JUnit出现红条;修改产品代码;JUnit出现绿条,任务完成。
3、增加一个测试边界情况的用例testBoundary,最终的完整版代码及运行结果截图如下:
(二)面向对象三要素
(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三要素的第一个要素是封装,封装就是将数据与相关行为包装在一起以实现信息就隐藏。
封装实际上使用方法(method)将类的数据隐藏起来,控制用户对类的修改和访问数据的程度,从而带来模块化(Modularity)和信息隐藏(Information hiding)的好处;接口(interface)是封装的准确描述手段。
1、例如:
Java中用类进行封装,比如一个Dog类,通过使用类和访问控制(private,public)隐藏了属性color,开放了接口setColor(),getColor(),bark()和toString。Dog类是一个模块。测试代码与运行结果如下:
2、用UML中的类图来描述类Dog,首先在环境中打开shell,在命令行中输入umbrello,打开UML建模软件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属性。其中:+表示public,#表示 protected,-表示 private。
注意:(a)UML类图要展示类之间的静态关系,AnimalTest类依赖Dog类和Cat类,UML中依赖用带箭头的直线表示。
(b)UML类图中继承的表示法,是用一个带三角的直线指向父类。
在建模环境中的结果截图如下:
Dog类和Cat类都有Color属性和相应的setter和getter方法,明显违反了前面提到的DRY原则,我们可以通过继承解决这个问题,把Color属性和相应的setter和getter方法放到父类Animal中。
通过继承,消除了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类中覆盖这个方法。
UML类图中的Animal类中的shout()方法是抽象方法,是斜体的,Animal类是抽象类,也是斜体的。
代码表示及运行结果截图如下:
这时getInfo只需要一个了,参数为父类Animal,当方法参数类型为父类时,可以传入子类的对象,如上面第6行所示。大家需要理解并记住“在Java中,当我们用父类声明引用,用子类生成对象时,多态就出现了”,如上面第6行所示。
(三)设计模式初步
(1)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,依赖倒置原则)
1、OCP是OOD中最重要的一个原则,OCP的内容是:
software entities (class, modules, function, etc.) should open for extension,but closed for modification.
软件实体(类,模块,函数等)应该对扩充开放,对修改封闭。
基于OCP,利用面向对象中的多态性(Polymorphic),更灵活地处理变更代码,OCP可以用以下手段实现:(1)抽象和继承,(2)面向接口编程。
2、SRP的内容是:
There should never be more than one reason for a class to change
决不要有一个以上的理由修改一个类
3、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
子类必须可以被其基类所代
使用指向基类的指针或引用的函数,必须能够在不知道具体派生类对象类型的情况下使用它
4、LSP的核心思想是父类型对象可以被子类型对象所取代。前面举的Animal,Dog,Cat的那个例子是符合LSP原则的。LSP主张不要滥用继承,LSP原则清楚地指出,OOD中“ISA关系”是就行为功能而言。行为功能(behavior)不是内在的、私有的,而是外在、公开的,是客户程序所依赖的接口。
5、ISP的内容是:
Clients should not be forced to depend upon interfaces that they do not use
客户不应该依赖他们并未使用的接口
6、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
7、高层模块不应该依赖于低层模块。二者都应该依赖于抽象,抽象不应该依赖于细节。细节应该依赖于抽象 通过接口或者抽象类,DIP在应用中通过依赖注入的方式实现解耦,重用低级模块,重用实现,解除依赖。
(2)模式与设计模式
模式是某外在环境(Context) 下﹐对特定问题(Problem)的惯用解决之道(Solution)。模式必须使得问题明晰,阐明为什么用它来求解问题,以及在什么情况下有用,什么情况下不能起作用,每个模式因其重复性从而可被复用,本身有自己的名字,有可传授性,能移植到不同情景下。模式可以看作对一个问题可复用的专家级解决方法。 计算机科学中有很多模式:GRASP模式、分析模式 、软件体系结构模式 、设计模式:创建型,结构型,行为型、管理模式: The Manager Pool 实现模式、界面设计交互模式 …
这里面最重要的是设计模式,在面向对象中设计模式的地位可以和面向过程编程中的数据结构的地位相当。
(3)设计模式实示例:设计模式(design pattern)提供一个用于细化软件系统的子系统或组件,或它们之间的关系图,它描述通信组件的公共再现结构,通信组件可以解决特定语境中的一个设计问题。 随着系统中对象的数量增多,对象之间的交互成指数增长,设计模式可以帮我们以最好的方式来设计系统。设计模式背后是抽象和SOLID原则。 设计模式有四个基本要素:
Pattern name:描述模式,便于交流,存档
Problem:描述何处应用该模式
Solution:描述一个设计的组成元素,不针对特例
Consequence:应用该模式的结果和权衡(trade-offs)
1、Java类库中大量使用设计模式:
Factory:java.util.Calendar
Compsite:java.awt.Container
Decorator:java I/0
Iterator:java.util.Enumeration
Strategy:java.awt.LayoutManager
…
2、首先设计一个文档:
如果要求系统支持Float类,这是一个合理的要求,要支持Float类,Document类要修改两个地方,这违反了OCP原则,使用多态可以解决部分问题:
要支持Float类,Document类要修改构造方法,这还违反了OCP原则。封装、继承、多态解决不了问题,则采用设计模式解决问题:
通过增加了一层抽象层使代码符合了OCP原则。代码有良好的可扩充性、可维护性,代价是代码多了,效率变低下了。 DRY原则和YAGNI原则并非完全兼容。前者追求"抽象化",要求找到通用的解决方法;后者追求"快和省",意味着不要把精力放在抽象化上面,因为很可能"你不会需要它"。为了谋求平衡,可以采用Rule of three (三次原则):第一次用到某个功能时,写一个特定的解决方法;第二次用到的时候,拷贝上一次的代码(违反了DRY);第三次出现的时候,才着手"抽象化",写出通用的解决方法。
除SOLID原则外还有很多其它的面向对象原则。如:
"组合替代继承":这是说相对于继承,要更倾向于使用组合;
"笛米特法则":这是说"你的类对其它类知道的越少越好";
"共同封闭原则":这是说"相关类应该打包在一起";
"稳定抽象原则":这是说"类越稳定,越应该由抽象类组成";
原则并不是孤立存在的,而是紧密联系的,设计模式是这些原则在一些特定场景的应用结果。因此,可以把设计模式看作"框架",把OOD原则看作"规范"。
(四)练习:使用TDD的方式设计关实现复数类Complex。
(1)设计成果
1、伪代码:
* 定义一个复数类:复数类内包含:
* 1、复数的生成:由设置实部虚部、获取实部虚部,拼接合成复数整体(返回复数整体)三部分组成;
* 2、复数的四则运算:均由实部与实部运算,虚部与虚部运算(返回新复数);
* 3、运算模式选择:包含输出语句,无返回值。
2、产品代码:
3、测试代码
4、运行结果:
(2)PSP(Personal Software Process)
步骤 |
耗时 |
百分比 |
需求分析 |
10min |
16.7% |
设计 |
10min |
16.7% |
代码实现 |
25min |
41.6% |
测试 |
5min |
8.3% |
分析总结 |
10min |
16.7% |
3、总结单元测试的好处:让代码维护更容易;有助于改进代码质量和设计;保证最后的代码修改不会破坏之前代码的功能;提升反馈速度,减少重复工作,提高开发效率;帮助开发人员编写代码,提升质量、减少bug。
五、遇到的问题与解决方法
当做实验第一部分的第二个模块(TDD)时,我直接在实验二的Java project中建立了part1的一个包,里面包含MyUtil和MyUtilTest两个类。第二步在同一个工程中建立测试用例类exp2_part2_TDDDemo_Test,在其下建立包exp2_part2,然后选择新建JUnit Test Case。因为我和实验示范步骤不同,没有将TDD演示建立在另一个工程中,且我为了解决.java文件中没有MyUtil.java的问题,我在代码首部导入了exp2_part1这个包,用于调用这个包中的其他.java文件。这样一来却没有得到示范中的错误提示,反而是一次性通过测试了。后来才发现是因为示范代码中MyUtil.java中只有return “错误”;这个语句,而我调用的代码已经在实验一中做到基本完善了。
另外,在编写最后的练习题时,我在代码书写中花了比较多的时间,虽然写了伪代码以后让编程思路清晰了不少,但是在写代码时还是会遇到一些细节性问题,也体现出我在之前的视频学习中对一些知识点的遗忘比较多,根据Eclipse中的调试和提示,我最后修正了代码,同时也查看了一些成品代码,发现他们在读入实部虚部时的设置、获取函数的编写和我的设计思路的差别,引发了我对设计的思考,以后我会多看看成品代码再动手写写代码,先从整体上学会一些编程格式和思路,再去实践,少走一些弯路。
六、实验收获
1、单元测试的操作和优点
2、建模的整体感悟
3、调试代码的经验和设计框架的总体优化
4、对Eclipse的熟练程度有些许提高
5、对设计模式有了初步了解
七、参考资料
1.《UML精粹》 2.《构建之法 (电子版)》,著者邹欣Blog 3.《深入浅出设计模式》 4.《解析极限编程》 5.《单元测试之道》
八、工具