四、软件设计阶段 _ 软件详细设计
概述
任务
中层设计的:过程、调用;类、协作
+ 低层设计的全部:数据结构、算法;类型、语句、控制结构
有两类设计思想:
-
结构化设计思想:按
算法
分解。 (因为此时项目 以编程为驱动) -
面向对象设计:按照个体的单一
职责
来分解:
(一)、结构化设计思想:按算法 分解。 (因为此时项目 以编程为驱动)
-
把系统看做一系列相互关联的过程。
-
再针对某个单一过程再次分解,分解出更多更加细小的过程。
任务: 将DFD到结构图
(二)、面向对象设计的思想 按照个体的单一职责来分解:
设计“类以及与之相互通信的对象之间的协作”。
1)建立设计模型
-
通过职责建立静态设计模型:抽象类的职责+抽象类之间的关系
按单一职责进行类的抽象,确定类的属性,就是建立概念类图
-
通过协作建立动态设计模型:添加辅助类+抽象对象之间协作
就是建立顺序图
,再从动态模型(类的协作),确定类的操作,
2)重构设计模型
- 根据模块化思想进行重构,目标为高内聚、低耦合。
- 根据信息隐藏思想重构,目标为隐藏职责与变更。
- 利用设计模式重构。
补充:UML类图建模
3. 通过职责建立静态模型
1)抽象对象的职责
属性职责
对象的静态特性(状态特性)
方法职责
对象的动态特性(行为)
对象的行为用于维护属性(依赖于属性),或提供某项服务。
面向对象设计方法案例
(一)、案例引入:
功能:
软件体系结构设计:
(二)、面向对象设计过程
1)通过职责建立静态模型
-
抽象类及类的职责:属性职责、方法职责。
-
抽象类之间的关系:关系描述类之间的静态结构。
注意:类的行为是依赖 其属性的
- 检验: 基于当前精化的设计模型,能否在某车辆发生一次行程时,完成“收费”功能?
2)通过协作建立动态模型
添加辅助类+抽象对象之间协作
就是先建立顺序图、状态机图
,再从动态模型(类的协作),确定类的操作,
-
状态机图
场景:某车辆发生一次行程,产生收费。
-
顺序图:每一条消息,都要设计为接收对象的一个方法。
结果:
模块化与信息隐藏
本节内容:★★★
详细设计中的模块化与信息隐藏主要是两个方面:
- 模块化与耦合、内聚
- 面向对象中的耦合与内聚
设计质量
- 好的软件可以通过多个独立的团队在交流相对较少的情况下同时进行开发,即并行开发不同模块,以缩短整个开发时间。
- 当变化发生的时候,好的软件也只需要修改一个模块,而不影响别的模块。
可理解、易修改、易复用
是软件设计中需要特别关注的设计质量标准。
模块化和信息隐藏思想就是为了实现上述重要的质量标准而提出的设计方法。
高质量的软件设计应该将复杂系统分解为独立的模块。
-
模块化是从分解的角度思考:如何将系统分解为独立的模块。
高内聚、低耦合
-
信息隐藏更多地从模块的外部(抽象)角度思考:需要对外公开什么接口,隐藏什么秘密(目的是为了做到模块间尽可能的独立)。
模块化
模块化通过分解来实现,分解后的模块很难相互独立,
因此有两个概念来量化模块的质量:
- 耦合:分解后模块之间的关系的复杂程度。
- 内聚:一个模块内部的联系的紧密性。
模块化的原则就是:高内聚,低耦合。
耦合
类型 | 解释 | 例子 | 耦合性 | 备注 |
---|---|---|---|---|
内容耦合 | 一个模块直接修改或依赖于另一个模块的内容 | GOTO语句;某代码直接改变另一模块的内部数据。 | 最高 | 消 除 |
公共耦合 | 一组模块共享同一个全局数据结构 | 公共区域 | 比较强 | 不可避免,尽量消除 |
外部耦合 | 一组模块访问同一全局简单变量 | 比较强 | 不可避免,减少使用 | |
重复耦合 | 模块之间有同样逻辑的重复代码。 | 逻辑代码复制到两个地方。 | 比较强 | 不可避免,减少使用 |
控制耦合 | 一个模块给另一个模块传递控制信息 | 较弱 | 可以接受 | |
印记耦合(特征耦合) | 模块之间传递数据结构,但只使用其中一部分。 | 一组模块通过参数表传递记录信息。 | 较弱 | 可以接受 |
数据耦合 | 两个模块之间只传递简单的数据项参数 | 方法传递参数 | 最弱 | 最理想的 |
非直接耦合 | 两个模块之间没有直接关系。两个模块仅通过主模块的控制和调用来产生联系 | 购物网站:商品购买、数据备份 | 最弱 | 最理想的 |
小 结
- 高耦合对模块的灵活性及稳定性,产生不好的影响。
- 可以将数据放在模块内部进行管理,以消除高耦合。
举例
构造两个类似于qq聊天的模块。
-
内容耦合:一个模块直接修改或依赖于另一个模块的内容。
耦合度:最高
-
公共耦合:一组模块共享同一个全局数据结构。
耦合度:比较强( 不可避免,尽量消除。)
-
外部耦合:一组模块访问同一全局简单变量。
耦合度:比较强( 不可避免,尽量消除。)
-
控制耦合:一个模块给另一个模块传递控制信息。
耦合性:较弱(可以接受)
-
印记耦合(特征耦合):模块之间传递数据结构,但只使用其中一部分。
传入参数为 对象
耦合性:较弱(可以接受)
-
数据耦合:两个模块之间只传递简单的数据项参数。
耦合性:最理想的
内聚
类型 | 解释 | 例子 | 耦合性 | 备注 |
---|---|---|---|---|
偶然内聚 | 模块执行多个完全不相关的操作 | Word的工具菜单 | 最高 | 消 除 |
逻辑内聚 | 模块执行一系列逻辑上相似的,但没有直接关联的操作 | 一个函数能打印季度开支报告、月份开支报告、每日开支报告,具体打印哪个,由传入的控制标志决定。 | 比较强 | 尽量消除 |
时间内聚 | 模块执行一系列在同一时间段内发生的操作 | 操作系统的开机初始化工作 | 比较强 | 减少使用 |
过程内聚 | 模块执行一些与步骤顺序有关的操作 | 成绩查询模块,必须先登录,然后再查询 | 较弱 | 可以接受 |
通信内聚 | 模块执行一系列与步骤有关的操作,并且这些操作在相同的数据上进行 | 网络购物时,购物模块先接收购买信息,生成订单,保存订单,并支付订单。 | 较弱 | 可以接受,不可避免 |
功能内聚 | 模块执行一系列与步骤有关的操作,并且这些操作在相同的数据上进行 | 登录模块 | 弱 | 理想的 |
信息内聚 | 模块进行许多操作,各个都有各自的入口点,每个操作的代码相对独立,而且所有操作都在相同 的数据结构上完成 | ATM机取款模块,先读入取款额,再检查余额是否足够,再计算新的余额,修改账户余额,再吐钞。 | 最弱 | 最理想的 |
举例
-
偶然内聚:模块执行多个完全不相关的操作。
-
逻辑内聚:模块执行一系列逻辑上相似的,但没有直接关联的操作。
消 除
-
时间内聚:模块执行一系列在同一时间段内发生的操作。
操作系统的开机。 -
过程内聚:模块执行一些与步骤顺序有关的操作。
不可避免,限制使用
查询成绩模块:先登录,再查询。 -
通信内聚:模块执行一系列与步骤有关的操作,并且这些操作在相同的数据上进行。
不可避免,限制使用
在线购物模块:先检验购物信息、生成订单、支付订单。 -
功能内聚:模块只执行一个操作,或达到一个单一目的。
理想的
设计专门的 登录模块。 -
信息内聚:模块进行许多操作,各个都有各自的入口点,每个操作的代码相对独立,而且所有操作都在相同的数据结构上完成。
理想的
ATM机取款模块,先读入取款额,再检查余额是否足够,再计算新的余额,修改账户余额,再吐钞。
模块化设计实践
-
低耦合设计
分层的体系结构设计
将可复用的代码或设计抽象为构件(Component) 等 -
高内聚设计
分层的体系结构设计
设计抽象类 等
信息隐藏
信息隐藏是利用 抽象 的方法,抽象出每个类的关键细节(模块的职责),从而聚焦到本质特征,降低认知的复杂度。
设计思路:
- 抽象出每个模块对外承担的职责,对外表现为一份契约。
- 而隐藏决策实现的具体细节。
抽象出来的契约就是接口, 隐藏的是内部的实现细节。
模块说明
主要秘密:模块要实现的用户需求
次要秘密:实现职责时涉及的关键实现细节:数据结构、算法
等
角色:模块在整个系统中所承担的角色,与谁有关系。
对外接口:提供给别的模块的接口
例:
面向对象中的模块
面向对象中类=模块
模块化希望代码片段(模块)由两部分组成:接口和实现。
-
接口\(\to\)对外接口
代码片段之间用来交互的协议,包括供接口(供给别人使用的契约)和需接口(需要使用别人的契约)。 -
实现\(\to\)秘密
该协议的具体实施。
类之间的关系:耦合
-
方法之间的调用关系,与结构化方法下的6种耦合(
内公外控印数据
)一致 -
关联\(\to\)访问耦合
如果某个类关联另一个类,它就持有另一个类的引用,该类的所有对象就具有了向另一个类的对象发送消息的能力。 -
继承\(\to\)继承耦合
子类可以访问父类的成员方法和成员变量。
面向对象的耦合
访问耦合分类
隐式访问 , 必须消除
public class ClassA
{ public ClassB methodA(){…} }
public class ClassB
{ public void methodB(){…} }
------------------------------------------
ClassA a = new ClassA();
a.methodA().methodB(); //连续方法调用
实现中访问,可以接受
public class ClassA
{
public void testMethod()
{
//cb 是ClassA中某个方法的局部变量。
ClassB cb=new ClassB();
cb.methodB();
} }
参数变量访问,可以接受
public class ClassA
{ //ClassB 是ClassA中某个方法的参数。
public void testMethod(ClassB test)
{
test.bMethod();
}
}
成员变量访问,可以接受
public class ClassA
{ //cb 是ClassA的成员。
ClassB cb;
}
}
降低“访问耦合”的方法:
-
针对接口编程(Programming to Interface)
在类与类之间的访问时(非继承),要求只访问对方的接口。
为每个类都定义明确的契约(接口),并按照契约组织和理解软件结构,以做到“针对接口编程”。 -
接口分离原则/接口最小化(ISP原则)
类不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。
将臃肿的接口拆分为独立的几个接口,类只与需要的接口建立依赖关系。 -
访问耦合的合理范围/迪米特法则 The Law of Demeter
避免隐式访问耦合
每个单元对于其他的单元只能拥有有限的知识,只是与当前单元紧密联系的单元
每个单元只能和它的朋友交谈,不能和陌生单元交谈
只和自己直接的朋友交谈
即对于对象 O 中的一个方法 M,那么 M 只能调用下列对象的方法
O 自己
M 中的参数对象
任何在 M 中创建的对象
O 的成员变量
小结:
软件设计过程中,耦合是极易发生,且不可避免的。
耦合度高会影响设计方案的灵活性和稳定性。
如果耦合度高,要及时的分析和降低耦合。
继承耦合
定义:
面向对象方法中,由于继承关系,父类和子类之间存在的耦合。
里氏替换原则(LSP):
子类必须能够替换基类,而且起同样的作用。 如果违反该原则,继承耦合会比较强。
注意:继承机制的两重性: (使用继承时的两种含义)
- 继承(复用)父类接口及实现:复用父类代码\(\to\)用组合代替继承。
- 继承父类接口,override实现:继承接口,override实现时,要符合LSP。
Public class client {
public static void main(String[] args){
Super a = new Sub1();
a.method();
} }
--------------------------------------------------------------------
Public class Super {
public void method() { //父类的实现 }
}
Public class Sub1 extends Super {
public void method() { base.method() //复用父类实现}
public void method() { //子类的实现 }
}
面向对象的内聚
面向对象的内聚有三种:
-
方法的内聚: 与结构化方法下的6种内聚(
功通过时逻偶
)一致 -
类的内聚 成员变量和方法之间的内聚
减少类的内聚的方法:单一职责原则(SRP):
一个高内聚的类,不仅是信息内聚
的,还应该是功能内聚
的。
面向对象方法下的信息隐藏(为变更而设计)
为变更而设计 -- 信息隐藏(封装变更) 的主要内容就是:
开闭原则OCP及其实现(1.使用多态实现OCP;2.使用DIP实现OCP)
// Dish类代码
class Dish {
public String showMaterials( String _dN)
{
if( _dN.equals(“岐山臊子面”))
materials = “五花肉、面…”;
else if( _dN.equals(“四川泡菜”))
materials = “萝卜、豇豆…”;
}
需 求 变 更:在程序中,增加“山东煎饼”的食材显示。
代码实现:
// Dish类代码
class Dish {
public String showMaterials( String _dN)
{
if( _dN.equals(“岐山臊子面”))
materials = “五花肉、面…”;
else if( _dN.equals(“四川泡菜”))
materials = “萝卜、豇豆…”;
/*************************************************/
else if( _dN.equals(“山东煎饼”))
materials = “煎饼、葱花、…”;
}
}
方法一:
修改Dish类代码。
方法二:
不修改底层代码,
而是增加新的类。
// NewDish类代码
class NewDish extends Dish{
//重写继承方法
public String showMaterials(String _dN)
{
if( _dN.equals(“山东煎饼”))
materials = “煎饼、葱花、…”;
else //其他的调用父类方法
materials = super.showMaterials(_dN);
…
}
}
开闭原则(OCP)
原则: 好的设计应该对“扩展”开放,对“修改”关闭。
(当有新需求时,尽量不修改之前的代码,而是增加新代码)。
实现
- 使用多态实现OCP
- 使用DIP实现OCP
使用多态实现OCP
重构上面的代码就是:
使用DIP实现OCP
依赖倒置原则(DIP)(Dependency Inversion Principle)
描述:
- 高层模块不应该依赖于低层模块,双方都应该依赖于抽象。
- 抽象不应该依赖于细节,细节应该依赖于抽象。
优化前:
优化后:
总结
设计原则及设计模式:
- 6个设计原则(SRP、OCP、LSP、DIP、ISP、LKP)
- 2个设计模式 ( Stratery、Factory)
不要求掌握