【设计模式】面向对象的设计原则

(一) UML 和面向对象设计原则

1. 一种某唱片播放器不仅可以播放唱片,而且可以连接计算机并把计算机中的歌曲刻录到

唱片上(同步歌曲)。连接计算机的过程中还可自动完成充电。

关于唱片,还有如下描述信息:

(1) 每首歌曲的描述信息包括:歌曲的名字,谱写这首歌曲的艺术家以及演奏这首

歌曲的艺术家。只有两首歌曲的这三部分信息完全相同时,才认为它们是同一

首歌曲。艺术家可能是一名歌手或一支由 2 人或 2 名以上的歌手组成的乐队。

一名歌手可以不属于任何乐队,也可以属于一个或多个乐队。

(2) 每章唱片有多条音轨构成;一条音轨中只包含一首歌曲或为空,一首歌曲可分

布在多条音轨上,通一首歌曲在一张唱片中最多只能出现一次。

(3) 每条音轨都有一个开始位置和持续时间。一张唱片中音轨的次序是非常重要的,

因此对于任意一条音轨,播放器需要准确地指导它的下一条音轨和上一条音轨

是什么(如果存在的话)。

根据上述描述,采用面向对象方法对其进行分析或设计,得到了如表 1-3 所示的

类列表和如图 1-16 所示的初始类图。

表 1-3 类列表

类名 说明 类名 说明

Artist 艺术家 Musician 歌手

Song 歌曲 Track 音轨

Band 乐队 Album 唱片

【问题 1】根据题干中的描述,使用表 1-3 给出的类的名称,给图 1-16 中 A~F 所对应的类。

【问题 2】根据题干中的描述,给出图 1-16 中(1)~(6)处的多重性。

【问题 3】图 1-16 中缺少了一条关联,请指出这条关联两端所对应的类以及每一端的多重

性。

2. 在某绘图软件中提供了多种大小不同的画笔(Pen),并且可以给画笔指定不同颜色,某

设计人员针对画笔的结构设计了如下类图:

通过仔细分析,设计人员发现该类图存在非常严重的问题,如果需要增加一种新的大

小的笔或者增加一种新的颜色,都需要增加很多子类,如增加一种绿色,则对应每一种大小

的笔都需要增加一支绿色笔,系统中类的个数急剧增加。

试根据依赖倒转原则和合成复用原则对该设计方案进行重构,使得增加新的大小的笔

和增加新的颜色都较为方便。

本练习可以通过依赖倒转原则和合成复用原则进行重构,重构方案如下所示:

    在本重构方案中,将笔的大小和颜色设计为两个继承结构,两者可以独立变化,根据依赖倒转原则,建立一个抽象的关联关系,将颜色对象注入到画笔中;再根据合成复用原则,画笔在保持原有方法的同时还可以调用颜色类的方法,保持原有性质不变。如果需要增加一种新的画笔或增加一种新的颜色,只需对应增加一个具体类即可,且客户端可以针对高层类Pen和Color编程,在运行时再注入具体的子类对象,系统具有良好的可扩展性,满足开闭原则。(注:本重构方法即为桥接模式,在第4章将对该模式进行进一步讲解并提供实例代码来实现该模式。

也可以进一步将Size进行和Color一样的处理,单独封装,继承产生子类,最后组合到Pen当中。

  1. 结合面向对象设计原则分析:正方形是否是长方形的子类?

根据面向对象设计中的Liskov Substitution Principle(里氏替换原则),如果正方形是长方形的子类,则正方形应该可以替换长方形在任何情况下使用,而不会导致错误或异常。

然而,在现实世界中,一个正方形具有长和宽两个相等的边,而长方形具有两组相等的对边。因此,正方形和长方形之间存在本质上的差异,不能完全等同。例如,如果我们编写了一个函数,该函数需要接受一个长方形作为参数,并根据长方形的参数计算面积。如果我们将一个正方形作为参数传递给该函数,则可能会产生错误的结果,因为正方形的两条边可能被错误地视为不同的值,并导致面积计算错误。

因此,可以得出结论:正方形不应该是长方形的子类,因为它们的属性和行为不完全相同,并且不满足LSP原则。虽然正方形和长方形之间存在继承关系,但这种关系是不合适的。正确的方法是将它们定义为两个独立的类,每个类应该只定义适用于自己的属性和方法。

长方形和正方形之间的关系可以通过共同继承一个抽象类或接口来表示,该抽象类或接口定义共同的属性和行为。例如,我们可以定义一个名为“四边形”的抽象类或接口,其中包含一个计算面积的方法。然后,我们可以创建一个名为“长方形”的类和一个名为“正方形”的类,它们都继承自“四边形”类或实现“四边形”接口,并且在自己的类中实现各自对应的属性和方法。

这样设计既满足了开闭原则(应用程序对扩展开放,对修改关闭),也遵循了里氏替换原则,即子类可以完全替代父类。因为“长方形”和“正方形”都是“四边形”的子类,所以可以将它们视为同一种类型的对象,而不会导致任何错误或异常

4. 在某公司财务系统的初始设计方案中存在如图 1-2 所示的 Employee 类,该类包含员工

编号(ID)、姓名(name)、年龄(age)、性别(gender)、薪水(salary)、每月工作时数

(workHoursPerMonth)、每月请假天数(leaveDaysPerMonth)等属性。该公司的员工包括

全职和兼职两类,其中每月工作时数用于存储兼职员工每个月工作的小时数,每月请假天数

用于存储全职员工每个月请假的天数。系统中两类员工计算工资的方法也不一样,全职员工

按照工作日数计算工资,兼职员工按照工作时数计算工资,因此在 Employee 类中提供了两

个方法 calculateSalaryDays()和 calculateSalaryHours(),分别用于按照天数和时数计算工

资,此外,还提供了方法 displaySalary()用于显示工资。

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

根据所提供的问题描述,对图1-2中的Employee类进行分析,存在以下问题:

单一职责原则:Employee类承担了过多的职责,包括保存员工信息、计算工资和显示工资等。应该将这些不同的职责进行解耦和分离。

开放封闭原则:系统应该对扩展开放,对修改封闭。但是在原设计中,新增不同类型员工时,需要修改Employee类的方法来适应不同的计算工资方式,违反了开放封闭原则。

继承关系:全职员工和兼职员工都是员工的特殊类型,但是在原设计中并没有明确体现出继承关系。

基于上述问题,可以进行如下的重构:

创建一个基类Employee,包含共同的属性(员工编号、姓名、年龄、性别)和方法(显示员工信息等)。

在Employee类中,定义一个抽象方法calculateSalary()用于计算工资。不再区分全职员工和兼职员工的计算方式,而是将计算工资的逻辑留给具体的子类实现。

创建两个子类:FullTimeEmployee(全职员工)和PartTimeEmployee(兼职员工)。这两个子类继承Employee类,并实现calculateSalary()方法来根据自己的特定规则计算工资。

在FullTimeEmployee类中添加一个新属性leaveDaysPerMonth,用于存储全职员工每个月请假的天数。

在PartTimeEmployee类中添加一个新属性workHoursPerMonth,用于存储兼职员工每个月工作的小时数。

通过这样的重构,可以解决原设计中违反单一职责原则和开放封闭原则的问题,并明确了全职员工和兼职员工的继承关系。

重构后的类图如下所示:

问题分析

在某公司财务系统的初始设计方案中存在一个名为Employee类的类,它包含员工编号(ID)、姓名(name)、年龄(age)、性别(gender)、薪水(salary)、每月工作时数(workHoursPerMonth)、每月请假天数(leaveDaysPerMonth)等属性。该公司的员工有两种类型:全职和兼职,其中每月工作时数用于存储兼职员工每个月工作的小时数,每月请假天数用于存储全职员工每个月请假的天数。根据员工类型的不同,计算工资的方法也不一样。全职员工按照工作日数计算工资,兼职员工按照工作时数计算工资。因此,在Employee类中提供了两个方法calculateSalaryDays()和calculateSalaryHours(),分别用于按照天数和时数计算工资。此外,还提供了方法displaySalary()用于显示工资。

我们需要使用面向对象设计原则分析Employee类存在的问题,并对其进行重构,绘制重构后的类图。

解决方案

单一职责原则(SRP) - Employee类违反SRP,因为它具有太多的职责,如存储员工数据、计算工资和显示工资等。因此,我们可以为每个职责创建单独的类,以实现高内聚和低耦合。

开闭原则(OCP) - Employee类没有任何抽象或接口,因此违反了OCP。因此,我们需要使用抽象或接口来允许扩展而无需修改源代码。

里氏替换原则(LSP) - Employee类不违反LSP,因为它是一个具体类,没有继承关系。

接口隔离原则(ISP) - Employee类违反ISP,因为其方法calculateSalaryDays()、calculateSalaryHours()和displaySalary()之间没有关联。因此,我们可以根据功能将这些方法分成不同的接口。

依赖倒置原则(DIP) - Employee类违反DIP,因为它依赖于低级模块(薪资计算)和高级模块(显示薪资)。因此,我们可以创建抽象或接口来减少这些模块之间的耦合。

根据这些原则,我们可以重构Employee类如下:

创建一个基本接口IEmployee,其中声明三个方法:getSalary()、getName()和getID()。

创建两个具体类FullTimeEmployee和PartTimeEmployee,它们都继承自IEmployee。FullTimeEmployee类将有一个属性用于存储每月请假天数,而PartTimeEmployee类将有一个属性用于存储每月工作的小时数。

为基于工作日和工作时数计算工资的方法分别定义两个单独的接口IDaysSalaryCalculator和IHoursSalaryCalculator。这些接口将具有一个方法calculateSalary(),该方法以FullTimeEmployee或PartTimeEmployee实例作为输入,并返回计算出的工资作为输出。

在一个具体类DaysSalaryCalculator中实现IDaysSalaryCalculator接口,用于计算每月请假天数的全职员工的薪水。

在一个具体类HoursSalaryCalculator中实现IHoursSalaryCalculator接口,用于计算每月工作时数的兼职员工的薪水。

创建一个SalaryDisplay类,它具有一个方法displaySalary(IEmployee employee),用于显示任何类型的员工的薪水。

5. 在某图形库 API 中提供了多种矢量图模板,用户可以基于这些矢量图创建不同的显示

图形,图形库设计人员设计的初始类图如下所示:

在该图形库中,每个图形类(如 Circle、Triangle 等)的 init()方法用于初始化所创建的图形,

setColor()方法用于给图形设置边框颜色,fill()方法用于给图形设置填充颜色,setSize()方法

用于设置图形的大小,display()方法用于显示图形。

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

(1) 由于在创建窗口时每次只需要使用图形库中的一种图形,因此在更换图形时需要修

改客户类源代码;

(2) 在图形库中增加并使用新的图形时需要修改客户类源代码;

(3) 客户类在每次使用图形对象之前需要先创建图形对象,有些图形的创建过程较为复

杂,导致客户类代码冗长且难以维护。

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

(1) 隔离图形的创建和使用,将图形的创建过程封装在专门的类中,客户类在使用图形

时无须直接创建图形对象,甚至不需要关心具体图形类类名;

(2) 客户类能够方便地更换图形或使用新增图形,无须针对具体图形类编程,符合开闭

原则

正确答案: 

本练习可以通过单一职责原则和依赖倒转原则进行重构,具体过程可分为如下两步:

(1) 由于图形对象的创建过程较为复杂,因此可以将创建过程封装在专门的类中(这种专门用于创建对象的类称为工厂类),将对象的创建和使用分离,符合单一职责原则;

(2) 引入抽象的图形类Shape,并对应提供一个抽象的创建类,将具体图形类作为 Shape的子类,而具体的图形创建类作为抽象创建类的子类,根据依赖倒转原则,客户端针对抽象图形类和抽象图形创建类编程,而将具体的图形创建类类名存储在配置文件中。

重构之后的类图如下所示:

 

    通过上述重构,在抽象类Creator中声明了创建图形对象的方法create(),在其子类中实现了该方法,用于创建具体的图形对象。客户端针对抽象Creator编程,而将其具体子类类名存储在配置文件config.xml中,如果需要更换图形,只需在配置文件中更改Creator的具体子类类名即可;如果需要增加图形,则对应增加一个新的Creator子类用于创建新增图形对象,再修改配置文件,在配置文件中存储新的图形创建类类名。更换和增加图形都无须修改源代码,完全符合开闭原则。(注:本重构方法即为工厂方法模式,在第3章将对该模式进行进一步讲解并提供实例代码来实现该模式。)

简答题]

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值