Dunwu Blog

大道至简,知易行难

技术文档规范

文档采用 Markdown 语法书写。

📚 “参考”Markdown 语法可以参考:

1. 标题

1.1. 标题层级

标题分为四级。

  1. 一级标题:文章的标题
  2. 二级标题:文章内容的大标题
  3. 三级标题:二级标题下一级的标题
  4. 四级标题:三级标题下一级的标题

1.2. 标题原则

  • 一篇文章中应该尽力避免同名标题。
  • 一级标题下,不能直接出现三级标题。
  • 标题要避免孤立编号(即同级标题只有一个)。
  • 下级标题不重复上一级标题的内容。
  • 谨慎使用四级标题,尽量避免出现,保持层级的简单和防止出现过于复杂的章节。如果三级标题下有并列性的内容,建议只使用项目列表(Item list)。

2. 文本

2.1. 字间距

全角中文字符与半角英文字符之间,应有一个半角空格。

1
2
3
反例:本文介绍如何快速启动Windows系统。

正例:本文介绍如何快速启动 Windows 系统。

全角中文字符与半角阿拉伯数字之间,有没有半角空格都可,但必须保证风格统一,不能两种风格混杂。

1
2
3
正例:2011年5月15日,我订购了5台笔记本电脑与10台平板电脑。

正例:2011 年 5 15 日,我订购了 5 台笔记本电脑与 10 台平板电脑。

半角的百分号,视同阿拉伯数字。

英文单位若不翻译,单位前的阿拉伯数字与单位间不留空格。

1
2
3
反例:一部容量为 16 GB 的智能手机

正例:一部容量为 16GB 的智能手机

半角英文字符和半角阿拉伯数字,与全角标点符号之间不留空格。

1
2
3
反例:他的电脑是 MacBook Air 。

正例:他的电脑是 MacBook Air。

2.2. 句子

  • 避免使用长句。一个句子建议不超过 100 字或者正文的 3 行。
  • 尽量使用简单句和并列句,避免使用复合句。

2.3. 写作风格

尽量不使用被动语态,改为使用主动语态。

1
2
3
反例:假如此软件尚未被安装,

正例:假如尚未安装这个软件,

不使用非正式的语言风格。

1
2
3
反例:Lady Gaga 的演唱会真是酷毙了,从没看过这么给力的表演!!!

正例:无法参加本次活动,我深感遗憾。

用对“的”、“地”、“得”。

1
2
3
4
5
6
7
8
她露出了开心的笑容。
(形容词+的+名词)

她开心地笑了。
(副词+地+动词)

她笑得很开心。
(动词+得+副词)

使用代词时(比如“其”、“该”、“此”、“这”等词),必须明确指代的内容,保证只有一个含义。

1
2
3
反例:从管理系统可以监视中继系统和受其直接控制的分配系统。

正例:从管理系统可以监视两个系统:中继系统和受中继系统直接控制的分配系统。

名词前不要使用过多的形容词。

1
2
3
反例:此设备的使用必须在接受过本公司举办的正式的设备培训的技师的指导下进行。

正例:此设备必须在技师的指导下使用,且指导技师必须接受过由本公司举办的正式设备培训。

单个句子的长度尽量保持在 20 个字以内;20 ~ 29 个字的句子,可以接受;30 ~ 39 个字的句子,语义必须明确,才能接受;多于 40 个字的句子,在任何情况下都不能接受。

1
2
3
反例:本产品适用于从由一台服务器进行动作控制的单一节点结构到由多台服务器进行动作控制的并行处理程序结构等多种体系结构。

正例:本产品适用于多种体系结构。无论是由一台服务器(单一节点结构),还是由多台服务器(并行处理结构)进行动作控制,均可以使用本产品。

同样一个意思,尽量使用肯定句表达,不使用否定句表达。

1
2
3
反例:请确认没有接通装置的电源。

正例:请确认装置的电源已关闭。

避免使用双重否定句。

1
2
3
反例:没有删除权限的用户,不能删除此文件。

正例:用户必须拥有删除权限,才能删除此文件。

2.4. 英文处理

英文原文如果使用了复数形式,翻译成中文时,应该将其还原为单数形式。

1
2
3
英文:⋯information stored in random access memory (RAMs)⋯

中文:……存储在随机存取存储器(RAM)里的信息……

外文缩写可以使用半角圆点(.)表示缩写。

1
2
U.S.A.
Apple, Inc.

表示中文时,英文省略号()应改为中文省略号(……)。

1
2
3
英文:5 minutes later

中文:5 分钟过去了⋯⋯

英文书名或电影名改用中文表达时,双引号应改为书名号。

1
2
3
英文:He published an article entitled "The Future of the Aviation".

中文:他发表了一篇名为《航空业的未来》的文章。

第一次出现英文词汇时,在括号中给出中文标注。此后再次出现时,直接使用英文缩写即可。

1
IOC(International Olympic Committee,国际奥林匹克委员会)。这样定义后,便可以直接使用“IOC”了。

专有名词中每个词第一个字母均应大写,非专有名词则不需要大写。

1
2
3
“American Association of Physicists in Medicine”(美国医学物理学家协会)是专有名词,需要大写。

“online transaction processing”(在线事务处理)不是专有名词,不应大写。

3. 段落

3.1. 段落原则

  • 一个段落只能有一个主题,或一个中心句子。
  • 段落的中心句子放在段首,对全段内容进行概述。后面陈述的句子为核心句服务。
  • 一个段落的长度不能超过七行,最佳段落长度小于等于四行。
  • 段落的句子语气要使用陈述和肯定语气,避免使用感叹语气。
  • 段落之间使用一个空行隔开。
  • 段落开头不要留出空白字符。

3.2. 引用

引用第三方内容时,应注明出处。

1
One man’s constant is another man’s variable. — Alan Perlis

如果是全篇转载,请在全文开头显著位置注明作者和出处,并链接至原文。

1
本文转载自 WikiQuote

使用外部图片时,必须在图片下方或文末标明来源。

1
本文部分图片来自 Wikipedia

3.3. 强调

一些特殊的强调内容可以按照如下方式书写:

🔔 “注意”

💡 “提示”

📚 “参考”

4. 数值

4.1. 半角数字

数字一律使用半角形式,不得使用全角形式。

1
2
3
反例: 这件商品的价格是1000元。

正例: 这件商品的价格是 1000 元。

4.2. 千分号

数值为千位以上,应添加千分号(半角逗号)。

1
XXX 公司的实收资本为 RMB1,258,000

对于 4 ~ 6 位的数值,千分号是选用的,比如10001,000都可以接受。对于 7 位及以上的数值,千分号是必须的。

多位小数要从小数点后从左向右添加千分号,比如4.234,345

4.3. 货币

货币应为阿拉伯数字,并在数字前写出货币符号,或在数字后写出货币中文名称。

1
2
$1,000
1,000 美元

4.4. 数值范围

表示数值范围时,用连接。参见《标点符号》一节的“连接号”部分。

带有单位或百分号时,两个数字都要加上单位或百分号,不能只加后面一个。

1
2
3
4
5
反例:132234kg
正例:132kg~234kg

反例:6789%
正例:67%89%

4.5. 变化程度的表示法

数字的增加要使用“增加了”、“增加到”。“了”表示增量,“到”表示定量。

1
2
3
4
5
增加到过去的两倍
(过去为一,现在为二)

增加了两倍
(过去为一,现在为三)

数字的减少要使用“降低了”、“降低到”。“了”表示增量,“到”表示定量。

1
2
3
4
5
降低到百分之八十
(定额是一百,现在是八十)

降低了百分之八十
(原来是一百,现在是二十)

不能用“降低 N 倍”或“减少 N 倍”的表示法,要用“降低百分之几”或“减少百分之几”。因为减少(或降低)一倍表示数值原来为一百,现在等于零。

5. 符号

5.1. 符号原则

  • 中文语句的标点符号,均应该采取全角符号,这样可以保证视觉的一致。
  • 如果整句为英文,则该句使用英文/半角标点。
  • 句号、问号、叹号、逗号、顿号、分号和冒号不得出现在一行之首。

5.2. 句号

中文语句中的结尾处应该用全角句号()。

句子末尾用括号加注时,句号应在括号之外。

1
2
3
反例:关于文件的输出,请参照第 1.3 节(见第 26 页。)

正例:关于文件的输出,请参照第 1.3 节(见第 26 页)。

5.3. 逗号

逗号表示句子内部的一般性停顿。

注意避免“一逗到底”,即整个段落除了结尾,全部停顿都使用逗号。

5.4. 顿号

句子内部的并列词,应该用全角顿号() 分隔,而不用逗号,即使并列词是英语也是如此。

1
2
3
反例:我最欣赏的科技公司有 Google, Facebook, 腾讯, 阿里和百度等。

正例:我最欣赏的科技公司有 Google、Facebook、腾讯、阿里和百度等。

英文句子中,并列词语之间使用半角逗号(,)分隔。

1
例句:Microsoft Office includes Word, Excel, PowerPoint, Outlook and other components.

5.5. 分号

分号表示复句内部并列分句之间的停顿。

5.6. 引号

引用时,应该使用全角双引号(“ ”),注意前后双引号不同。

1
例句:许多人都认为客户服务的核心是“友好”和“专业”。

引号里面还要用引号时,外面一层用双引号,里面一层用单引号(‘ ’),注意前后单引号不同。

1
例句:鲍勃解释道:“我要放音乐,可萨利说,‘不行!’。”

5.7. 圆括号

补充说明时,使用全角圆括号(),括号前后不加空格。

1
例句:请确认所有的连接(电缆和接插件)均安装牢固。

5.8. 冒号

全角冒号()常用在需要解释的词语后边,引出解释和说明。

1
例句:请确认以下几项内容:时间、地点、活动名称,以及来宾数量。

表示时间时,应使用半角冒号(:)。

1
例句:早上 8:00

5.9. 省略号

省略号……表示语句未完、或者语气的不连续。它占两个汉字空间、包含六个省略点,不要使用。。。...等非标准形式。

省略号不应与“等”这个词一起使用。

1
2
3
4
5
反例:我们为会餐准备了香蕉、苹果、梨…等各色水果。

正例:我们为会餐准备了各色水果,有香蕉、苹果、梨……

正例:我们为会餐准备了香蕉、苹果、梨等各色水果。

5.10. 感叹号

应该使用平静的语气叙述,尽量避免使用感叹号

不得多个感叹号连用,比如!!!!!

5.11. 破折号

破折号————一般用于做进一步解释。破折号应占两个汉字的位置。

1
例句:直觉————尽管它并不总是可靠的————告诉我,这事可能出了些问题。

5.12. 连接号

连接号用于连接两个类似的词。

以下场合应该使用直线连接号(-),占一个半角字符的位置。

  • 两个名词的复合
  • 图表编号
1
2
3
例句:氧化-还原反应

例句:图 1-1

以下场合应该使用波浪连接号(),占一个全角字符的位置。

  • 数值范围(例如日期、时间或数字)
1
例句:2009 年~2011 年

注意,波浪连接号前后两个值都应该加上单位。

波浪连接号也可以用汉字“至”代替。

1
例句:周围温度:-20°C-10°C

6. 结构

6.1. 目录结构

技术手册目录结构是一部完整的书,建议采用下面的结构。

  • 简介(Introduction) - [必选][目录|文件] 提供对产品和文档本身的总体的、扼要的说明
  • 入门篇(Quickstart) - [可选][文件] 如何最快速地使用产品
  • 基础篇(Basics) - [必选][目录] 又称”使用篇“,提供初级的使用教程
    • 环境准备(Prerequisite) - [可选][文件] 软件使用需要满足的前置条件
    • 安装(Installation) - [可选][文件] 软件的安装方法
    • 配置(Configuration) - [可选][目录|文件] 软件的配置
    • 特性(Feature) - [必选][目录|文件] 软件的功能特性
  • 进阶篇(Advanced) - [可选][目录] 又称”开发篇“,提供中高级的开发教程
    • 原理(Principle) - [可选][目录|文件] 软件的原理
    • 设计(Design) - [可选][目录|文件] 软件的设计,如:架构、设计思想等
  • 实战篇(Action) - [可选][目录] 提供一些具有实战意义的示例说明
  • API(API) - [可选][目录|文件] 软件 API 的逐一介绍
  • 常见问题(FAQ) - [可选][目录|文件] 常见问题解答
  • 附录(Appendix) - [可选][目录] 不属于教程本身、但对阅读教程有帮助的内容
    • 命令(Command) - [可选][目录] 命令
    • 资源(Resource) - [必选][文件] 资源
    • 术语(Glossary) - [可选][文件] 名词解释
    • 技巧(Recipe) - [可选][文件] 最佳实践
    • 版本(Changelog) - [可选][文件] 版本说明
    • 反馈(Feedback) - [可选][文件] 反馈方式

下面是两个真实范例,可参考。

6.2. 文件名

文档的文件名不得含有空格。

文件名必须使用半角字符,不得使用全角字符。这也意味着,中文不能用于文件名。

1
2
3
反例: 名词解释.md

正例: glossary.md

文件名建议只使用小写字母,不使用大写字母。

1
2
3
反例:TroubleShooting.md

正例:troubleshooting.md

为了醒目,某些说明文件的文件名,可以使用大写字母,比如READMELICENSE

文件名包含多个单词时,单词之间建议使用半角的连词线(-)分隔。

1
2
3
反例:advanced_usage.md

正例:advanced-usage.md

7. Emoji

在 markdown 文档中,普遍会使用 emoji,帮助理解内容。但是,如果滥用 emoji,可能会适得其反。

这里,将一些比较约定俗成的 emoji 表情使用场景列举一下:

  • 💡 提示 - [推荐]
  • 🔔 注意、警告 - [推荐]
  • ⭕ 正确 - [推荐]
  • ❌ 错误 - [推荐]
  • ❓ 问题 - [推荐]
  • ⛔ 禁止 - [推荐]
  • 🚧 未完待续、有待补充 - [推荐]
  • 📚 参考、参考资料 - [可选]
  • ⌨ 源码 - [可选]

8. 参考

个人目录管理规范

作为程序员,想必每个人都会有大量的资料、数据。按照条理清晰的目录结构去分类化存储,十分有助于管理文件。

目录结构

以下是我个人整理的目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.
├── Codes #代码目录
│ ├── Other #第三方代码目录
│ ├── My #个人代码目录
│ └── Work #工作代码目录
├── Data #数据目录
├── Downloads #下载文件目录
├── Docs #文档目录
│ ├── Books #电子书目录
│ ├── My #个人文档目录
│ └── Work #工作文档目录
├── Movies #视频目录
├── Music #音乐目录
├── Pictures #图片目录
├── Public #共享目录
├── Temp #临时文件目录
└── Tools #工具软件目录
└── Packages #安装包目录

注:如果您使用的操作系统是 Mac 这种可以为目录或文件添加 tag 的操作系统,那么您可以根据自己的喜好更细致化的管理。

2. 文件管理软件

选用便利的文件管理软件,可以让你的文件管理如虎添翼。这里推荐几款经典的文件管理工具。

2.1. Clover

Clover 是 Windows Explorer 资源管理器的一个扩展,为其增加类似谷歌 Chrome 浏览器的多标签页功能。

2.2. Everything

Everything 可以立即在 windows 系统中找到制定名称的文件和文件夹。

2.3. Wox

Wox 是一款简单易用的 Windows 启动器。可以把它视为 windows 版的 Alfred。

2.4. Q-dir

Q-dir 是轻量的文件管理器,特点鲜明,各种布局视图切换灵活,默认四个小窗口组成一个大窗口,操作快捷。

Maven 实战问题和最佳实践

Maven 常见问题

dependencies 和 dependencyManagement,plugins 和 pluginManagement 有什么区别

dependencyManagement 是表示依赖 jar 包的声明,即你在项目中的 dependencyManagement 下声明了依赖,maven 不会加载该依赖,dependencyManagement 声明可以被继承。

dependencyManagement 的一个使用案例是当有父子项目的时候,父项目中可以利用 dependencyManagement 声明子项目中需要用到的依赖 jar 包,之后,当某个或者某几个子项目需要加载该插件的时候,就可以在子项目中 dependencies 节点只配置 groupId 和 artifactId 就可以完成插件的引用。

dependencyManagement 主要是为了统一管理插件,确保所有子项目使用的插件版本保持一致,类似的还有 plugins 和 pluginManagement。

IDEA 修改 JDK 版本后编译报错

错误现象:

修改 JDK 版本,指定 maven-compiler-plugin 的 source 和 target 为 1.8 。

然后,在 Intellij IDEA 中执行 maven 指令,报错:

1
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.0:compile (default-compile) on project apollo-common: Fatal error compiling: 无效的目标版本: 1.8 -> [Help 1]

错误原因:

maven 的 JDK 源与指定的 JDK 编译版本不符。

排错手段:

  • 查看 Project Settings

Project SDK 是否正确

img

SDK 路径是否正确

img

  • 查看 Settings > Maven 的配置

JDK for importer 是否正确

img

Runner 是否正确

img

重复引入依赖

在 Idea 中,选中 Module,使用 Ctrl+Alt+Shift+U,打开依赖图,检索是否存在重复引用的情况。如果存在重复引用,可以将多余的引用删除。

如何打包一个可以直接运行的 Spring Boot jar 包

可以使用 spring-boot-maven-plugin 插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

如果引入了第三方 jar 包,如何打包?

首先,要添加依赖

1
2
3
4
5
6
7
<dependency>
<groupId>io.github.dunwu</groupId>
<artifactId>dunwu-common</artifactId>
<version>1.0.0</version>
<scope>system</scope>
<systemPath>${project.basedir}/src/main/resources/lib/dunwu-common-1.0.0.jar</systemPath>
</dependency>

接着,需要配置 spring-boot-maven-plugin 插件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
<configuration>
<includeSystemScope>true</includeSystemScope>
</configuration>
</plugin>
</plugins>
</build>

去哪儿找 maven dependency

问:刚接触 maven 的新手,往往会有这样的疑问,我该去哪儿找 jar?

答:官方推荐的搜索 maven dependency 网址:

如何指定编码

问:众所周知,不同编码格式常常会产生意想不到的诡异问题,那么 maven 构建时如何指定 maven 构建时的编码?

答:在 properties 中指定 project.build.sourceEncoding

1
2
3
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

如何指定 JDK 版本

问:如何指定 maven 构建时的 JDK 版本

答:有两种方法:

(1)properties 方式

1
2
3
4
5
6
7
8
<project>
...
<properties>
<maven.compiler.source>1.7</maven.compiler.source>
<maven.compiler.target>1.7</maven.compiler.target>
</properties>
...
</project>

(2)使用 maven-compiler-plugin 插件,并指定 source 和 target 版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<build>
...
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.3</version>
<configuration>
<source>1.7</source>
<target>1.7</target>
</configuration>
</plugin>
</plugins>
...
</build>

如何避免将 dependency 打包到构件中

答:指定 maven dependency 的 scope 为 provided,这意味着:依赖关系将在运行时由其容器或 JDK 提供。
具有此范围的依赖关系不会传递,也不会捆绑在诸如 WAR 之类的包中,也不会包含在运行时类路径中。

如何跳过单元测试

问:执行 mvn package 或 mvn install 时,会自动编译所有单元测试(src/test/java 目录下的代码),如何跳过这一步?

答:在执行命令的后面,添加命令行参数 -Dmaven.test.skip=true 或者 -DskipTests=true

如何引入本地 jar

问:有时候,需要引入在中央仓库找不到的 jar,但又想通过 maven 进行管理,那么应该如何做到呢?
答:可以通过设置 dependency 的 scope 为 system 来引入本地 jar。
例:

  • 将私有 jar 放置在 resouces/lib 下,然后以如下方式添加依赖:
  • groupId 和 artifactId 可以按照 jar 包中的 package 设置,只要和其他 jar 不冲突即可。
1
2
3
4
5
6
7
<dependency>
<groupId>xxx</groupId>
<artifactId>xxx</artifactId>
<version>1.0.0</version>
<scope>system</scope>
<systemPath>${project.basedir}/src/main/resources/lib/xxx-6.0.0.jar</systemPath>
</dependency>

如何排除依赖

问:如何排除依赖一个依赖关系?比方项目中使用的 libA 依赖某个库的 1.0 版。libB 以来某个库的 2.0 版,如今想统一使用 2.0 版,怎样去掉 1.0 版的依赖?

答:通过 exclusion 排除指定依赖即可。

例:

1
2
3
4
5
6
7
8
9
10
11
12
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.12</version>
<optional>true</optional>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
</exclusions>
</dependency>

Maven 最佳实践

通过 bom 统一管理版本

采用类似 spring-boot-dependencies 的方式统一管理依赖版本。

spring-boot-dependencies 的 pom.xml 形式:

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
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.1.9.RELEASE</version>
<packaging>pom</packaging>

<!-- 省略 -->

<!-- 依赖包版本管理 -->
<dependencyManagement>
<dependencies>
<!-- 省略 -->
</dependencies>
</dependencyManagement>

<build>
<!-- 插件版本管理 -->
<pluginManagement>
<plugins>
<!-- 省略 -->
</plugins>
</pluginManagement>
</build>
</project>

其他项目引入 spring-boot-dependencies 来管理依赖版本的方式:

1
2
3
4
5
6
7
8
9
10
11
 <dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

UML 结构建模图

结构图定义了一个模型的静态架构。它们通常被用来对那些构成模型的‘要素’建模,诸如:类,对象,接口和物理组件。另外,它们也被用来对元素间关联和依赖关系进行建模。

关键词:部署图, 组件图, 包图, 类图, 复合结构图, 对象图

部署图

部署图(Deployment Diagram)用于对系统的物理结构建模。部署图将显示系统中的软件组件和硬件组件之间的关系以及处理工作的物理分布。

节点

节点既可以是硬件元素,也可以是软件元素。它显示为一个立方体,如下图所示。

节点实例

图可以显示节点实例,实例与节点的区分是:实例的名称带下划线,冒号放在它的基本节点类型之前。实例在冒号之前可以有名称,也可以没有名称。下图显示了一个具名的计算机实例。

节点构造型

为节点提供了许多标准的构造型,分别命名为 «cdrom», «cd-rom», «computer», «disk array», «pc», «pc client», «pc server», «secure», «server», «storage», «unix server», «user pc»。 并在节点符号的右上角显示适当的图标。

工件

工件是软件开发过程中的产品。包括过程模型(如:用例模型,设计模型等),源文件,执行文件,设计文档,测试报告,构造型,用户手册等等。

工件表示为带有工件名称的矩形,并显示«artifact»关键字和文档符号。

关联

在部署图的上下文联系中,关联代表节点间的联系通道。下图显示了一个网络系统的部署图,描述了网络协议为构造型和关联终端的多重性,

作为容器的节点

节点可以包含其他元素,如组件和工件。下图显示了一个嵌入式系统某个部分的部署图。描写了一个被主板节点包含的可执行工件。

组件图

组件图(Component Diagram)描绘了组成一个软件系统的模块和嵌入控件。组件图比类图具有更高层次的抽象-通常运行时一个组件被一个或多个类(或对象)实现。它们象积木那样使得组件能最终构成系统的绝大部分。

上图演示了一些组件和它们的内部关系。装配连接器(Assembly connectors)“连接”由”Product”和”Customer”的提供接口到由 “Order”指定的需求接口。 一个依赖关系映射了客户相关的帐户信息到“Order”需要的 “Payment”需求接口。

实际上,组件图同包图很相似,它们都有明确的界限,把元素分组到逻辑结构中。他们之间的不同是:组件图提供了语义更丰富的分组机制,在组件图中,所有的模型元素都是私有的,而包图只显示公有的成员。

表现组件

组件可表示为带关键字 «component»的矩形类元;也可用右上角有组件图标的矩形表示。

装配连接器

装配连接器在组件 “Component1”的需求接口和另一个组件 “Component2”的提供接口之间建立桥梁; 这个桥梁使得一个组件能提供另一个组件所需要的服务。

带端口组件

使用端口的组件图允许在它的环境指定一个服务和行为,同时这个服务和行为也是组件需要的。当端口进行双向操作的时候,它可以指定输入和输出。下图详述了用于在线服务的带端口组件,它有两个提供接口 “order entry”和 “tracking”,也有 “payment” 需求接口。

包图

包图(Package Diagram)用来表现包和它所包含元素的组织。当用来代表类元素时,包图提供了命名空间的可视化。包图最常用的用途是用来组织用例图和类图,尽管它不局限于这些 UML 元素。

下面是一个包图的例子。

包中的元素共享相同的命名空间,因此,一个指定命名空间的元素必须有唯一的名称。

包可以用来代表物理或逻辑关系。选择把类包括在指定的包里,有助于在同一个包里赋予这些类相同继承层次。通常认为把通过复合相关联的类,以及与它们相协作的类放在同一个包里。

在 UML2.5 中,包用文件夹来表示,包中的元素共享同一个命名空间,并且必须是可识别的,因此要有唯一的名称或类型。包必须显示包名,在附属方框部分有选择的显示包内的元素。

  • 包的合并 - 包之间的合并连接符«merge»定义了源包元素与目标包同名元素之间的泛化关系。源包元素的定义被扩展来包含目标包元素定义。当源包元素与目标包内没有同名元素时,目标包元素的定义不受影响。
  • 包的导入 - 导入连接符 «import»表明目标包的元素,在该例中是一个类 ,在源包中被引用要用非限定修饰名。源包的命名空间获得目标类的接口,目标包的命名空间则不受影响。
  • 嵌套连接符 - 源包和目标包间的嵌套连接符说明目标包完全包含源包。

类图

类图(Class Diagram)展示了面向对象系统的构造模块。描绘了模型或部分模型的静态视图,显示它包含的属性和行为,而不是详细描述操作的功能或完善方法。类图最常用来表达多个类和接口之间的关系。泛化(Generalizations),聚合(aggregations)和关联(associations)分别是类之间继承,复合或应用,及连接的表现。

下面的图显示了类之间的聚合关系。弱聚合(浅色箭头)表现在类 “Account” 使用 “AddressBook”,但是不必要包含它的一个实例。强聚合(图中的黑色箭头)表示了目标类包含源类,例如,”Contact” 和 “ContactGroup”值被包含在 “AddressBook”中。

类(Classes)

类是定义对象所具有的属性和行为的元素。行为用类能理解的合适消息和适合每条消息的操作来描述。 类中也可能定义约束,标记值,构造型。

类的标柱(Class Notation)

类用矩形表示。除类的名称外,还可以选择性地显示属性和操作。 分栏分别用来显示类的名称,属性和操作。

在下面图中,类的类名显示在最上面的分栏,它下面的分栏显示详细属性,如:”center” 属性显示初始化的值。最后面的分栏显示操作,如: setWidth,setLength 和 setPosition 以及他们的参数。 属性和操作名前的标注表示了该属性或操作的可见性: 如果使用 “+”号,这个属性或操作是公共的 ; “-“ 号则代表这个属性或操作是私有的。 “#”号是这个属性或操作被定义为保护的,” ~“ 号代表包的可见性。

接口(Interfaces)

接口是实施者同意满足的行为规范,是一种约定。实现一个接口,类必需支持其要求的行为,使系统按照同样的方式,即公共的接口,处理不相关的元素。

接口有相似于类的外形风格,含有指定的操作,如下图所示。如果没有明确的详细操作,也可以画成一个圆环。当画成圆环的时候,到这个环形标柱的实现连接没有目标箭头。

表(Tables)

表尽管不是基本 UML 的一部分,仍然是“图型”能完成的实例用。在右上角画一个表的小图标来表示。表属性用“图型” «column»表示。 绝大多数表单有一个主键,是由一个或几个字段组成的一个唯一的字码组合加主键操作来访问表格,主键操作“图型”为«PK»。 一些表有一个或多个外键,使用一个或多个字段加一个外键操作,映射到相关表的主键上去,外键操作“图型”为«FK»。

关联(Associations)

关联表明两个模型元素之间有关系,通常用在一个类中被实现为一个实例变量。连接符可以包含两端的命名的角色,基数性,方向和约束。关联是元素之间普通的关系。如果多于两个元素,也可以使用菱形的关联关系。当从类图生成代码时,关联末端的对象将变成目标类中实例变量。见下图示例 “playsFor” 将变成”Player”类中的实例变量。

泛化(Generalizations)

泛化被用来说明继承关系。连接从特定类元到一般类元。泛化的含义是源类继承了目标类的特性。下图的图显示了一个父类泛化一个子类, 类“Circle”的一个实例将会有属性 “ x_position”,“ y_position” , “radius” 和 方法 “display()”。 注意:类 “Shape” 是抽象的,类名显示为斜体。

下图显示了与上图相同信息的视图。

聚合(Aggregations)

聚合通常被用来描述由更小的组件所构成的元素。聚合关系表示为白色菱形箭头指向目标类或父类。

聚合的更强形式 -组合聚合(强聚合) - 显示为黑色菱形箭头,用来组合每次最大化的包含组件。如果一个组合聚合的父类被删除,通常与他相关的所有部分都会被删除,但是,如果一个部件从组合中去掉,将不用删除整个组合。组合是可迁,非对称的关系和递归的。

下面的图示:显示了弱聚合和强聚合的不同。“ address book” 由许多 “contacts” 和 “contact groups”组成。 “contact group” 是一个“contacts”的虚分组; “contact”可以被包含在不止一个 “ contact group”。 如果你删除一个“ address book”,所有的 “contacts” 和 “contact groups” 也将会被删除;如果你删除“ contact group”, 没有 “contacts”会被删除。

关联类(Association Classes)

关联类是一个允许关联连接有属性和操作的构造。下面的示例:显示了远不止简单连接两个类的连接,如给“employee”分配项目。“ employee”在项目中所起的作用是一个复杂的实体,既有自身的也有不属于“employee” 或 “project” 类的细节。 例如,“ employee”可以同时为几个项目工作,有不同的职务头衔和对应的安全权限。

依赖(Dependencies)

依赖被用来描述模型元素间广泛的依赖关系。通常在设计过程早期显示两个元素之间存在某种关系,因为是初期而不能确定具体是什么关系,在设计过程末期,该继承关系会被归入已有构造型 (构造型 可以是实例化 «instantiate»,跟踪 «trace»,导入 «import», 和其它的关系),或被替换成一个更明确类型的连接符。

跟踪(Traces)

跟踪关系是一种特殊化的依赖关系。连接模型元素或跨模型但是具有相同概念的模型元素集。跟踪被经常用来追踪需求和模型的变化。由于变化是双向的,这种依赖关系的顺序通常被忽略。这种关系的属性可以被指定为单向映射,但跟踪是双向的,非正式的和很少可计算的。

实现(Realizations)

是源对象执行或实现目标,实现被用来表达模型的可跟踪性和完整性-业务模型或需求被一个或多个用例实现,用例则被类实现,类被组件实现,等等。这种实现贯穿于系统设计的映射需求和类等,直至抽象建模水平级。从而确保整个系统的一张宏图,它也反映系统的所有微小组成,以及约束和定义它的细节。实现关系用带虚线的实箭头表示。

嵌套(Nestings)

嵌套连接符用来表示源元素嵌套在目标元素中。下图显示“ inner class”的定义,尽管在 EA 中,更多地按照着他们在项目层次视图中的位置来显示这种关系。

复合结构图

复合结构图显示类的内部结构,包括它与系统其他部分的交互点。也显示各部分的配置与关系,这些部分一起执行类元的行为。

类元素已经在类图部分被详细地阐述,这部分用来说明类表现复合元素的方式,如:暴露接口,包含端口和部件。

部件

部件是代表一组(一个或多个)实例的元素,这组实例的拥有者是一类元实例,例如:如果一个图的实例有一组图形元素,则这些图形元素可以被表示为部件,并可以对他们之间的某种关系建模。注意:一个部件可以在它的父类被删除之前从父类中被去掉,这样部件就不会被同时删除了。
部件在类或组件内部显示为不加修饰的方框。

端口

端口是类型化的元素,代表一个包含类元实例的外部可视的部分。端口定义了类元和它的环境之间的交互。端口显示在包含它的部件,类或组合结构的边缘上。端口指定了类元提供的服务,以及类元要求环境提供的服务。
端口显示为所属类元边界指定的方框。

接口

接口与类相似,但是有一些限制,所有的接口操作都是公共和抽象的,不提供任何默认的实现。所有的接口属性都必须是常量。然而,当一个类从一个单独的超级类继承而来,它可以实现多个接口。
当一个接口在图中单列出来,它既可以显示为类元素的方框,带 «interface» 关键字和表明它是抽象的斜体名称,也可以显示为圆环。

注意:圆环标注不显示接口操作。当接口显示为类所有的接口,它们会被当作暴露接口引用。暴露接口可以定义为是提供的,还是需求的。提供接口确认包含它的类元提供指定接口元素定义的操作,可通过类和接口间实现的连接来定义。需求接口说明该类元能与其他类元进行通信,这些类元提供了指定接口元素所定义的操作。需求接口可通过在类和接口间建立依赖连接来定义。
提供接口显示为“带棒球体”,依附在类元边缘。需求接口显示为“带棒杯体”,也是依附在类元边缘。

委托

委托连接器用来定义组件外部端口和接口的内部工作方式。委托连接器表示为带有 «delegate» 关键字的箭头。它连接组件的外部约定,表现为它的端口,到组件部件行为的内部实现。

协作

协作定义了一系列共同协作的角色,它们集体展示一个指定的设计功能。协作图应仅仅显示完成指定任务或功能的角色与属性。隔离主要角色是用来简化结构和澄清行为,也用于重用。一个协作通常实现一个模式。
协作元素显示为椭圆。

角色绑定

角色绑定连接器是一条从连接协作到所要完成该任务类元的连线。它显示为虚线,并在类元端显示作用名。

表现

表现连接器用于连接协作到类元来表示此类元中使用了该协作。显示为带关键字 «represents»的虚线箭头。

发生
发生连接器用于连接协作到类元来表示此协作表现了(同原文)该类元;显示为带关键字«occurrence»的虚线箭头。

对象图

对象图(Object Diagram)可以认为是类图的特殊情形,是类图元素子集,被用来及时强调在某些点,类的实例间的关系。这对理解类图很有帮助。他们在构造上与类图显示没有不同,但是反映出多样性和作用。

类和对象元素

下面的图显示了类元素和对象元素外观上的不同。注意:类元素包括三个部分,分别是名字栏,属性栏和操作栏;对象元素默认为没有分栏。名称显示也有不同:对象名称有下划线,并可能显示该对象实例化所用类元的名称。

运行状态

类元元素可以有任意数量的属性和操作。在对象实例中不会被显示出来。但可能定义对象的运行状态,显示特殊实例的属性设置值。

类和对象图示例

下图是一个对象图,其中插入了类定义图。它例示如何用对象图来测试类图中任务多重性的方法。“car” 类对 “wheel” 类有“1 对多” 的多重性,但是如果已经选择用“1 对 4” 来替代,那样就不会在对象图显示“3 个轮子”的汽车。

参考资料

域名解析协议 DNS

域名系统(英文:Domain Name System,缩写:DNS)是互联网的一项服务。它作为将域名和 IP 地址相互映射的一个分布式数据库,能够使人更方便地访问互联网。DNS 使用 TCP 和 UDP 端口 53。当前,对于每一级域名长度的限制是 63 个字符,域名总长度则不能超过 253 个字符。

关键词:DNS, 域名解析

简介

什么是 DNS

DNS 是一个应用层协议。

域名系统 (DNS) 的作用是将人类可读的域名 (如,www.example.com) 转换为机器可读的 IP 地址 (如,192.0.2.44)。

什么是域名

域名是由一串用点分隔符 . 组成的互联网上某一台计算机或计算机组的名称,用于在数据传输时标识计算机的方位。域名可以说是一个 IP 地址的代称,目的是为了便于记忆后者。例如,wikipedia.org 是一个域名,和 IP 地址 208.80.152.2 相对应。人们可以直接访问 wikipedia.org 来代替 IP 地址,然后域名系统(DNS)就会将它转化成便于机器识别的 IP 地址。这样,人们只需要记忆 wikipedia.org 这一串带有特殊含义的字符,而不需要记忆没有含义的数字。

DNS 的分层

域名系统是分层次的。

在域名系统的层次结构中,各种域名都隶属于域名系统根域的下级。域名的第一级是顶级域,它包括通用顶级域,例如 .com.net.org;以及国家和地区顶级域,例如 .us.cn.tk。顶级域名下一层是二级域名,一级一级地往下。这些域名向人们提供注册服务,人们可以用它创建公开的互联网资源或运行网站。顶级域名的管理服务由对应的域名注册管理机构(域名注册局)负责,注册服务通常由域名注册商负责。

DNS 服务类型

  • 授权型 DNS - 一种授权型 DNS 服务提供一种更新机制,供开发人员用于管理其公用 DNS 名称。然后,它响应 DNS 查询,将域名转换为 IP 地址,以便计算机可以相互通信。授权型 DNS 对域有最终授权且负责提供递归型 DNS 服务器对 IP 地址信息的响应。Amazon Route 53 是一种授权型 DNS 系统。
  • 递归型 DNS - 客户端通常不会对授权型 DNS 服务直接进行查询。而是通常连接到称为解析程序的其他类型 DNS 服务,或递归型 DNS 服务。递归型 DNS 服务就像是旅馆的门童:尽管没有任何自身的 DNS 记录,但是可充当代表您获得 DNS 信息的中间程序。如果递归型 DNS 拥有已缓存或存储一段时间的 DNS 参考,那么它会通过提供源或 IP 信息来响应 DNS 查询。如果没有,则它会将查询传递到一个或多个授权型 DNS 服务器以查找信息。

记录类型

DNS 中,常见的资源记录类型有:

  • NS 记录(域名服务) ─ 指定解析域名或子域名的 DNS 服务器。
  • MX 记录(邮件交换) ─ 指定接收信息的邮件服务器。
  • A 记录(地址) ─ 指定域名对应的 IPv4 地址记录。
  • AAAA 记录(地址) ─ 指定域名对应的 IPv6 地址记录。
  • CNAME(规范) ─ 一个域名映射到另一个域名或 CNAME 记录( example.com 指向 www.example.com )或映射到一个 A记录。
  • PTR 记录(反向记录) ─ PTR 记录用于定义与 IP 地址相关联的名称。 PTR 记录是 A 或 AAAA 记录的逆。 PTR 记录是唯一的,因为它们以 .arpa 根开始并被委派给 IP 地址的所有者。

详细可以参考:维基百科 - 域名服务器记录类型列表

域名解析

主机名到 IP 地址的映射有两种方式:

  • 静态映射 - 在本机上配置域名和 IP 的映射,旨在本机上使用。Windows 和 Linux 的 hosts 文件中的内容就属于静态映射。
  • 动态映射 - 建立一套域名解析系统(DNS),只在专门的 DNS 服务器上配置主机到 IP 地址的映射,网络上需要使用主机名通信的设备,首先需要到 DNS 服务器查询主机所对应的 IP 地址。

通过域名去查询域名服务器,得到 IP 地址的过程叫做域名解析。在解析域名时,一般先静态域名解析,再动态解析域名。可以将一些常用的域名放入静态域名解析表中,这样可以大大提高域名解析效率。

上图展示了一个动态域名解析的流程,步骤如下:

  1. 用户打开 Web 浏览器,在地址栏中输入 www.example.com,然后按 Enter 键。
  2. www.example.com 的请求被路由到 DNS 解析程序,这一般由用户的 Internet 服务提供商 (ISP) 进行管理,例如有线 Internet 服务提供商、DSL 宽带提供商或公司网络。
  3. ISP 的 DNS 解析程序将 www.example.com 的请求转发到 DNS 根名称服务器。
  4. ISP 的 DNS 解析程序再次转发 www.example.com 的请求,这次转发到 .com 域的一个 TLD 名称服务器。.com 域的名称服务器使用与 example.com 域相关的四个 Amazon Route 53 名称服务器的名称来响应该请求。
  5. ISP 的 DNS 解析程序选择一个 Amazon Route 53 名称服务器,并将 www.example.com 的请求转发到该名称服务器。
  6. Amazon Route 53 名称服务器在 example.com 托管区域中查找 www.example.com 记录,获得相关值,例如,Web 服务器的 IP 地址 (192.0.2.44),并将 IP 地址返回至 DNS 解析程序。
  7. ISP 的 DNS 解析程序最终获得用户需要的 IP 地址。解析程序将此值返回至 Web 浏览器。DNS 解析程序还会将 example.com 的 IP 地址缓存 (存储) 您指定的时长,以便它能够在下次有人浏览 example.com 时更快地作出响应。有关更多信息,请参阅存活期 (TTL)。
  8. Web 浏览器将 www.example.com 的请求发送到从 DNS 解析程序中获得的 IP 地址。这是您的内容所处位置,例如,在 Amazon EC2 实例中或配置为网站终端节点的 Amazon S3 存储桶中运行的 Web 服务器。
  9. 192.0.2.44 上的 Web 服务器或其他资源将 www.example.com 的 Web 页面返回到 Web 浏览器,且 Web 浏览器会显示该页面。

🔔 注意:只有配置了域名服务器,才能执行域名解析。

例如,在 Linux 中执行 vim /etc/resolv.conf 命令,在其中添加下面的内容来配置域名服务器地址:

1
nameserver 218.2.135.1

Linux 上的域名相关命令

hostname

hostname 命令用于查看和设置系统的主机名称。环境变量 HOSTNAME 也保存了当前的主机名。在使用 hostname 命令设置主机名后,系统并不会永久保存新的主机名,重新启动机器之后还是原来的主机名。如果需要永久修改主机名,需要同时修改 /etc/hosts/etc/sysconfig/network 的相关内容。

参考:http://man.linuxde.net/hostname

示例:

1
2
$ hostname
AY1307311912260196fcZ

nslookup

nslookup 命令是常用域名查询工具,就是查 DNS 信息用的命令。

参考:http://man.linuxde.net/nslookup

示例:

1
2
3
4
5
6
7
8
[root@localhost ~]# nslookup www.jsdig.com
Server: 202.96.104.15
Address: 202.96.104.15#53

Non-authoritative answer:
www.jsdig.com canonical name = host.1.jsdig.com.
Name: host.1.jsdig.com
Address: 100.42.212.8

更多内容

UML 行为建模图

行为图用来记录在一个模型内部,随时间的变化,模型执行的交互变化和瞬间的状态;并跟踪系统在真实环境下如何表现,以及观察系统对一个操作或事件的反应,以及它的结果。

关键词:活动图, 状态机图, 用例图, 通信图, 交互概述图, 时序图, 时间图

活动图

UML 中,活动图用来展示活动的顺序。显示了从起始点到终点的工作流,描述了活动图中存在于事件进程的判断路径。活动图可以用来详细阐述某些活动执行中发生并行处理的情况。活动图对业务建模也比较有用,用来详细描述发生在业务活动中的过程。
一个活动图的示例如下所示。

下面描述组成活动图的元素。

活动

活动是行为参数化顺序的规范。活动被表示为圆角矩形,内含全部的动作,工作流和其他组成活动的元素。

动作

一个动作代表活动中的一个步骤。动作用圆角矩形表示。

动作约束

动作可以附带约束,下图显示了一个带前置条件和后置条件的动作。

控制流

控制流显示一个动作到下一个动作的流。表示为带箭头实线

初始节点

一个开始或起始点用大黑圆点表示,如下图。

结束节点

结束节点有两种类型:活动结束节点和流结束节点。活动结束节点表示为中心带黑点的圆环。

流结束节点表示为内部为叉号的圆环。

这两种不同类型节点的区别为:流结束节点表明单独的控制流的终点。活动结束终点是活动图内所有控制流的结束。

对象和对象流

对象流是对象和数据转递的通道。对象显示为矩形。

对象流显示为带箭头的连接器,表明方向和通过的对象。

一个对象流在它的至少一个终端有一个对象。在上图中,可以采用带输入输出引脚的速记标柱表示。

数据存储显示为带 «datastore» 关键字的对象。

判断节点和合并节点

判断节点和合并节点是相同标注:菱形。它们可以被命名。从判断节点出来的控制流有监护条件,当监护条件满足时,可以对流控制。下图显示了判断节点和合并节点的使用。

分叉和结合节点

分叉和结合节点有同样的标柱:垂直或水平条(方向取决于工作流从左到右,还是从上到下)。它们说明了控制的并发线程的起始和终点,下图显示他们的使用示例。

结合节点与合并节点不同之处在于:结合节点同步两个输入量,产生一个单独的输出量。来自结合节点的输出量要接收到所有的输入量后才能执行。合并节点直接将控制流传递通过。如果两个或更多的输入量到达合并节点。则它的输出流指定的动作会被执行两次或更多次。

扩展域

扩展域是会执行多次的结构活动域。输入输出扩展节点表示为一组“3 厢” ,代表多个选择项。关键词 “iterative”, “parallel” 或 “stream”显示在区域的左上角

异常处理器

异常处理器在活动图中可以建模。

可中断活动区

可中断活动区环绕一组可以中断的动作。在下面非常简单的例子中: 当控制被传递到结束订单 “Close Order” 动作,定单处理”Process Order” 动作会执行直到完成,除非”Cancel Request”取消请求中断被接受,这会将控制传递给”Cancel Order”动作。

分割

一个活动分割显示为垂直或水平泳道。在下图中,分割被用来在活动图中分隔动作,有在 “accounting department”中执行的,有在 “customer”中执行的。

状态机图

状态机图(state-machine-diagram)对一个单独对象的行为建模,指明对象在它的整个生命周期里,响应不同事件时,执行相关事件的顺序。

如下示例, 下列的状态机图显示了门在它的整个生命周期里如何运作。

门可以处于以下的三种状态之一: “Opened”打开状态, “Closed”关闭状态,或者”Locked”锁定状态。 它分别响应事件:“Open”开门, “Close”关门, “Lock”锁门 和 “Unlock”解锁。 注意:不是所有的事件,在所有的状态下都是有效的。如:一个门打开的时候是不可能锁定的,除非你关上门。并且,状态转移可能有附加监护条件:假设门是开的,如果“doorWay->isEmpty”(门是空的)被满足,那么它只能响应关门事件。状态机图使用的语法和约定将在下面的部分进行讨论。

状态

状态被表示为圆角矩形,状态名写在里面。

起始和结束状态

初始状态表示为实心黑圆环,可以标注名称。结束状态表示为中心带黑点圆环,也可以被标注名称。

转移

一个状态到下一个状态的转移表示为带箭头实线。转移可以有一个“Trigger”触发器,一个“Guard”监护条件和一个“effect”效果。如下所示:

“Trigger”触发器是转移的起因,它可以是某个条件下的一个信号,一个事件,一个变化或一个时间通路。”Guard”监护是一个条件,而且必须为真,以便于让触发器引起转移。效果”Effect”是直接作用到对象上的一个动作,该对象具有做为转移结果的状态机。

状态活动

在上面的状态转移示例中,一个效果与该转移相关联。如果目标状态有多个转移到达,并且每一个转移都有相同的效果与它相关联,那最好将该效果与目标状态相关联,而不与转移相关联。你可以通过为这个状态定义初始动作来实现。下图显示了一个带入口动作和出口动作的状态。

可以定义发生在事件上的动作或一直发生的动作。每一种类型的动作是可以定义任意数量的。

自转移

一个状态可能有一个返回到自身的转移,如下图。效果与转移关联是十分有帮助。

复合状态

一个状态机图可以有子状态机图,如下图所示:

可选择不同方式显示相同信息,如下图所示:

上面版本的标注说明”Check PIN”的子状态机图显示在单独的图中。

入口点

有时,你不想在正常的初始状态进入子状态机。例如下面的子状态机,它通常从”初始化”状态开始,但是如果因为某些原因,它不必执行初始化,可能靠转移到指定的入口点来从 “Ready” 状态开始。

下图显示了状态机的上一层。

出口点

有与入口点相类似的方式,它可能也指定可选择的出口点。下图给出了主处理状态执行后,所执行状态的去向将取决于该状态转移时所使用的路径。

选择伪状态

选择伪状态显示为菱形,有一个转移输入,两个或多个输出。下图显示不管到达哪一个状态,经过选择伪状态后的去向,取决于在伪状态中执行时所选择的消息格式。

连接伪状态

连接伪状态用来将多个状态转移链接在一起。一个单独的连接伪状态可以有一个或多个输入和一个或多个输出,监护可能应用于每一个转移,连接是没有语义的。连接可以把一个输入转移分成多个输出转移来实现一个静态分支。与之对照的是选择伪状态实现一个动态条件分支。

终止伪状态

进入终止伪状态是指状态机生命线已经终止。终止伪状态表示为叉号。

历史状态

历史状态用来当状态机中断时,恢复状态机之前状态。下面例图说明了历史状态的使用。这个例子是关于洗衣机的状态机。

在这个状态机中,当洗衣机运行时,它会按照”Washing” 到 Rinsing”再到”Spinning”来进行。如果电源被切断 ,洗衣机会停止运行并进入”Power Off” 状态。当电源恢复,运行状态在”History State”符号处进入,表示它会从上次离开的地方恢复。

并发区

一个状态可以被分成几个不同的区,包含同时存在和执行的子状态。下面的例子显示状态 “Applying Brakes”, “front brake”和”rear brakes” 将同时独立运作。注意使用了分叉和结合伪状态而不是选择和合并伪状态。这些符号用来同步并发的线程。

用例图

用例图用来记录系统的需求,它提供系统与用户及其他参与者的一种通信手段。

执行者

用例图显示了系统和系统外实体之间的交互。这些实体被引用为执行者。执行者代表角色,可以包括:用户,外部硬件和其他系统。执行者往往被画成简笔画小人。也可以用带«actor»关键字的类矩形表示。

在下图中,执行者可以详细的泛化其他执行者:

用例

用例是有意义的单独工作单元。它向系统外部的人或事提供一个易于观察的高层次行为视图。 用例的标注符号是一个椭圆。

使用用例的符号是带可选择箭头的连接线,箭头显示控制的方向。下图说明执行者 “Customer”使用 “Withdraw”用例。

用途连接器(uses connector)可以有选择性的在每一个端点有多重性值,如下图,显示客户一次可能只执行一次取款交易。但是银行可以同时执行许多取款交易。

用例定义

一个典型的用例包括:

  • 名称和描述 - 用例通常用一个动词词组定义,而且有一个简短的文字说明。
  • 需求 - 需求定义了一个用例必须提供给终端用户的正式功能性需求。它们符合构造方法建立的功能性规范。一个需求是用例将执行一个动作或提供多个值给系统的约定或承诺。
  • 约束 - 一个约束是一个用例运行的条件或限制。它包括:前置条件,后置条件和不变化条件 。前置条件指明了用例在发生之前需要符合的条件。后置条件用来说明在用例执行之后一些条件必须为”真”。不变化条件说明用例整个执行过程中该条件始终为”真”。
  • 情形 - 情形是用例的实例在执行过程中,事件发生流程的形式描述。它定义了系统和外部执行者之间的事件指定顺序。通常用文本方式来表示,并对应时序图中的文字描述。
  • 情形图
  • 附加信息

包含用例

用例可能包含其他用例的功能来作为它正常处理的一部分。通常它假设,任何被包含的用例在基本程序运行时每一次都会被调用。下面例子:用例“卡的确认”<Card Identification> 在运行时,被用例“取钱”<Withdraw>当作一个子部分。

用例可以被一个或多个用例包含。通过提炼通用的行为,将它变成可以多次重复使用的用例。有助于降低功能重复级别。

扩展用例

一个用例可以被用来扩展另一个用例的行为,通常使用在特别情况下。例如:假设在修改一个特别类型的客户订单之前,用户必须得到某种更高级别的许可,然后“获得许可”<Get Approval>用例将有选择的扩展常规的“修改订单”<Modify Order>用例。

扩展点 - 扩展用例的加入点被定义为扩展点。

系统边界 - 它用来显示用例在系统内部,执行者在系统的外部。

通信图

通信图,以前称之为协作图,是一种交互图,所显示消息与时序图相似,但是它更侧重于对象间的联系

在通信图中,对象之间显示关联连接器。消息附加到这些关联上,显示短箭头指向消息流的方向。消息的顺序通过编号码显示。

下面的两个图用通信图和时序图分别显示相同的信息。尽管我们可能从通信图的编号码得到消息顺序,但它不是立即可见的。通信图十分清楚的显示了邻近对象间全部完整的消息传递。

交互概述图

一个交互概览图是活动图的一种形式,它的节点代表交互图。交互图包含时序图,通信图,交互概览图和时间图。 大多数交互概览图标注与活动图一样。例如:起始,结束,判断,合并,分叉和结合节点是完全相同。并且,交互概览图介绍了两种新的元素:交互发生和交互元素。

交互发生

交互发生引用现有的交互图。显示为一个引用框,左上角显示 “ref” 。被引用的图名显示在框的中央。

交互元素

交互元素与交互发生相似之处在于都是在一个矩形框中显示一个现有的交互图。不同之处在内部显示参考图的内容不同。

将它们放在一起

所有的活动图控件,都可以相同地被使用于交互概览图,如:分叉,结合,合并等等。它把控制逻辑放入较低一级的图中。下面的例子就说明了一个典型的销售过程。子过程是从交互发生抽象而来。

时序图

时序图是交互图的一种形式,它显示对象沿生命线发展,对象之间随时间的交互表示为从源生命线指向目标生命线的消息。时序图能很好地显示那些对象与其它那些对象通信,什么消息触发了这些通信,时序图不能很好显示复杂过程的逻辑。

生命线

一条生命线在时序图中代表一个独立的参与者。表示为包含对象名的矩形,如果它的名字是”self”,则说明该生命线代表控制带时序图的类元。

有时,时序图会包含一个顶端是执行者的生命线。这情况说明掌握这个时序图的是用例。健壮图中的边界,控制和实体元素也可以有生命线。

消息

消息显示为箭头。消息可以完成传输,也可能丢失和找回,它可以是同步的,也可以是异步的,即可以是调用,也可以是信号。在下图中,第一条消息是同步消息(标为实箭头)完成传输,并隐含一条返回消息。第二条消息是异步消息 (标为实线箭头),第三条是异步返回消息(标为虚线)。

执行发生

向下延伸的细条状矩形表示执行事件或控制焦点的激活。在上图中有三个执行事件。第一个是源对象发送两条消息和收到两条回复。第二个是目标对象收到一条同步消息并返回一条回复。第三个是目标对象收到一条异步消息并返回一条回复。

内部通信

内部消息表现为一个操作的递归调用,或一个方法调用属于同一个对象的其他方法。显示为生命线上执行事件的嵌套控制焦点。

迷路消息和拾取消息

迷路消息是那些发送了却没有到达指定接收者,或者到达的接收者不再当前图中。拾取消息是收到来自那些未知的发送者,或者来自没有显示在当前图的发送者的消息。它们都表明是去往或来自一个终点元素。

生命线开始与结束

生命线可以在时序图时间刻度范围内创建和销毁,在下面的例子中,生命线被停止符号(叉号)终止。在前面的例子中,生命线顶端的符号(Child)显示在比创建它的对象符号(parent)沿页面要低的位置上。下图显示创建和终止对象。

时间和期限约束

消息默认显示为水平线。因为生命线显示为沿屏幕向下的时间通道,所以当给实时系统建模,或是有时间约束的业务过程建模,考虑执行动作所需时间长度是很重要的。因此可以给消息设置一个期限约束,这样的消息显示为下斜线。

复合片段

如前面所说,时序图不适合表达复杂的过程逻辑。在一种情况下,有许多机制允许把一定程度的过程逻辑加入到图中,并把它们放到复合片段的标题下。复合片段是一个或多个处理顺序被包含在一个框架中,并在指定名称的环境下执行。片段可以是:

  • 选择性片段 (显示 “alt”) 为 if…then…else 结构建模。
  • 选项片段 (显示 “opt”) 为 “switch”(开关) 结构建模。
  • 中断片段对被处理事件的可选择顺序建模,而不是该图的其他部分。
  • 并行片段(显示 “par”) 为并发处理建模。
  • 弱顺序片段 (显示 “seq”) 包含了一组消息,这组消息必须在后继片段开始之前被处理。但不会把片段内消息的先后顺序强加到不共享同一条生命线的消息上。
  • 严格顺序片段 (显示 “strict”) 包含了一系列需要按照给定顺序处理的消息。
  • 非片段 (显示 “neg”) 包含了一系列不可用的消息。
  • 关键片段 具有关键部分。
  • 忽略片段 声明一个没有意义的消息,如果它出现在当前上下文中。
  • 考虑片段与忽略片段相反,不包含在考虑片段内的消息都应该被忽略。
  • 断言片段 (显示 “assert”)标明任何没有显示为声明操作数的顺序都是无效的。
  • 循环片段 包含一系列被重复的消息。

下图显示的是循环片段:

这也是一个类似于复合片段的交互发生。 交互发生被其他图参考,显示为左上角带”ref”,将被参考图名显示在方框的中间。

门是连接片段内消息和片段外消息的连接点。 在 EA 中,门显示为片段框架上的小正方形。作用为时序图与页面外的连接器。 用来表示进来的消息源,或者出去消息的终点。下面两个图显示它们在实践中的使用。注意:” top level diagram”中的门用消息箭头指向参考片段,在这里没有必要把它画成方块。

部分分解

一个对象可以引出多条生命线,使得对象内部和对象之间的消息显示在同一图上。

状态常量/延续

状态常量是生命线的约束,运行时始终为”真”。显示为两侧半圆的矩形,如下图:

延续虽与状态常量有同样的标注,但是被用于复合片段,并可以延伸跨越多条生命线。

时间图

UML 时间图被用来显示随时间变化,一个或多个元素的值或状态的更改。也显示时控事件之间的交互和管理它们的时间和期限约束。

状态生命线

状态生命线显示随时间变化,一个单项状态的改变。不论时间单位如何选择,X 轴显示经过的时间,Y 轴被标为给出状态的列表。状态生命线如下所示:

值生命线

值生命线显示随时间变化,一个单项的值的变化。X 轴显示经过的时间,时间单位为任意,和状态生命线一样。平行线之间显示值,每次值变化,平行线交叉。如下图所示。

将它们放在一起

状态和值的生命线能叠加组合。它们必须有相同的 X 轴。 消息可以从一个生命线传递到另一个。每一个状态和值的变换能有一个定义的事件,一个时间限制是指一个事件何时必须发生,和一个期限限制说明状态或值多长时间必须有效。一旦这些已经被应用,其时间图可能显示如下。

参考资料

第一次读《重构:改善既有代码的设计》时,我曾整理过一个简单的笔记。最近,因为参与一个重构项目,再一次温习了《重构:改善既有代码的设计》。过程中,萌发了认真总结、整理重构方法的冲动,于是有了这系列文字。

症与药

对代码的坏味道的思考

“有病要早治,不要放弃治疗”。多么朴素的道理 ,人人都懂。

病,就是不健康。

人有病,可以通过打针、吃药、做手术来进行治疗。

如果把代码的坏味道(代码质量问题)比作病症,那么重构就是治疗代码的坏味道的药。

个人认为,在重构这件事上,也可以应用治病的道理:

  • 防患于未然。
    —— 春秋战国时期的一代名医扁鹊,曾经有个很著名的医学主张:防患于未然。 我觉得这个道理应用于软件代码的重构亦然。编程前要有合理的设计、编程时要有良好的编程风格,尽量减少问题。从这个层面上说,了解代码的坏味道,不仅仅是为了发现问题、解决问题。更重要的作用是:指导我们在编程过程中有意识的去规避这些问题。

  • 小病不医,易得大病。
    —— 刘备说过:“勿以善小而不为,勿以恶小而为之”。发现问题就及时修改,代码质量自然容易进入良性循环;反之,亦然。要重视积累的力量,别总以为代码出现点小问题,那都不是事儿。

  • 对症下药。
    —— 程序出现了问题,要分析出问题的根本,有针对性的制定合理的重构方案。大家都知道吃错药的后果,同样的,瞎改还不如不改

  • 忌猛药
    —— 医病用猛药容易产生副作用。换一句俗语:步子大了容易扯着蛋。重构如果大刀阔斧的干,那你就要有随时可能扑街的心理准备。推倒重来不是重构,而是重写。重构应该是循序渐进,步步为营的过程。当你发现重写代码比重构代码更简单,往往说明你早就该重构了。

重构的原则

前面把代码质量问题比作病症,而把重构比作药。这里,我们再进一步讨论一下重构的原则。

何谓重构(What)

重构(Refactoring) 的常见定义是:不改变软件系统外部行为的前提下,改善它的内部结构。

个人觉得这个定义有点生涩。不妨理解为:重构是给代码治病的行为。而代码有病是指代码的质量(可靠性、安全性、可复用性、可维护性)和性能有问题。

重构的目的是为了提高代码的质量和性能

注:功能不全或者不正确,那是残疾代码。就像治病治不了残疾,重构也解决不了功能问题。

为何重构(Why)

翻翻书,上网搜一下,谈到重构的理由大体相同:

  • 重构改进软件设计
  • 重构使软件更容易理解
  • 重构帮助找到 bug
  • 重构提高编程速度

总之就是,重构可以提高代码质量

何时重构(When)

关于何时重构,我先引用一下 重构并非难在如何做,而是难在何时开始做 一文的观点。

对于一个高速发展的公司来说,停止业务开发,专门来做重构项目,从来就不是一个可接受的选项,“边开飞机边换引擎”才是这种公司想要的。

我们不妨来衡量一下重构的成本和收益。

  • 重构的成本

    重构是有成本的,费时费力(时间、人力)不说,还有可能会使本来正常运行的程序出错。所以,很多人都抱着“不求有功,但求无过”的心理得过且过。

    还有一种成本:重构使用较新且较为复杂的技术,学习曲线不平滑,团队成员技术切换困难,短期内开发效率可能不升反降。

    但是,如果一直放任代码腐朽下去,技术债务会越来越沉重。当代码最终快要跑不动时,架构师们往往还是不得不使用激进的手段来治疗代码的顽疾。但是,这个过程通常都是非常痛苦的,而且有着很高的失败风险。

  • 重构的收益

    重构的收益是提高代码的质量和性能,并提高未来的开发效率。但是,应当看到,重构往往并不能在短期内带来实际的效益,或者很难直观看出效益。而对于一个企业来说,没有什么比效益更重要。换句话说,没有实际效益的事,通常也没有价值。很多领导,尤其是非技术方向的领导,并不关心你应用了什么新技术,让代码变得多么优雅等等。

  • 重构的合适时机

    从以上来看,重构实在是个吃力不讨好的事情。

    于是,很多人屈服于万恶的 KPI 和要命的 deadline,一边吐槽着以前的代码是垃圾,一边自己也在造垃圾。

    但是,重构本应该是个渐进式的过程,不是只有伤筋动骨的改造才叫重构。如果非要等到代码已经烂到病入膏肓,再使用激进方式来重构,那必然是困难重重,风险极高。

    《重构》书中提到的重构时机应该在添加功能、修复功能、审查代码时,不建议专门抽出时间专门做重构项目。

    我认为,其思想就是指:重构应该是在开发过程中实时的、渐进的演化过程。

  • 重构的不恰当时机

    但是,这里我也要强调一下:不是所有软件开发过程都一定要重构。

    较能凸显重构价值的场景是:代码规模较大、生命周期还较长、承担了较多责任、有一个较大(且较不稳定,人员流动频繁)团队在其上工作的单一代码库。

    与之相反,有一些场景的重构价值就很小:

    • 代码库生命周期快要走到尾声,开发逐渐减少,以维护为主。
    • 代码库当前版本马上要发布了,这时重构无疑是给自己找麻烦。
    • 重构代价过于沉重:重构后功能的正确性、稳定性难以保障;技术过于超前,团队成员技术迁移难度太大。

如何重构(How)

重构行为在我看来,也是可以分层级的。由高到低,越高层级难度越大:

  • 系统架构、集群架构、框架、服务、数据库:这个层面的重构属于战略级重构。现代软件往往业务复杂、庞大。使用微服务、数据迁移来拆分业务,降低业务复杂度成为了主流。但是,这些技术的测试、部署复杂,技术难度很高。
  • 组件、模块、接口:这个层面的重构属于战术级重构。组件、模块、框架的重构,主要是针对代码的设计问题。解决的是代码的整体结构问题。需要对框架、设计模式、分布式、并发等等有足够的了解。
  • 类、接口、函数、字段等:这个层面的重构属于战法级重构。《重构》一书提到了 代码的坏味道 以及相关的重构方法。这些都是对类、接口、函数、字段级别代码的重构手段。由于这一级别的重构方法较为简单,所以可操作性较强。具体细节可以阅读《代码的坏味道》篇章。

前两种层级的重构已经涉及到架构层面,影响较大,难度较高,如果功力不够不要轻易变动。由于这两个层级涉及领域较广,这里不做论述。

此处为分割线。下面是代码的坏味道系列。。。

代码的坏味道

《重构:改善既有代码的设计》中介绍了 22 种代码的坏味道以及重构手法。这些坏味道可以进一步归类。我总觉得将事物分类有助于理解和记忆。所以本系列将坏味道按照特性分类,然后逐一讲解。

img

代码坏味道之代码臃肿

代码臃肿(Bloated)这组坏味道意味着:代码中的类、函数、字段没有经过合理的组织,只是简单的堆砌起来。这一类型的问题通常在代码的初期并不明显,但是随着代码规模的增长而逐渐积累(特别是当没有人努力去根除它们时)。

代码坏味道之滥用面向对象

滥用面向对象(Object-Orientation Abusers)这组坏味道意味着:代码部分或完全地违背了面向对象编程原则。

代码坏味道之变革的障碍

变革的障碍(Change Preventers)这组坏味道意味着:当你需要改变一处代码时,却发现不得不改变其他的地方。这使得程序开发变得复杂、代价高昂。

代码坏味道之非必要的

非必要的(Dispensables)这组坏味道意味着:这样的代码可有可无,它的存在反而影响整体代码的整洁和可读性。

代码坏味道之耦合

耦合(Couplers)这组坏味道意味着:不同类之间过度耦合。

扩展阅读

参考资料

翻译自:https://sourcemaking.com/refactoring/smells/bloaters

代码臃肿(Bloated)这组坏味道意味着:代码中的类、函数、字段没有经过合理的组织,只是简单的堆砌起来。这一类型的问题通常在代码的初期并不明显,但是随着代码规模的增长而逐渐积累(特别是当没有人努力去根除它们时)。

基本类型偏执

基本类型偏执(Primitive Obsession)

  • 使用基本类型而不是小对象来实现简单任务(例如货币、范围、电话号码字符串等)。
  • 使用常量编码信息(例如一个用于引用管理员权限的常量USER_ADMIN_ROLE = 1 )。
  • 使用字符串常量作为字段名在数组中使用。

img

问题原因

类似其他大部分坏味道,基本类型偏执诞生于类初建的时候。一开始,可能只是不多的字段,随着表示的特性越来越多,基本数据类型字段也越来越多。

基本类型常常被用于表示模型的类型。你有一组数字或字符串用来表示某个实体。

还有一个场景:在模拟场景,大量的字符串常量被用于数组的索引。

解决方法

img

大多数编程语言都支持基本数据类型和结构类型(类、结构体等)。结构类型允许程序员将基本数据类型组织起来,以代表某一事物的模型。

基本数据类型可以看成是机构类型的积木块。当基本数据类型数量成规模后,将它们有组织地结合起来,可以更方便的管理这些数据。

  • 如果你有大量的基本数据类型字段,就有可能将其中部分存在逻辑联系的字段组织起来,形成一个类。更进一步的是,将与这些数据有关联的方法也一并移入类中。为了实现这个目标,可以尝试 以类取代类型码(Replace Type Code with Class)
  • 如果基本数据类型字段的值是用于方法的参数,可以使用 引入参数对象(Introduce Parameter Object)保持对象完整(Preserve Whole Object)
  • 如果想要替换的数据值是类型码,而它并不影响行为,则可以运用 以类取代类型码(Replace Type Code with Class) 将它替换掉。如果你有与类型码相关的条件表达式,可运用 以子类取代类型码(Replace Type Code with Subclass)以状态/策略模式取代类型码(Replace Type Code with State/Strategy) 加以处理。
  • 如果你发现自己正从数组中挑选数据,可运用 以对象取代数组(Replace Array with Object)

收益

  • 多亏了使用对象替代基本数据类型,使得代码变得更加灵活。
  • 代码变得更加易读和更加有组织。特殊数据可以集中进行操作,而不像之前那样分散。不用再猜测这些陌生的常量的意义以及它们为什么在数组中。
  • 更容易发现重复代码。

img

重构方法说明

以类取代类型码(Replace Type Code with Class)

问题

类之中有一个数值类型码,但它并不影响类的行为。

img

解决

以一个新的类替换该数值类型码。

img

引入参数对象(Introduce Parameter Object)

问题

某些参数总是很自然地同时出现。

img

解决

以一个对象来取代这些参数。

img

保持对象完整(Preserve Whole Object)

问题

你从某个对象中取出若干值,将它们作为某一次函数调用时的参数。

1
2
3
int low = daysTempRange.getLow();
int high = daysTempRange.getHigh();
boolean withinPlan = plan.withinRange(low, high);

解决

改为传递整个对象。

1
boolean withinPlan = plan.withinRange(daysTempRange);

以子类取代类型码(Replace Type Code with Subclass)

问题

你有一个不可变的类型码,它会影响类的行为。

img

解决

以子类取代这个类型码。

img

以状态/策略模式取代类型码(Replace Type Code with State/Strategy)

问题

你有一个类型码,它会影响类的行为,但你无法通过继承消除它。

img

解决

以状态对象取代类型码。

img

以对象取代数组(Replace Array with Object)

问题

你有一个数组,其中的元素各自代表不同的东西。

1
2
3
String[] row = new String[3];
row[0] = "Liverpool";
row[1] = "15";

解决

以对象替换数组。对于数组中的每个元素,以一个字段来表示。

1
2
3
Performance row = new Performance();
row.setName("Liverpool");
row.setWins("15");

数据泥团

数据泥团(Data Clumps)

有时,代码的不同部分包含相同的变量组(例如用于连接到数据库的参数)。这些绑在一起出现的数据应该拥有自己的对象。

img

问题原因

通常,数据泥团的出现时因为糟糕的编程结构或“复制-粘贴式编程”。

有一个判断是否是数据泥团的好办法:删掉众多数据中的一项。这么做,其他数据有没有因而失去意义?如果它们不再有意义,这就是个明确的信号:你应该为它们产生一个新的对象。

解决方法

  • 首先找出这些数据以字段形式出现的地方,运用 提炼类(Extract Class) 将它们提炼到一个独立对象中。
  • 如果数据泥团在函数的参数列中出现,运用 引入参数对象(Introduce Parameter Object) 将它们组织成一个类。
  • 如果数据泥团的部分数据出现在其他函数中,考虑运用 保持对象完整(Preserve Whole Object) 将整个数据对象传入到函数中。
  • 检视一下使用这些字段的代码,也许,将它们移入一个数据类是个不错的主意。

收益

  • 提高代码易读性和组织性。对于特殊数据的操作,可以集中进行处理,而不像以前那样分散。
  • 减少代码量。

img

何时忽略

  • 有时为了对象中的部分数据而将整个对象作为参数传递给函数,可能会产生让两个类之间不收欢迎的依赖关系,这中情况下可以不传递整个对象。

重构方法说明

提炼类(Extract Class)

问题

某个类做了不止一件事。

img

解决

建立一个新类,将相关的字段和函数从旧类搬移到新类。

img

引入参数对象(Introduce Parameter Object)

问题

某些参数总是很自然地同时出现。

img

解决

以一个对象来取代这些参数。

img

保持对象完整(Preserve Whole Object)

问题

你从某个对象中取出若干值,将它们作为某一次函数调用时的参数。

1
2
3
int low = daysTempRange.getLow();
int high = daysTempRange.getHigh();
boolean withinPlan = plan.withinRange(low, high);

解决

改为传递整个对象。

1
boolean withinPlan = plan.withinRange(daysTempRange);

过大的类

过大的类(Large Class)

一个类含有过多字段、函数、代码行。

img

问题原因

类通常一开始很小,但是随着程序的增长而逐渐膨胀。

类似于过长函数,程序员通常觉得在一个现存类中添加新特性比创建一个新的类要容易。

解决方法

设计模式中有一条重要原则:职责单一原则。一个类应该只赋予它一个职责。如果它所承担的职责太多,就该考虑为它减减负。

img

  • 如果过大类中的部分行为可以提炼到一个独立的组件中,可以使用 提炼类(Extract Class)
  • 如果过大类中的部分行为可以用不同方式实现或使用于特殊场景,可以使用 提炼子类(Extract Subclass)
  • 如果有必要为客户端提供一组操作和行为,可以使用 提炼接口(Extract Interface)
  • 如果你的过大类是个 GUI 类,可能需要把数据和行为移到一个独立的领域对象去。你可能需要两边各保留一些重复数据,并保持两边同步。 复制被监视数据(Duplicate Observed Data) 可以告诉你怎么做。

收益

  • 重构过大的类可以使程序员不必记住一个类中大量的属性。
  • 在大多数情况下,分割过大的类可以避免代码和功能的重复。

img

重构方法说明

提炼类(Extract Class)

问题

某个类做了不止一件事。

img

解决

建立一个新类,将相关的字段和函数从旧类搬移到新类。

img

提炼子类(Extract Subclass)

问题

一个类中有些特性仅用于特定场景。

img

解决

创建一个子类,并将用于特殊场景的特性置入其中。

img

提炼接口(Extract Interface)

问题

多个客户端使用一个类部分相同的函数。另一个场景是两个类中的部分函数相同。

img

解决

移动相同的部分函数到接口中。

img

复制被监视数据(Duplicate Observed Data)

问题

如果存储在类中的数据是负责 GUI 的。

img

解决

一个比较好的方法是将负责 GUI 的数据放入一个独立的类,以确保 GUI 数据与域类之间的连接和同步。

img

过长函数

过长函数(Long Method)

一个函数含有太多行代码。一般来说,任何函数超过 10 行时,你就可以考虑是不是过长了。
函数中的代码行数原则上不要超过 100 行。

img

问题的原因

通常情况下,创建一个新函数的难度要大于添加功能到一个已存在的函数。大部分人都觉得:“我就添加这么两行代码,为此新建一个函数实在是小题大做了。”于是,张三加两行,李四加两行,王五加两行。。。函数日益庞大,最终烂的像一锅浆糊,再也没人能完全看懂了。于是大家就更不敢轻易动这个函数了,只能恶性循环的往其中添加代码。所以,如果你看到一个超过 200 行的函数,通常都是多个程序员东拼西凑出来的。

解决函数

一个很好的技巧是:寻找注释。添加注释,一般有这么几个原因:代码逻辑较为晦涩或复杂;这段代码功能相对独立;特殊处理。
如果代码前方有一行注释,就是在提醒你:可以将这段代码替换成一个函数,而且可以在注释的基础上给这个函数命名。如果函数有一个描述恰当的名字,就不需要去看内部代码究竟是如何实现的。就算只有一行代码,如果它需要以注释来说明,那也值得将它提炼到独立函数中。

img

  • 为了给一个函数瘦身,可以使用 提炼函数(Extract Method)
  • 如果局部变量和参数干扰提炼函数,可以使用 以查询取代临时变量(Replace Temp with Query)引入参数对象(Introduce Parameter Object)保持对象完整(Preserve Whole Object)
  • 如果前面两条没有帮助,可以通过 以函数对象取代函数(Replace Method with Method Object) 尝试移动整个函数到一个独立的对象中。
  • 条件表达式和循环常常也是提炼的信号。对于条件表达式,可以使用 分解条件表达式(Decompose Conditional) 。至于循环,应该使用 提炼函数(Extract Method) 将循环和其内的代码提炼到独立函数中。

收益

  • 在所有类型的面向对象代码中,函数比较短小精悍的类往往生命周期较长。一个函数越长,就越不容易理解和维护。
  • 此外,过长函数中往往含有难以发现的重复代码。

img

性能

是否像许多人说的那样,增加函数的数量会影响性能?在几乎绝大多数情况下,这种影响是可以忽略不计,所以不用担心。
此外,现在有了清晰和易读的代码,在需要的时候,你将更容易找到真正有效的函数来重组代码和提高性能。

重构方法说明

提炼函数(Extract Method)

问题

你有一段代码可以组织在一起。

1
2
3
4
5
6
7
void printOwing() {
printBanner();

//print details
System.out.println("name: " + name);
System.out.println("amount: " + getOutstanding());
}

解决

移动这段代码到一个新的函数中,使用函数的调用来替代老代码。

1
2
3
4
5
6
7
8
9
void printOwing() {
printBanner();
printDetails(getOutstanding());
}

void printDetails(double outstanding) {
System.out.println("name: " + name);
System.out.println("amount: " + outstanding);
}

以查询取代临时变量(Replace Temp with Query)

问题

将表达式的结果放在局部变量中,然后在代码中使用。

1
2
3
4
5
6
7
8
9
double calculateTotal() {
double basePrice = quantity * itemPrice;
if (basePrice > 1000) {
return basePrice * 0.95;
}
else {
return basePrice * 0.98;
}
}

解决

将整个表达式移动到一个独立的函数中并返回结果。使用查询函数来替代使用变量。如果需要,可以在其他函数中合并新函数。

1
2
3
4
5
6
7
8
9
10
11
double calculateTotal() {
if (basePrice() > 1000) {
return basePrice() * 0.95;
}
else {
return basePrice() * 0.98;
}
}
double basePrice() {
return quantity * itemPrice;
}

引入参数对象(Introduce Parameter Object)

问题

某些参数总是很自然地同时出现。

img

解决

以一个对象来取代这些参数。

img

保持对象完整(Preserve Whole Object)

问题

你从某个对象中取出若干值,将它们作为某一次函数调用时的参数。

1
2
3
int low = daysTempRange.getLow();
int high = daysTempRange.getHigh();
boolean withinPlan = plan.withinRange(low, high);

解决

改为传递整个对象。

1
boolean withinPlan = plan.withinRange(daysTempRange);

以函数对象取代函数(Replace Method with Method Object)

问题

你有一个过长函数,它的局部变量交织在一起,以致于你无法应用提炼函数(Extract Method) 。

1
2
3
4
5
6
7
8
9
10
class Order {
//...
public double price() {
double primaryBasePrice;
double secondaryBasePrice;
double tertiaryBasePrice;
// long computation.
//...
}
}

解决

将函数移到一个独立的类中,使得局部变量成了这个类的字段。然后,你可以将函数分割成这个类中的多个函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Order {
//...
public double price() {
return new PriceCalculator(this).compute();
}
}

class PriceCalculator {
private double primaryBasePrice;
private double secondaryBasePrice;
private double tertiaryBasePrice;

public PriceCalculator(Order order) {
// copy relevant information from order object.
//...
}

public double compute() {
// long computation.
//...
}
}

分解条件表达式(Decompose Conditional)

问题

你有复杂的条件表达式。

1
2
3
4
5
6
if (date.before(SUMMER_START) || date.after(SUMMER_END)) {
charge = quantity * winterRate + winterServiceCharge;
}
else {
charge = quantity * summerRate;
}

解决

根据条件分支将整个条件表达式分解成几个函数。

1
2
3
4
5
6
if (notSummer(date)) {
charge = winterCharge(quantity);
}
else {
charge = summerCharge(quantity);
}

过长参数列

过长参数列(Long Parameter List)

一个函数有超过 3、4 个入参。

img

问题原因

过长参数列可能是将多个算法并到一个函数中时发生的。函数中的入参可以用来控制最终选用哪个算法去执行。

过长参数列也可能是解耦类之间依赖关系时的副产品。例如,用于创建函数中所需的特定对象的代码已从函数移动到调用函数的代码处,但创建的对象是作为参数传递到函数中。因此,原始类不再知道对象之间的关系,并且依赖性也已经减少。但是如果创建的这些对象,每一个都将需要它自己的参数,这意味着过长参数列。

太长的参数列难以理解,太多参数会造成前后不一致、不易使用,而且一旦需要更多数据,就不得不修改它。

解决方案

img

  • 如果向已有的对象发出一条请求就可以取代一个参数,那么你应该使用 以函数取代参数(Replace Parameter with Methods) 。在这里,,“已有的对象”可能是函数所属类里的一个字段,也可能是另一个参数。
  • 你还可以运用 保持对象完整(Preserve Whole Object) 将来自同一对象的一堆数据收集起来,并以该对象替换它们。
  • 如果某些数据缺乏合理的对象归属,可使用 引入参数对象(Introduce Parameter Object) 为它们制造出一个“参数对象”。

收益

  • 更易读,更简短的代码。
  • 重构可能会暴露出之前未注意到的重复代码。

何时忽略

  • 这里有一个重要的例外:有时候你明显不想造成”被调用对象”与”较大对象”间的某种依赖关系。这时候将数据从对象中拆解出来单独作为参数,也很合情理。但是请注意其所引发的代价。如果参数列太长或变化太频繁,就需要重新考虑自己的依赖结构了。

重构方法说明

以函数取代参数(Replace Parameter with Methods)

问题

对象调用某个函数,并将所得结果作为参数,传递给另一个函数。而接受该参数的函数本身也能够调用前一个函数。

1
2
3
4
int basePrice = quantity * itemPrice;
double seasonDiscount = this.getSeasonalDiscount();
double fees = this.getFees();
double finalPrice = discountedPrice(basePrice, seasonDiscount, fees);

解决

让参数接受者去除该项参数,并直接调用前一个函数。

1
2
int basePrice = quantity * itemPrice;
double finalPrice = discountedPrice(basePrice);

保持对象完整(Preserve Whole Object)

问题

你从某个对象中取出若干值,将它们作为某一次函数调用时的参数。

1
2
3
int low = daysTempRange.getLow();
int high = daysTempRange.getHigh();
boolean withinPlan = plan.withinRange(low, high);

解决

改为传递整个对象。

1
boolean withinPlan = plan.withinRange(daysTempRange);

引入参数对象(Introduce Parameter Object)

问题

某些参数总是很自然地同时出现。

img

解决

以一个对象来取代这些参数。

img

扩展阅读

参考资料

翻译自:https://sourcemaking.com/refactoring/smells/oo-abusers

滥用面向对象(Object-Orientation Abusers)这组坏味道意味着:代码部分或完全地违背了面向对象编程原则。

Switch 声明

Switch 声明(Switch Statements)

你有一个复杂的 switch 语句或 if 序列语句。

img

问题原因

面向对象程序的一个最明显特征就是:少用 switchcase 语句。从本质上说,switch 语句的问题在于重复(if 序列也同样如此)。你常会发现 switch 语句散布于不同地点。如果要为它添加一个新的 case 子句,就必须找到所有 switch 语句并修改它们。面向对象中的多态概念可为此带来优雅的解决办法。

大多数时候,一看到 switch 语句,就应该考虑以多态来替换它。

解决方法

  • 问题是多态该出现在哪?switch 语句常常根据类型码进行选择,你要的是“与该类型码相关的函数或类”,所以应该运用 提炼函数(Extract Method)switch 语句提炼到一个独立函数中,再以 搬移函数(Move Method) 将它搬移到需要多态性的那个类里。
  • 如果你的 switch 是基于类型码来识别分支,这时可以运用 以子类取代类型码(Replace Type Code with Subclass)以状态/策略模式取代类型码(Replace Type Code with State/Strategy)
  • 一旦完成这样的继承结构后,就可以运用 以多态取代条件表达式(Replace Conditional with Polymorphism) 了。
  • 如果条件分支并不多并且它们使用不同参数调用相同的函数,多态就没必要了。在这种情况下,你可以运用 以明确函数取代参数(Replace Parameter with Explicit Methods)
  • 如果你的选择条件之一是 null,可以运用 引入 Null 对象(Introduce Null Object)

收益

  • 提升代码组织性。

img

何时忽略

  • 如果一个 switch 操作只是执行简单的行为,就没有重构的必要了。
  • switch 常被工厂设计模式族(工厂方法模式(Factory Method)抽象工厂模式(Abstract Factory))所使用,这种情况下也没必要重构。

重构方法说明

提炼函数(Extract Method)

问题

你有一段代码可以组织在一起。

1
2
3
4
5
6
7
void printOwing() {
printBanner();

//print details
System.out.println("name: " + name);
System.out.println("amount: " + getOutstanding());
}

解决

移动这段代码到一个新的函数中,使用函数的调用来替代老代码。

1
2
3
4
5
6
7
8
9
void printOwing() {
printBanner();
printDetails(getOutstanding());
}

void printDetails(double outstanding) {
System.out.println("name: " + name);
System.out.println("amount: " + outstanding);
}

搬移函数(Move Method)

问题

你的程序中,有个函数与其所驻类之外的另一个类进行更多交流:调用后者,或被后者调用。

img

解决

在该函数最常引用的类中建立一个有着类似行为的新函数。将旧函数变成一个单纯的委托函数,或是旧函数完全移除。

img

以子类取代类型码(Replace Type Code with Subclass)

问题

你有一个不可变的类型码,它会影响类的行为。

img

解决

以子类取代这个类型码。

img

以状态/策略模式取代类型码(Replace Type Code with State/Strategy)

问题

你有一个类型码,它会影响类的行为,但你无法通过继承消除它。

img

解决

以状态对象取代类型码。

img

以多态取代条件表达式(Replace Conditional with Polymorphism)

问题

你手上有个条件表达式,它根据对象类型的不同而选择不同的行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Bird {
//...
double getSpeed() {
switch (type) {
case EUROPEAN:
return getBaseSpeed();
case AFRICAN:
return getBaseSpeed() - getLoadFactor() * numberOfCoconuts;
case NORWEGIAN_BLUE:
return (isNailed) ? 0 : getBaseSpeed(voltage);
}
throw new RuntimeException("Should be unreachable");
}
}

解决

将这个条件表达式的每个分支放进一个子类内的覆写函数中,然后将原始函数声明为抽象函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
abstract class Bird {
//...
abstract double getSpeed();
}

class European extends Bird {
double getSpeed() {
return getBaseSpeed();
}
}
class African extends Bird {
double getSpeed() {
return getBaseSpeed() - getLoadFactor() * numberOfCoconuts;
}
}
class NorwegianBlue extends Bird {
double getSpeed() {
return (isNailed) ? 0 : getBaseSpeed(voltage);
}
}

// Somewhere in client code
speed = bird.getSpeed();

以明确函数取代参数(Replace Parameter with Explicit Methods)

问题

你有一个函数,其中完全取决于参数值而采取不同的行为。

1
2
3
4
5
6
7
8
9
10
11
void setValue(String name, int value) {
if (name.equals("height")) {
height = value;
return;
}
if (name.equals("width")) {
width = value;
return;
}
Assert.shouldNeverReachHere();
}

解决

针对该参数的每一个可能值,建立一个独立函数。

1
2
3
4
5
6
void setHeight(int arg) {
height = arg;
}
void setWidth(int arg) {
width = arg;
}

引入 Null 对象(Introduce Null Object)

问题

你需要再三检查某对象是否为 null。

1
2
3
4
5
6
if (customer == null) {
plan = BillingPlan.basic();
}
else {
plan = customer.getPlan();
}

解决

将 null 值替换为 null 对象。

1
2
3
4
5
6
7
8
9
10
11
12
class NullCustomer extends Customer {
Plan getPlan() {
return new NullPlan();
}
// Some other NULL functionality.
}

// Replace null values with Null-object.
customer = (order.customer != null) ? order.customer : new NullCustomer();

// Use Null-object as if it's normal subclass.
plan = customer.getPlan();

临时字段

临时字段(Temporary Field)的值只在特定环境下有意义,离开这个环境,它们就什么也不是了。

img

问题原因

有时你会看到这样的对象:其内某个实例变量仅为某种特定情况而设。这样的代码让人不易理解,因为你通常认为对象在所有时候都需要它的所有变量。在变量未被使用的情况下猜测当初设置目的,会让你发疯。
通常,临时字段是在某一算法需要大量输入时而创建。因此,为了避免函数有过多参数,程序员决定在类中创建这些数据的临时字段。这些临时字段仅仅在算法中使用,其他时候却毫无用处。
这种代码不好理解。你期望查看对象字段的数据,但是出于某种原因,它们总是为空。

解决方法

  • 可以通过 提炼类(Extract Class) 将临时字段和操作它们的所有代码提炼到一个单独的类中。此外,你可以运用 以函数对象取代函数(Replace Method with Method Object) 来实现同样的目的。
  • 引入 Null 对象(Introduce Null Object) 在“变量不合法”的情况下创建一个 null 对象,从而避免写出条件表达式。

img

收益

  • 更好的代码清晰度和组织性。

img

重构方法说明

提炼类(Extract Class)

问题

某个类做了不止一件事。

img

解决

建立一个新类,将相关的字段和函数从旧类搬移到新类。

img

以函数对象取代函数(Replace Method with Method Object)

问题

你有一个过长函数,它的局部变量交织在一起,以致于你无法应用提炼函数(Extract Method) 。

1
2
3
4
5
6
7
8
9
10
class Order {
//...
public double price() {
double primaryBasePrice;
double secondaryBasePrice;
double tertiaryBasePrice;
// long computation.
//...
}
}

解决

将函数移到一个独立的类中,使得局部变量成了这个类的字段。然后,你可以将函数分割成这个类中的多个函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Order {
//...
public double price() {
return new PriceCalculator(this).compute();
}
}

class PriceCalculator {
private double primaryBasePrice;
private double secondaryBasePrice;
private double tertiaryBasePrice;

public PriceCalculator(Order order) {
// copy relevant information from order object.
//...
}

public double compute() {
// long computation.
//...
}
}

引入 Null 对象(Introduce Null Object)

问题

你需要再三检查某对象是否为 null。

1
2
3
4
5
6
if (customer == null) {
plan = BillingPlan.basic();
}
else {
plan = customer.getPlan();
}

解决

将 null 值替换为 null 对象。

1
2
3
4
5
6
7
8
9
10
11
12
class NullCustomer extends Customer {
Plan getPlan() {
return new NullPlan();
}
// Some other NULL functionality.
}

// Replace null values with Null-object.
customer = (order.customer != null) ? order.customer : new NullCustomer();

// Use Null-object as if it's normal subclass.
plan = customer.getPlan();

异曲同工的类

异曲同工的类(Alternative Classes with Different Interfaces)

两个类中有着不同的函数,却在做着同一件事。

img

问题原因

这种情况往往是因为:创建这个类的程序员并不知道已经有实现这个功能的类存在了。

解决方法

  • 如果两个函数做同一件事,却有着不同的签名,请运用 函数改名(Rename Method) 根据它们的用途重新命名。
  • 运用 搬移函数(Move Method)添加参数(Add Parameter)令函数携带参数(Parameterize Method) 来使得方法的名称和实现一致。
  • 如果两个类仅有部分功能是重复的,尝试运用 提炼超类(Extract Superclass) 。这种情况下,已存在的类就成了超类。
  • 当最终选择并运用某种方法来重构后,也许你就能删除其中一个类了。

收益

  • 消除了不必要的重复代码,为代码瘦身了。
  • 代码更易读(不再需要猜测为什么要有两个功能相同的类)。

img

何时忽略

  • 有时合并类是不可能的,或者是如此困难以至于没有意义。例如:两个功能相似的类存在于不同的 lib 库中。

重构方法说明

函数改名(Rename Method)

问题

函数的名称未能恰当的揭示函数的用途。

1
2
3
class Person {
public String getsnm();
}

解决

修改函数名。

1
2
3
class Person {
public String getSecondName();
}

搬移函数(Move Method)

问题

你的程序中,有个函数与其所驻类之外的另一个类进行更多交流:调用后者,或被后者调用。

img

解决

在该函数最常引用的类中建立一个有着类似行为的新函数。将旧函数变成一个单纯的委托函数,或是旧函数完全移除。

img

添加参数(Add Parameter)

问题
某个函数需要从调用端得到更多信息。

1
2
3
class Customer {
public Contact getContact();
}

解决
为此函数添加一个对象函数,让改对象带进函数所需信息。

1
2
3
class Customer {
public Contact getContact(Date date);
}

令函数携带参数(Parameterize Method)

问题

若干函数做了类似的工作,但在函数本体中却包含了不同的值。

img
解决

建立单一函数,以参数表达哪些不同的值。

img

提炼超类(Extract Superclass)

问题

两个类有相似特性。

img

解决

为这两个类建立一个超类,将相同特性移至超类。

img

被拒绝的馈赠

被拒绝的馈赠(Refused Bequest)

子类仅仅使用父类中的部分方法和属性。其他来自父类的馈赠成为了累赘。

img

问题原因

有些人仅仅是想重用超类中的部分代码而创建了子类。但实际上超类和子类完全不同。

解决方法

  • 如果继承没有意义并且子类和父类之间确实没有共同点,可以运用 以委托取代继承(Replace Inheritance with Delegation) 消除继承。
  • 如果继承是适当的,则去除子类中不需要的字段和方法。运用 提炼超类(Extract Superclass) 将所有超类中对于子类有用的字段和函数提取出来,置入一个新的超类中,然后让两个类都继承自它。

img

收益

  • 提高代码的清晰度和组织性。

img

重构方法说明

以委托取代继承(Replace Inheritance with Delegation)

问题

某个子类只使用超类接口中的一部分,或是根本不需要继承而来的数据。

img

解决

  1. 在子类中新建一个字段用以保存超类;
  2. 调整子类函数,令它改而委托超类;
  3. 然后去掉两者之间的继承关系。

img

提炼超类(Extract Superclass)

问题

两个类有相似特性。

img

解决

为这两个类建立一个超类,将相同特性移至超类。

img

扩展阅读

参考资料

翻译自:https://sourcemaking.com/refactoring/smells/change-preventers

变革的障碍(Change Preventers)这组坏味道意味着:当你需要改变一处代码时,却发现不得不改变其他的地方。这使得程序开发变得复杂、代价高昂。

发散式变化

发散式变化(Divergent Change) 类似于 霰弹式修改(Shotgun Surgery) ,但实际上完全不同。发散式变化(Divergent Change) 是指一个类受多种变化的影响。霰弹式修改(Shotgun Surgery) 是指多种变化引发多个类相应的修改。

特征

你发现你想要修改一个函数,却必须要同时修改许多不相关的函数。例如,当你想要添加一个新的产品类型时,你需要同步修改对产品进行查找、显示、排序的函数。

img

问题原因

通常,这种发散式修改是由于编程结构不合理或者“复制-粘贴式编程”。

解决办法

  • 运用 提炼类(Extract Class) 拆分类的行为。

收益

  • 提高代码组织结构
  • 减少重复代码

重构方法说明

提炼类(Extract Class)

问题

某个类做了不止一件事。

img

解决

建立一个新类,将相关的字段和函数从旧类搬移到新类。

img

平行继承体系

平行继承体系(Parallel Inheritance Hierarchies) 其实是 霰弹式修改(Shotgun Surgery) 的特殊情况。

特征

每当你为某个类添加一个子类,必须同时为另一个类相应添加一个子类。这种情况的典型特征是:某个继承体系的类名前缀或类名后缀完全相同。

img

问题原因

起初的继承体系很小,随着不断添加新类,继承体系越来越大,也越来越难修改。

解决方法

  • 一般策略是:让一个继承体系的实例引用另一个继承体系的实例。如果再接再厉运用 搬移函数(Move Method)搬移字段(Move Field),就可以消除引用端的继承体系。

收益

  • 更好的代码组织
  • 减少重复代码

何时忽略

  • 有时具有并行类层次结构只是一种为了避免程序体系结构更混乱的方法。如果你发现尝试消除平行继承体系导致代码更加丑陋,那么你应该回滚你的修改。

重构方法说明

搬移函数(Move Method)

问题

你的程序中,有个函数与其所驻类之外的另一个类进行更多交流:调用后者,或被后者调用。

img

解决

在该函数最常引用的类中建立一个有着类似行为的新函数。将旧函数变成一个单纯的委托函数,或是旧函数完全移除。

img

搬移字段(Move Field)

问题

在你的程序中,某个字段被其所驻类之外的另一个类更多地用到。

img

解决

在目标类新建一个字段,修改源字段的所有用户,令他们改用新字段。

img

霰弹式修改

霰弹式修改(Shotgun Surgery) 类似于 发散式变化(Divergent Change) ,但实际上完全不同。发散式变化(Divergent Change) 是指一个类受多种变化的影响。霰弹式修改(Shotgun Surgery) 是指多种变化引发多个类相应的修改。

特征

任何修改都需要在许多不同类上做小幅度修改。

img

问题原因

一个单一的职责被拆分成大量的类。

解决方法

  • 运用搬移函数(Move Method)搬移字段(Move Field) 来搬移不同类中相同的行为到一个独立类中。如果没有适合存放搬移函数或字段的类,就创建一个新类。
  • 通常,可以运用 将类内联化(Inline Class) 将一些列相关行为放进同一个类。

img

收益

  • 更好的代码组织
  • 减少重复代码
  • 更易维护

img

重构方法说明

搬移函数(Move Method)

问题

你的程序中,有个函数与其所驻类之外的另一个类进行更多交流:调用后者,或被后者调用。

img

解决

在该函数最常引用的类中建立一个有着类似行为的新函数。将旧函数变成一个单纯的委托函数,或是旧函数完全移除。

img

搬移字段(Move Field)

问题

在你的程序中,某个字段被其所驻类之外的另一个类更多地用到。

img

解决

在目标类新建一个字段,修改源字段的所有用户,令他们改用新字段。

img

将类内联化(Inline Class)

问题

某个类没有做太多事情。

img

解决

将这个类的所有特性搬移到另一个类中,然后移除原类。

img

扩展阅读

参考资料