20135239益西拉姆第二次实验报告
实验二 java面向对象程序设计
实验内容
1. 初步掌握单元测试和TDD
2. 理解并掌握面向对象三要素:封装、继承、多态
3. 初步掌握UML建模
4. 熟悉S.O.L.I.D原则
5. 了解设计模式
实验步骤
(一)单元测试
(1) 三种代码
编程是智力活动,不是打字,编程前要把干什么、如何干想清楚才能把程序写对、写好。与目前不少同学一说编程就打开编辑器写代码不同,我希望同学们养成一个习惯,当你们想用程序解决问题时,要会写三种码:
- 伪代码
- 产品代码
- 测试代码
我们通过一个例子说明如何写这三种代码。
需求:我们要在一个
MyUtil
类中解决一个百分制成绩转成“优、良、中、及格、不及格”五级制成绩的功能。
我们先写伪代码
,伪代码可以用汉语写,推荐大家用英语写,伪代码与具体编程语言无关,不要写与具体编程语言语法相关的语句(如用malloc分配内存
,这样只能用C语言编程了),伪代码
从意图层面来解决问题,最终,伪代码
是产品代码
最自然的、最好的注释。针对上面的问题,我们可以通过伪代码这样解决:
百分制转五分制: 如果成绩小于60,转成“不及格” 如果成绩在60与70之间,转成“及格” 如果成绩在70与80之间,转成“中等” 如果成绩在80与90之间,转成“良好” 如果成绩在90与100之间,转成“优秀” 其他,转成“错误”
简单吧?想用编程来解决问题,首先要用伪代码
表明自己想明白了。 有了伪代码
,我们用特定编程语言翻译一下,就是可用的产品代码
了,当然,我们在这要选用Java,小菜一碟了,翻译好的MyUtil.java
如下:
public class MyUtil{
public static String percentage2fivegrade(int grade){
//如果成绩小于60,转成“不及格”
if (grade < 60)
return "不及格";
//如果成绩在60与70之间,转成“及格”
else if (grade < 70)
return "及格";
//如果成绩在70与80之间,转成“中等”
else if (grade < 80)
return "中等";
//如果成绩在80与90之间,转成“良好”
else if (grade < 90)
return "良好";
//如果成绩在90与100之间,转成“优秀”
else if (grade < 100)
return "优秀";
//其他,转成“错误”
else
return "错误";
}
}
产品代码
写完了,如果别人要使用这个代码,把MyUtil.java
拷给他就可以了。但是作为负责任的你,肯定会担心自己的程序会有Bug。如果别人用自己的代码发现一堆Bugs,那多没面子!怎么办?写了产品代码
,我们还要写测试代码
,证明自己的代码没有问题。Java编程时,程序员对类实现的测试叫单元测试
。类XXXX
的单元测试
,我们一般写建一个XXXXTest
的类,针对MyUtil
类我们写一个MyUtilTest.java
的测试模块,代码如下:
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)
,测试用例
是为某个特殊目标而编制的一组测试输入、执行条件以及预期结果,以便测试某个程序路径或核实是否满足某个特定需求。
其结果还是和上面一样。
运行程序发现负分时与期望不一致,终于找到了一个bug,原因是判断不及格时没有要求成绩大于零。我们修改MyUtil.java
,增加对负分的判断,代码如下:
public class MyUtil{
public static String percentage2fivegrade(int grade){
//如果成绩小于0,转成“错误”
if ((grade < 0))
return "错误";
//如果成绩小于60,转成“不及格”
else if (grade < 60)
return "不及格";
//如果成绩在60与70之间,转成“及格”
else if (grade < 70)
return "及格";
//如果成绩在70与80之间,转成“中等”
else if (grade < 80)
return "中等";
//如果成绩在80与90之间,转成“良好”
else if (grade < 90)
return "良好";
//如果成绩在90与100之间,转成“优秀”
else if (grade < 100)
return "优秀";
//如果成绩大于100,转成“错误”
else
return "错误";
}
}
结果还是test passed!
通过。
测试够了吗?还不够,一般代码在边界处最容易出错,我们还没有测试边界情况,我们对输入为“0,60,70,80,90,100”这些边界情况进行测试的代码如下:
public class MyUtilTest {
public static void main(String[] args) {
//测试边界情况
if(MyUtil.percentage2fivegrade(0) != "不及格")
System.out.println("test failed 1!");
else if(MyUtil.percentage2fivegrade(60) != "及格")
System.out.println("test failed 2!");
else if(MyUtil.percentage2fivegrade(70) != "中等")
System.out.println("test failed 3!");
else if(MyUtil.percentage2fivegrade(80) != "良好")
System.out.println("test failed 4!");
else if(MyUtil.percentage2fivegrade(90) != "优秀")
System.out.println("test failed 5!");
else if(MyUtil.percentage2fivegrade(100) != "优秀")
System.out.println("test failed 6!");
else
System.out.println("test passed!");
}
}
这同eclipse环境下出现的结果一样。
我们发现边界情况中输入100时有一个Bug。我们修改MyUtil.java
,把判断优秀的条件中包含输入为100的情况,代码如下:
public class MyUtil{ public static String percentage2fivegrade(int grade){ //如果成绩小于0,转成“错误” if ((grade < 0)) return "错误"; //如果成绩小于60,转成“不及格” else if (grade < 60) return "不及格"; //如果成绩在60与70之间,转成“及格” else if (grade < 70) return "及格"; //如果成绩在70与80之间,转成“中等” else if (grade < 80) return "中等"; //如果成绩在80与90之间,转成“良好” else if (grade < 90) return "良好"; //如果成绩在90与100之间,转成“优秀” else if (grade <= 100) return "优秀"; //如果成绩大于100,转成“错误” else return "错误"; } }
符合预期。成功。
(2) TDD(Test Driven Devlopment, 测试驱动开发)
这种先写测试代码
,然后再写产品代码
的开发方法叫“测试驱动开发”(TDD)。TDD的一般步骤如下:
- 明确当前要完成的功能,记录成一个测试列表
- 快速完成编写针对此功能的测试用例
- 测试代码编译不通过(没产品代码呢)
- 编写产品代码
- 测试通过
- 对代码进行重构,并保证测试通过(重构下次实验练习)
- 循环完成所有功能的开发
基于TDD,我们不会出现过度设计的情况,需求通过测试用例表达出来了,我们的产品代码
只要让测试通过就可以了。 Java中有单元测试工具JUnit来辅助进行TDD,我们用TDD的方式把前面百分制转五分制的例子重写一次,体会一下有测试工具支持的开发的好处。 打开Eclipse
,单击File->New->Java Project
新建一个TDDDemo
的Java项目
经过一系列的创建,得到如下界面。
在这儿我把全部截屏都发在这里。
这个是成功的结果。
这里我出现了一个问题,就是dog's color是空的,没有颜色,还没解决
我们增加第一个测试用例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)); } }
测试结果出现了一个绿条(green bar),说明测试通过了。TDD的目标是"Clean Code That Works",TDD的slogan是"Keep the bar green, to Keep the code clean",大家体会一下。
TDD的编码节奏是:
- 增加测试代码,JUnit出现红条
- 修改产品代码
- JUnit出现绿条,任务完成
(二)面向对象三要素
(1)抽象
抽象一词的本意是指人在认识思维活动中对事物表象因素的舍弃和对本质因素的抽取。抽象是人类认识复杂事物和现象时经常使用的思维工具,抽象思维能力在程序设计中非常重要,"去粗取精、化繁为简、由表及里、异中求同"的抽象能力很大程度上决定了程序员的程序设计能力。
抽象就是抽出事物的本质特征而暂时不考虑他们的细节。对于复杂系统问题人们借助分层次抽象的方法进行问题求解;在抽象的最高层,可以使用问题环境的语言,以概括的方式叙述问题的解。在抽象的较低层,则采用过程化的方式进行描述。在描述问题解时,使用面向问题和面向实现的术语。 程序设计中,抽象包括两个方面,一是过程抽象,二是数据抽象。
为了避免输出一个重复的数字,避免粘贴又粘贴,可以直接写println代码:
public void printn(int n){ for(int i=1; i<=n; i++) System.out.println(n); }
(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中用类进行封装,比如一个Dog类:
public class Dog { private String color; public String getColor() { return color; } public void setColor(String color) { this.color = color; } public String bark(){ return "汪汪"; } public String toString(){ return "The Dog's color is " + this.getColor() +", and it shouts "+ this.bark() + "!"; } }
Dog
类通过使用类和访问控制(private,public)隐藏了属性color
,开放了接口setColor()
,getColor()
,bark()
和toString
。Dog
类是一个模块,我们可以通过下面的代码使用它,测试代码与运行结果如下:
我们可以看到,在UML 里,一个类的属性能显示它的名字,类型,初始化值,属性也可以显示private,public,protected。 类的方法能显示它们的方法名,参数,返回类型,以及方法的private,public,protected属性。其中:
- +表示public
- #表示 protected
- -表示 private
使用UML可以让我们不必关注细节。同样,我们可以建立一个Cat
类(请大家模仿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
,
用户现大需要一个Triangle
模块是一个合理的要求,由于我们使用了多态,原先的模块不需要改变,只要新增加一个模块Triangle
就可以了,如下图所示:
这个图形系统是符合OCP
原则的。
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
- 子类必须可以被其基类所代
- 使用指向基类的指针或引用的函数,必须能够在不知道具体派生类对象类型的情况下使用它
LSP
的核心思想是父类型对象可以被子类型对象所取代。我们前面举的Animal
,Dog
,Cat
的那个例子是符合LSP
原则的。
这是反例,是错误的
LSP
告诉大家的一点是不要滥用继承,LSP
原则清楚地指出,OOD中“ISA关系”是就行为功能而言。行为功能(behavior)不是内在的、私有的,而是外在、公开的,是客户程序所依赖的接口。
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)
GOF的23个设计模式
Java类库中大量使用设计模式:
- Factory:java.util.Calendar
- Compsite:java.awt.Container
- Decorator:java I/0
- Iterator:java.util.Enumeration
- Strategy:java.awt.LayoutManager
- …
要支持Float
类,Document
类要修改构造方法,这还违反了OCP
原则。封装、继承、多态解决不了问题了,这时需要设计模式了:
对应的代码如下:
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(); } }
除SOLID原则外还有很多其它的面向对象原则。如:
- "组合替代继承":这是说相对于继承,要更倾向于使用组合;
- "笛米特法则":这是说"你的类对其它类知道的越少越好";
- "共同封闭原则":这是说"相关类应该打包在一起";
- "稳定抽象原则":这是说"类越稳定,越应该由抽象类组成";
当然,这些原则并不是孤立存在的,而是紧密联系的,遵循一个原则的同时也就遵循了另外一个或多个原则;反之,违反了其中一个原则也很可能同时就违反了另外一个或多个原则。 设计模式是这些原则在一些特定场景的应用结果。因此,可以把设计模式看作"框架",把OOD原则看作"规范"。 在学习设计模式的过程中,要经常性的反思,这个设计模式体现了面向对象设计原则中的哪个或哪一些原则。
(四)练习
1使用TDD的方式设计关实现复数类Complex。
2.实验报告中统计自己的PSP(Personal Software Process)时间
PSP时间我不是很会算。但有做的记录。
步骤 耗时 百分比 需求分析 30 设计 79 代码实现 59 测试 68 分析总结
总结单元测试的好处:
我觉得单元测试就是为了方便顾客,也方便程序员本身以后查找问题的快速。
posted on 2015-05-07 20:27 20135239益西拉姆 阅读(174) 评论(0) 编辑 收藏 举报