在软件总体设计阶段,除了追求"高内聚、低耦合"这个大原则外,还有7条经典的启发式规则(由著名软件工程学家Myers等人提出),用于指导模块划分和结构优化。下面逐条解释并举例。
规则1:改进软件结构,提高模块独立性
含义:初步划分出的结构往往不完美,需要审查、分解或合并模块,力求降低耦合、提高内聚。
具体做法:
找出耦合过高的模块,拆分或重构
找出内聚过低的模块,合并或重新划分
反复迭代优化结构
新闻管理系统举例
改进前(耦合高、内聚低):
public class NewsHandler {
public void handle(String type, Object data) {
if (type.equals("add")) {
// 添加新闻 + 校验 + 保存 + 发通知
} else if (type.equals("delete")) {
// 删除新闻 + 校验权限 + 删除评论
} else if (type.equals("query")) {
// 查询新闻 + 分页 + 排序
}
}
}
问题:逻辑内聚 + 控制耦合,一个模块干了所有事。
改进后(拆分模块,功能内聚 + 数据耦合):
public class NewsAddService {
public void add(News news) { ... }
}
public class NewsDeleteService {
public void delete(int newsId) { ... }
}
public class NewsQueryService {
public List<News> query(QueryCondition cond) { ... }
}
改进效果:
每个模块只做一件事(功能内聚)
模块间通过简单参数通信(数据耦合)
规则2:模块规模应该适中
含义:一个模块不应过大,最好能写在一页纸内(通常不超过60行语句)。过大难以理解、测试和维护;过小则模块数量膨胀,调用关系复杂。
判断标准:
超过60行 → 考虑拆分
少于10行 → 考虑合并(除非是高度复用的工具方法)
新闻管理系统举例
过大模块(需拆分):
public class NewsService {
public void processNews(News news) {
// 1. 校验标题(20行)
// 2. 校验正文(20行)
// 3. 校验图片(15行)
// 4. 保存数据库(15行)
// 5. 发送通知(15行)
// 6. 记录日志(10行)
// 总共约95行,明显过大
}
}
拆分后:
public class NewsValidator { // 约40行,负责校验
public ValidationResult validate(News news) { ... }
}
public class NewsRepository { // 约20行,负责存储
public void save(News news) { ... }
}
public class NewsNotifier { // 约15行,负责通知
public void notifyAuthor(int newsId) { ... }
}
public class NewsLogger { // 约10行,负责日志
public void log(String action) { ... }
}
改进效果:每个模块规模适中,职责清晰,易于测试。
规则3:深度、宽度、扇出和扇入都应适当
含义:软件结构是一个层次系统,需要关注四个指标:
| 指标 | 含义 | 建议值 |
|---|---|---|
| 深度 | 控制的层数 | 3~7层,不宜过深 |
| 宽度 | 同一层模块总数的最大值 | 不宜过宽 |
| 扇出 | 一个模块直接调用的下级模块数 | 3~5个,不宜过多 |
| 扇入 | 一个模块被多少个上级模块调用 | 越大越好(复用性高) |
新闻管理系统举例
结构图:
新闻管理系统(第1层)
┌──────┬────────┬──────┐
用户管理 新闻采编 新闻发布 新闻浏览 (第2层,宽度=4)
│
┌───────┼───────┐
录入模块 编辑模块 查询模块 (第3层,宽度=3)
│
┌──┼─────┬─────┐
标题 正文 图片 分类 (第4层,宽度=4)
分析:
深度 = 4层(合理,3~7之间)
宽度 = 4(第2层,合理)
扇出
扇入:数据访问模块被录入、编辑、查询、删除等多个模块调用,扇入高,复用性好
反例(结构不合理):
新闻管理系统
├── 模块A
│ ├── 模块B
│ │ ├── 模块C
│ │ │ ├── 模块D
│ │ │ │ ├── 模块E ← 深度过深,7层以上
问题:深度过深,调用链长,难以理解和调试。
优化建议:
深度过深 → 合并中间层
扇出过大 → 增加中间层分解
扇入过小 → 考虑是否该合并
规则4:模块的作用域应该在控制域之内
含义:
作用域:受该模块内一个判定(如if/switch)影响的所有模块的集合。
控制域:模块本身及其所有直接或间接从属的模块的集合。
规则:作用域应 ⊆ 控制域。即一个判定的影响范围,不应超出该模块的控制范围。
为什么:如果作用域超出控制域,说明有模块受该判定影响,但却不受该模块控制,导致耦合增加、逻辑混乱。
新闻管理系统举例
反例(违反规则):
public class NewsPublishService { // 控制域:本模块 + 审核模块 + 存储模块
public void publish(News news) {
if (news.getStatus() == 2) { // 判定
// 已审核,直接发布
}
}
}
public class NewsCommentService { // 不在 NewsPublishService 的控制域内
public void comment(int newsId) {
// 却依赖 news.getStatus() == 2 这个判定
if (news.getStatus() == 2) {
// 只有已发布的新闻才能评论
}
}
}
问题:NewsPublishService 里的判定 status == 2 影响了 NewsCommentService,但后者不受前者控制,违反规则。
改进方案:将判定上移到共同的父模块,或封装成独立的状态判定模块。
public class NewsStatusChecker { // 独立模块,统一判定
public boolean isPublished(News news) {
return news.getStatus() == 2;
}
}
// 两个模块都调用它,判定在控制域内
规则5:力争降低模块接口的复杂程度
含义:接口复杂是软件出错的主要原因之一。模块接口应信息传递简单,且与模块功能一致。
具体做法:
参数尽量少(1~3个为宜)
参数类型尽量简单(避免传复杂对象)
接口命名清晰,反映功能
避免通过参数传递控制信息
新闻管理系统举例
接口复杂(差):
public void processNews(
String title, String content, int categoryId,
int authorId, String status, Date publishTime,
boolean isTop, boolean isRecommend,
String tags, String coverImage,
int viewCount, int commentCount, int likeCount
) { ... }
// 13个参数,接口极其复杂,容易传错顺序
接口简单(好):
public void processNews(News news) { ... }
// 只传一个对象,参数简单清晰
或者更细粒度:
public void publish(int newsId, Date publishTime) { ... }
// 只传必要参数,功能明确
接口不一致(差):
public void handleUser(String name, int age, String email, int type) { ... }
// type 参数是控制信息,不应出现在接口中
改进:
public void addUser(User user) { ... }
public void deleteUser(int userId) { ... }
// 拆分方法,接口与功能一致,无控制参数
规则6:设计单入口单出口的模块
含义:模块应只有一个入口和一个出口。这条规则的核心目的是避免内容耦合,让软件易于理解和维护。
为什么:
单入口:从顶部进入,逻辑清晰
单出口:从底部退出,流程明确
多入口多出口:容易产生跳转混乱,是内容耦合的温床
新闻管理系统举例
多入口多出口(差):
public class NewsValidator {
public void validate(String title) {
if (title == null) {
System.out.println("标题为空");
return; // 出口1
}
if (title.length() > 100) {
System.out.println("标题过长");
return; // 出口2
}
if (title.contains("<")) {
System.out.println("标题含非法字符");
return; // 出口3
}
System.out.println("校验通过"); // 出口4
}
}
问题:多个 return,流程分散,调用者不知道从哪退出。
单入口单出口(好):
public class NewsValidator {
public ValidationResult validate(String title) {
ValidationResult result = ValidationResult.success();
if (title == null || title.trim().isEmpty()) {
result = ValidationResult.fail("标题不能为空");
} else if (title.length() > 100) {
result = ValidationResult.fail("标题过长");
} else if (title.contains("<")) {
result = ValidationResult.fail("标题含非法字符");
}
return result; // 唯一出口
}
}
改进效果:逻辑清晰,只有一个出口,易于理解和测试。
注意:现代编程中,适度的提前 return 是可接受的(如"卫语句"),但核心原则是避免通过 goto、异常跳转等造成流程混乱。
规则7:模块功能应该可以预测
含义:模块的功能应该明确、可预测——相同输入产生相同输出,不依赖外部状态或隐藏条件。但也要防止功能过分局限(只处理特例,缺乏通用性)。
具体做法:
功能单一明确,避免"看情况"行为
避免依赖全局变量、时间、随机数等不确定因素
保持一定的通用性,避免硬编码特例
新闻管理系统举例
功能不可预测(差):
public class NewsFormatter {
public String format(String content) {
if (new Date().getHours() < 12) {
return "【早报】" + content; // 依赖当前时间
} else {
return "【晚报】" + content;
}
}
}
问题:相同输入 content,上午和下午返回不同结果,功能不可预测。
功能可预测(好):
public class NewsFormatter {
public String format(String content, String type) {
return "【" + type + "】" + content;
}
}
// 相同输入 → 相同输出,功能可预测
功能过分局限(差):
public class NewsTitleValidator {
public boolean validate(String title) {
// 只处理"今日头条"这一种情况
return title.equals("今日头条");
}
}
问题:只处理特例,通用性差。
改进(通用但可预测):
public class NewsTitleValidator {
public ValidationResult validate(String title) {
// 通用规则:非空、长度、非法字符
// 相同输入 → 相同输出
}
}
七条规则总结表
| 规则 | 核心要点 | 新闻管理系统举例 |
|---|---|---|
| 1. 提高模块独立性 | 降耦合、提内聚 | 把 NewsHandler 拆成 Add/Delete/Query 三个模块 |
| 2. 模块规模适中 | 不超过60行 | NewsService 95行 → 拆成 Validator/Repository/Notifier |
| 3. 深度宽度扇出扇入适当 | 深度3~7,扇出3~5 | 结构图4层,采编模块扇出=3 |
| 4. 作用域在控制域内 | 判定的影响不超出控制 | status==2 判定封装成 NewsStatusChecker |
| 5. 降低接口复杂度 | 参数少、简单、与功能一致 | 13个参数 → 传 News 对象 |
| 6. 单入口单出口 | 避免内容耦合 | 多个 return → 单一 return result |
| 7. 功能可预测 | 相同输入相同输出 | 不依赖时间,改为传 type 参数 |
一句话记忆
独立性要高,规模要适中,层次要合理,作用域要受控,接口要简单,出入口要单一,功能要可预测。
这7条启发规则是总体设计阶段优化软件结构的"经验法则",与"高内聚低耦合"的总原则相辅相成,共同指导软件工程师设计出结构清晰、易于维护
