2017-2018-2 20165315 实验二《Java面向对象程序设计》实验报告
一、实验内容及步骤
1.初步掌握单元测试和TDD
- 单元测试
任务一:三种代码
用程序解决问题时,要学会写以下三种代码:
- 伪代码
- 产品代码
- 测试代码
TDD(测试驱动开发):
- 伪代码(思路)
- 测试代码(产品预期功能)
- 产品代码(实现预期功能)
TDD的一般步骤如下:
- 明确当前要完成的功能,记录成一个测试列表
- 快速完成编写针对此功能的测试用例
- 测试代码编译不通过(没产品代码呢)
- 编写产品代码
- 测试通过
- 对代码进行重构,并保证测试通过(重构下次实验练习)
- 循环完成所有功能的开发
基于TDD,可以有效避免过度开发的现象,因为我们只需要让测试通过即可。
测试类的创建:
- 将鼠标放在主类类名上,点击出现的黄色小灯泡,在下拉选项中选择
Create Test
- 随后在
Testing Library
中的下拉选项中选择Junit 3
,点击OK
便可创建Test
类 - 注意:不要忘记
@Test
的书写
举例:我们要在一个MyUtil
类中解决一个百分制成绩转成“优、良、中、及格、不及格”五级制成绩的功能。
测试结果:
任务二:TDD(Test Driven Devlopment, 测试驱动开发)
老师在教程里给出的程序如下:
public static void main(String [] args){
StringBuffer buffer = new StringBuffer();
buffer.append('S');
buffer.append("tringBuffer");
System.out.println(buffer.charAt(1));
System.out.println(buffer.capacity());
System.out.println(buffer.length());
System.out.println(buffer.indexOf("tring"));
System.out.println("buffer = " + buffer.toString());
对于这个程序,有五个方法需要测试,分别是:
-
charAt(int i)
:返回此序列中指定索引处的 char 值。第一个 char 值在索引 0 处,第二个在索引 1 处,依此类推,这类似于数组索引 -
capacity()
:返回当前容量。容量指可用于最新插入的字符的存储量,超过这一容量就需要再次进行分配 -
length()
:返回子浮窗的长度 -
indexOf(String s)
:返回输入的子字符串的第一个字母在母字符串的位置 -
toString(String s)
:返回此对象本身(它已经是一个字符串)
在产品代码里,我们需要为这五个方法加上返回值,并与我们的断言进行比较。产品代码如下:
public class StringBufferDemo {
StringBuffer buffer;
public static char CharAt(StringBuffer buffer, int index) {
return buffer.charAt(index);
}
public static int Capacity(StringBuffer buffer) {
return buffer.capacity();
}
public static int IndexOf(StringBuffer buffer, String str) {
return buffer.indexOf(str);
}
public static String ToString(StringBuffer buffer) {
return "buffer = " + buffer.toString();
}
public static int Length(StringBuffer buffer) {
return buffer.length();
}
}
根据上述该产品代码,写出对应的测试类,在测试类中我分别都使使用了3个例子来进行测试,如果出现问题,JUnit
会出现红条,IDEA会提示哪一个测试用例出现问题,由此可以对应改正产品代码中的问题,直到JUnit出现绿条,任务完成。
测试代码如下:
import junit.framework.TestCase;
import org.junit.*;
public class StringBufferDemoTest extends TestCase {
StringBuffer buffer1 = new StringBuffer("iamastudent");
StringBuffer buffer2 = new StringBuffer("youareastudent");
StringBuffer buffer3 = new StringBuffer("heisateacher");
@Test
public void testCharAt() {
assertEquals('i', StringBufferDemo.CharAt(buffer1, 0));
assertEquals('o', StringBufferDemo.CharAt(buffer2, 1));
assertEquals('r', StringBufferDemo.CharAt(buffer3, 11));
}
@Test
public void testCapital() {
assertEquals(27, StringBufferDemo.Capacity(buffer1));
assertEquals(30, StringBufferDemo.Capacity(buffer2));
assertEquals(28, StringBufferDemo.Capacity(buffer3));
}
@Test
public void testLenght() throws Exception {
assertEquals(11, StringBufferDemo.Length(buffer1));
assertEquals(14, StringBufferDemo.Length(buffer2));
assertEquals(12, StringBufferDemo.Length(buffer3));
}
@Test
public void testIndexOf() {
assertEquals(0, StringBufferDemo.IndexOf(buffer1, "iam"));
assertEquals(-1, StringBufferDemo.IndexOf(buffer2, "You"));
assertEquals(11, StringBufferDemo.IndexOf(buffer3, "r"));
}
@Test
public void testToString() {
assertEquals("buffer = iamastudent", StringBufferDemo.ToString(buffer1));
assertEquals("buffer = youareastudent", StringBufferDemo.ToString(buffer2));
assertEquals("buffer = heisateacher", StringBufferDemo.ToString(buffer3));
}
}
截图如下:
2.面向对象三要素:封装、继承、多态
面向对象(Object-Oriented)的三要素包括:封装、继承、多态。面向对象的思想涉及到软件开发的各个方面,如面向对象分析(OOA)、面向对象设计(OOD)、面向对象编程实现(OOP)。OOA根据抽象关键的问题域来分解系统,关注是什么(what)。OOD是一种提供符号设计系统的面向对象的实现过程,用非常接近问题域术语的方法把系统构造成“现实世界”的对象,关注怎么做(how),通过模型来实现功能规范。OOP则在设计的基础上用编程语言(如Java)编码。贯穿OOA、OOD和OOP的主线正是抽象。
OOD中建模会用图形化的建模语言UML(Unified Modeling Language),UML是一种通用的建模语言。
过程抽象的结果是函数,数据抽象的结果是抽象数据类型(Abstract Data Type,ADT),类可以作具有继承和多态机制的ADT。数据抽象才是OOP的核心和起源。
OO三要素的第一个要素是封装,封装就是将数据与相关行为包装在一起以实现信息就隐藏,Java中用类进行封装。
封装实际上使用方法(method)将类的数据隐藏起来,控制用户对类的修改和访问数据的程度,从而带来模块化(Modularity)和信息隐藏(Information hiding)的好处;接口(interface)是封装的准确描述手段。
任务三:使用StarUML对实验二中的代码进行建模
- 点开
StarUML
程序后,在右下角的选项中选择Class
选项建立一个类的UML图
- 按照下图中提示分别创建对应的变量层和方法层
- 在右下角选项中选择
Generalization
选项,创建类与类之间的调用关系
最终结果截图:
- 较为简单:
- 较为复杂:
3.设计模式
面向对象三要素是“封装、继承、多态”,任何面向对象编程语言都会在语法上支持这三要素。如何借助抽象思维用好三要素特别是多态还是非常困难的,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,依赖倒置原则)
任务四:对MyDoc类进行扩充,让其支持Long类,初步理解设计模式
OCP是OOD中最重要的一个原则,OCP的内容是:
软件实体(类,模块,函数等)应该对扩充开放,对修改封闭。
OCP可以用以下手段实现:
- 抽象和继承
- 面向接口编程
老师给出的以Int
型为例的代码如下:
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();
}
}
在上述代码的基础上,要求系统支持Long
类,这是一个合理的要求,要支持Long
类,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);
}
}
class Long extends Data {
long value;
Long() {
value = 111111111;
}
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();
}
}
class LongFactory extends Factory {
public Data CreateDataObject() {
return new Long();
}
}
//Client classes
class Document {
Data pd;
Document(Factory pf) {
pd = pf.CreateDataObject();
}
public void DisplayData() {
pd.DisplayValue();
}
}
//Test class
public class MyDoc {
static Document d1, d2;
public static void main(String[] args) {
d1 = new Document(new IntFactory());
d2 = new Document(new LongFactory());
d1.DisplayData();
d2.DisplayData();
}
}
运行结果截图:
4.练习
任务五:以TDD的方式开发一个复数类Complex
- 伪代码
// 定义属性并生成getter,setter
double RealPart;
double ImagePart;
// 定义构造函数
public Complex()
public Complex(double R,double I)
//Override Object
public boolean equals(Object obj)
public String toString()
// 定义公有方法:加减乘除
Complex ComplexAdd(Complex a)
Complex ComplexSub(Complex a)
Complex ComplexMulti(Complex a)
Complex ComplexDiv(Complex a)
- 产品代码
public class Complex {
// 定义属性并生成getter,setter
private double RealPart;
private double ImagePart;
public double getterRealPart() {
return this.RealPart;
}
public double getterImagePart() {
return this.ImagePart;
}
public static double getterRealPart(double RealPart) {
return RealPart;
}
public static double getterImagePart(double ImagePart) {
return ImagePart;
}
public void setterRealPart(double RealPart) {
this.RealPart = RealPart;
}
public void setterImagePart(double ImagePart) {
this.ImagePart = ImagePart;
}
// 定义构造函数
public Complex() {
this.RealPart = 0;
this.ImagePart = 0;
}
public Complex(double R, double I) {
this.RealPart = R;
this.ImagePart = I;
}
//Override Object
public boolean equals(Object obj) {
Complex complex = (Complex) obj;
if (this == obj) {
return true;
} else if (!(obj instanceof Complex)) {
return false;
} else if (getterRealPart() != complex.getterRealPart()) {
return false;
} else if (getterImagePart() != complex.getterImagePart()) {
return false;
} else
return true;
}
public String toString() {
if (getterRealPart() == 0)
return getterImagePart() + "i";
else if (getterImagePart() == 0)
return getterRealPart() + "";
else if (getterImagePart() < 0)
return getterRealPart() + "" + getterImagePart() + "i";
else
return getterRealPart() + "+" + getterImagePart() + "i";
}
// 定义公有方法:加减乘除
Complex ComplexAdd(Complex a) {
return new Complex(this.getterRealPart() + a.getterRealPart(), getterImagePart() + a.getterImagePart());
}
Complex ComplexSub(Complex a) {
return new Complex(this.getterRealPart() - a.getterRealPart(), getterImagePart() - a.getterImagePart());
}
Complex ComplexMulti(Complex a) {
return new Complex(this.getterRealPart() * a.getterRealPart() - a.getterImagePart() * this.getterImagePart(), a.getterImagePart() * this.getterRealPart() + a.getterRealPart() * this.getterImagePart());
}
Complex ComplexDiv(Complex a) {
Complex c = new Complex();
if (a.equals(c)) {
System.out.println("错误,分母不能为零!");
}
return new Complex(this.getterRealPart() / a.getterRealPart(), this.getterImagePart() / a.getterImagePart());
}
}
- 测试代码
import junit.framework.TestCase;
import org.junit.Test;
public class ComplexTest extends TestCase {
Complex complex1 = new Complex(3, 4);
Complex complex2 = new Complex(1, -2);
Complex complex3 = new Complex(1, 1);
@Test
public void testgetterRealPart() throws Exception {
assertEquals(3.0, Complex.getterRealPart(3.0));
assertEquals(1.0, Complex.getterRealPart(1.0));
assertEquals(-2.0, Complex.getterRealPart(-2.0));
}
@Test
public void testgetterImagePart() throws Exception {
assertEquals(4.0, Complex.getterImagePart(4.0));
assertEquals(-2.0, Complex.getterImagePart(-2.0));
assertEquals(0.0, Complex.getterImagePart(0.0));
}
@Test
public void testAdd() throws Exception {
assertEquals("4.0+2.0i", complex1.ComplexAdd(complex2).toString());
assertEquals("4.0+5.0i", complex1.ComplexAdd(complex3).toString());
assertEquals("2.0-1.0i", complex2.ComplexAdd(complex3).toString());
}
@Test
public void testSub() throws Exception {
assertEquals("2.0+6.0i", complex1.ComplexSub(complex2).toString());
assertEquals("2.0+3.0i", complex1.ComplexSub(complex3).toString());
assertEquals("-3.0i", complex2.ComplexSub(complex3).toString());
}
@Test
public void testMulti() throws Exception {
assertEquals("11.0-2.0i", complex1.ComplexMulti(complex2).toString());
assertEquals("-1.0+7.0i", complex1.ComplexMulti(complex3).toString());
assertEquals("3.0-1.0i", complex2.ComplexMulti(complex3).toString());
}
@Test
public void testDiv() throws Exception {
assertEquals("3.0-2.0i", complex1.ComplexDiv(complex2).toString());
assertEquals("3.0+4.0i", complex1.ComplexDiv(complex3).toString());
assertEquals("1.0-2.0i", complex2.ComplexDiv(complex3).toString());
}
}
二、实验过程中遇到的问题及解决
- 一开始以为
Test
类是自己手动在Test
目录下创建,导致结果不会出现测试条
解决过程:
1.将鼠标放在主类类名上,点击出现的黄色小灯泡,在下拉选项中选择Create Test
2.随后在Testing Library
中的下拉选项中选择Junit 3
,点击OK
便可创建Test
类
3.注意:不要忘记@Test
的书写
- 在书写测试类时,StringBuffer()中的capacity()方法总是出错
解决方法:
参考capacity()的用法可知,StringBuffer在内部维护一个字符数组,当你使用缺省的构造函数来创建StringBuffer对象的时候, StringBuffer的容量被初始化为16个字符,也就是说缺省容量就是16个字符。当StringBuffer达到最大容 量的时候,它会将自身容量增加到当前的2倍再加2,也就是(2*旧值+2)。
也就是说,对于空字符串,调用capacity()方法初始分配值为16;大小超过16时则扩充容量为34,再次扩充得到70。
三、实验体会与总结
我通过本次实验学会了如何编写测试代码、如何绘画UML图以及在TDD模式下编写代码。
在自己上手实践操作过程中,加深了对平时不清楚的知识点的理解,也掌握了的Junit的用法。在使用测试代码的时候,既可以测试到代码是否正确,又规范了编程习惯,单元测试提供了一种高效快速的测试代码正确性的方法。
四、PSP(Personal Software Process)时间
步骤 | 耗时 | 百分比 |
---|---|---|
步骤 | 耗时 | 百分比 |
需求分析 | 20min | 12% |
设计 | 30min | 18% |
代码实现 | 50min | 30% |
测试 | 30min | 18% |
分析总结 | 40min | 22% |
五、代码链接
https://gitee.com/BESTI-IS-JAVA-2018/ch1/tree/master/20165315exp2