面向对象设计原则实验

实验目的

  1. 通过实例深入理解和掌握所学的面向对象设计原则
  2. 熟练使用面向对象设计原则对系统进行重构
  3. 熟练绘制重构后的结构图(类图)

实验内容

练习1

在某绘图软件中提供了多种大小不同的画笔(Pen),并且可以给画笔指定不同颜色,某设计人员针对画笔的结构设计了如下图所示的初始类图

1

通过仔细分析,设计人员发现该类图存在非常严重的问题,即如果需要增加一种新的大小或颜色的笔,就需要增加很多子类,例如增加一种绿色的笔,则对应每一种大小的笔都需要增加一支绿色的笔,系统中类的个数急剧增加

试根据依赖倒转原则和合成复用原则对该设计方案进行重构,使得增加新的大小或颜色的笔都较为方便,请绘制重构之后的结构图(类图)

练习2

在某公司财务系统的初始设计方案中存在下图所示的Employee类

2

该类包含员工编号(ID)、姓名(name)、年龄(age)、性别(gender)、薪水(salary)、每月工作时数(workHoursPerMonth)、每月请假天数(leaveDaysPerMonth)等属性。该公司的员工包括全职和兼职两类,其中每月工作时数用于存储兼职员工每个月工作的小时数,每月请假天数用于存储全职员工每个月请假的天数。系统中两类员工计算工资的方法也不一样,全职员工按照工作日数计算工资,兼职员工按照工作时数计算工资,因此在Employee类中提供了两个方法calculateSalaryByDays()calculateSalaryByHours(),分别用于按照天数和按照时数计算工资,此外,还提供了方法displayalary()用于显示工资

试采用所学面向对象设计原则分析上图中Employee类存在的问题并对其进行重构,绘制重构之后的类图

练习3

在某图形界面中存在如下代码片段,组件类之间有较为复杂的相互引用关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
//按钮类
public class Button {
private List list;
private ComboBox cb;
private TextBox tb;
private Label label;
//...
public void change() {
list.update();
cb.update();
tb.update();
label.update();
}
public void update() {
//...
}
//...
}

//列表框类
public class List {
private ComboBox cb;
private TextBox tb;
//...
public void change() {
cb.update();
tb.update();
}
public void update() {
//...
}
//...
}

//组合框类
public class ComboBox {
private List list;
private TextBox tb;
//...
public void change() {
list.update();
tb.update();
}
public void update() {
//...
}
//...
}

//文本框类
public class TextBox {
private List list;
private ComboBox cb;
//...
public void change() {
list.update();
cb.update();
}
public void update() {
//...
}
//...
}

//文本标签类
public class Label {
//...
public void update() {
//...
}
//...
}

如果在上述系统中增加一个新的组件类,则必须修改与之交互的其他组件类的源代码,将导致多个类的源代码需要修改

基于上述代码,请结合所学知识完成以下两道练习题:

  1. 绘制上述代码对应的类图
  2. 根据迪米特法则对所绘制的类图进行重构,以降低组件之间的耦合度,绘制重构后的类图

练习4

在某图形库API中提供了多种矢量图模板,用户可以基于这些矢量图创建不同的图形,图形库设计人员设计的初始类图如下所示

3

在该图形库中,每个图形类(例如Circle、Triangle等)的init()方法用于初始化所创建的图形,setColor()方法用于给图形设置边框颜色,fill()方法用于给图形设置填充颜色,setSize()方法用于设置图形的大小,display()方法用于显示图形

用户在客户类(Client)中使用该图形库时发现存在如下问题:

  1. 由于在创建窗口时每次只需要使用图形库中的一种图形,因此在更换图形时需要修改客户类源代码
  2. 在图形库中增加并使用新的图形时,需要修改客户类源代码
  3. 客户类在每次使用图形对象之前需要先创建图形对象,有些图形的创建过程较为复杂,导致客户类代码冗长且难以维护

现需要根据面向对象设计原则对该系统进行重构,要求如下:

  1. 隔离图形的创建和使用,将图形的创建过程封装在专门的类中,客户类在使用图形时无须直接创建图形对象,甚至不需要关心具体图形类类名
  2. 客户类能够方便地更换图形或使用新增图形,无须针对具体图形类编程,符合开闭原则

请绘制重构后的结构图(类图)

实验要求

  1. 选择合适的面向对象设计原则对系统进行重构
  2. 绘制重构之后的类图

实验步骤

  • 练习1:分析初始设计方案存在的问题,根据依赖倒转原则和合成复用原则对初始设计方案进行重构,绘制重构之后的结构图(类图)
  • 练习2:采用所学面向对象设计原则分析初始设计方案中存在的问题并对其进行重构,绘制重构之后的类图
  • 练习3:绘制初始代码对应的类图,再根据迪米特法则对所绘制的类图进行重构,绘制重构后的类图
  • 练习4:分析初始设计方案存在的问题,根据面向对象设计原则进行重构并绘制重构后的结构图(类图)

实验结果

练习1:需要绘制重构之后的结构图(类图)

初始设计方案中,如果要添加一种新的大小或者颜色的笔,需要增加很多子类。例如新增蓝色笔,就需要在小型笔的类下,中型笔的类下以及大型笔的类下分别增加蓝色笔,种类数量提高后,会造成系统中类的个数急剧增加

根据依赖倒转原则和合成复用原则,应把相应型号,色号的类都改为接口,使用或添加任何一种笔时通过不同接口和类的组合关系来实现。类图如下所示

4

pen同时实现sizecolor两个接口,其中size接口由相应表示大小的类来实现,例如smallmiddle以及largecolor接口由相应表示颜色的类来实现,例如blackred

1
2
3
4
5
6
7
8
public class pen implements size,color {}
public interface size {}
public interface color {}
public class small implements size {}
public class middle implements size {}
public class large implements size {}
public class black implements color {}
public class red implements color {}

练习2:需要绘制重构之后的结构图(类图)

首先初始设计方案违反了单一职责原则,应该控制类的粒度大小,将对象解耦从而提高内聚性。一个员工不可能同时既是全职又是兼职,所以应将Employee类拆分为多个员工类或继承于多个员工子类。Employee类中只存放所有员工种类的共有属性以及方法,各种员工的私有属性以及方法存放在相应的子类中,类图如下所示

5

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Employee {
private String ID;
private String name;
private int age;
private String gender;
private double salary;
public void displaySalary();
}
public class FullEmployee extends Employee {
private int leaveDaysPerMonth;
public double calculateSalaryByDays();
}
public class PartEmployee extends Employee {
private int workHoursPerMonth;
public double calculateSalaryByHours();
}

练习3:需要绘制初始代码对应的类图和重构之后的类图

6

上图为初始代码对应的类图,可以看出,该系统结构较为复杂且耦合度较高,组件与组件之间密切相关,在类中相互调用。每个组件不能够单独使用,造成了低耦合,高内聚的状态。若一个组件发生变化,则与之相关的其它组件也需要进行处理和调整,形成了一种极为复杂的网状结构。若要新增或删除某一组件,需要对系统内部多个组件类进行修改,这违反了开闭原则,严重降低了整个系统的可扩展性。

根据迪米特法则(只与你直接的朋友交谈,不跟陌生人说话)对系统进行重构,使用中介者模式,首先创建中介者AbstractMediator抽象类及其ConcreteMediator实现类。再创建Component类作为ButtonListComboBoxTextBox以及Label类的共同父类。类图如下所示

8

练习4:需要绘制重构之后的结构图(类图)

由于在创建窗口时每次只需要使用图形库中的一种图形,因此在更换图形时需要修改客户类源代码;在图形库中增加并使用新的图形时,需要修改客户类源代码;客户类在每次使用图形对象之前需要先创建图形对象,有些图形的创建过程较为复杂,导致客户类代码冗长且难以维护

隔离图形的创建和使用,将图形的使用过程和创建过程拆开各自封装,重构后的类图如下

7

采用工厂模式进行重构,将图形的创建封装为接口,具体某一图形的创建由实现类来决定,解决了上述的问题,并且重构后符合开闭原则以及单一职责原则

实验小结

请总结本次实验的体会,包括学会了什么、遇到哪些问题。如何解决这些问题以及存在哪些有待改进的地方

在做一个项目之前的需求分析过程中就应该构思好整个系统的完整架构,运用的模式,以及类与类,类与接口之间的关系,以免造成不必要的麻烦。设计时要尽可能符合七大开发原则。对扩展开发而对修改关闭;面向接口编程,养成封装类以及使用抽象方法的习惯;控制每个类的粒度大小,尽量符合高内聚,低耦合的特性;确保父类所拥有的性质在子类中仍然成立;给每个类都建立他们需要的专用接口;每个类尽量只与具有直接关系的类进行交互;尽量使用组合或聚合的关联关系来实现系统的具体功能

在练习1中,根据依赖倒转原则以及合成复用原则,通过组合的方式来实现添加一种新的类,避免造成系统中类的个数急剧增加的情况

在练习2中,根据单一职责原则对类进行解耦拆分,控制类的粒度大小,从而将对象解耦提高内聚性。通过将类分为多个子类,仅将共有属性放在父类中,将私有属性放在子类中,保证了系统高内聚、低耦合的特性

在练习3中,根据迪米特法则,使用中介者模式,对复杂且耦合度高的系统进行处理和调整,类之间的交互通过中介者来完成,即仅与直接相关类活动,不与陌生类产生关系。处理后将原本复杂的网状结构简化,增加了系统的可扩展性,符合开闭原则,保证了整个系统高内聚、低耦合的特性

在练习4中,根据单一职责原则,依赖倒转原则,使用工厂模式对系统进行重构,解决了原本客户端代码冗长且难以维护的问题。通过工厂实现类来创建相应图形,用户可在直接使用图形的同时,无需了解系统的实现细节。重构后同时符合开闭原则