软件模块的耦合

软件模块的耦合

良好的软件模块的设计,需要遵守低耦合,高内聚。这将在代码维护中发挥重要的作用。本文将重点阐述七种耦合以及他们的区别,耦合程度由低到高:无直接耦合-->数据耦合--> 标记耦合 --> 控制耦合 --> 外部耦合 --> 公共耦合 -->内容耦合 。

无直接耦合

1.一组没有直接关系模块,这里是理想的状态。

数据耦合

1.通过基本数据联结,模块之间仅通过传递必要的基本数据值(整数、字符串等)进行通信。依赖最小化,接口清晰,修改影响小。是优秀的耦合类型。

2.代码示例:

python 复制代码
# 好的数据耦合
def calculate_area(width, height):  # 只传递必需的基本数据
    return width * height

area = calculate_area(10, 5)  # 调用简单清晰

标记耦合

1.通过数据结构联结。包含多余信息的"数据包裹,暴露了不必要的信息,产生隐含依赖。并且数据结构发生更改,被调用模块将重写。常见但不够理想的做法。理想的做法是转为数据耦合。

2.例子:

  • 订单模块 在创建订单时,只需要 userIdaddress
  • 调用方式createOrder(struct UserInfo user);
  • 解释 :调用者传递了整个 UserInfo 结构体。虽然功能上没问题,但订单模块现在"知道"了太多它本不需要知道的用户信息(如 email, birthDate, loyaltyPoints)。它和 UserInfo 这个数据结构形成了耦合。如果未来 UserInfo 结构改变(即使只是增加一个不相关的字段),createOrder 函数和订单模块可能都需要重新编译。

3.危害

降低可维护性:对数据结构的无关修改会产生"涟漪效应",导致依赖它的所有模块都需要重新检查、编译和测试,增加了维护成本。

降低可读性:函数签名 createOrder(UserInfo user) 不如 createOrder(int userId, Address addr) 清晰。后者一眼就能看出该函数需要什么。

增加错误风险:因为模块可以访问多余的数据,开发者可能会在无意中错误地使用了这些数据(例如,本应用 address,却误用了 email)。

降低复用性:订单模块与特定的 UserInfo 结构紧密绑定。如果想在另一个不使用 UserInfo 结构的项目中复用订单模块,会非常困难。

控制耦合

1.一个模块通过控制参数/标志/命令控制另一个模块的内部逻辑流程。调用模块需要知道被调用模块的逻辑,当被调用模块发生更改,例如加条件分支, 调用模块需要更改。

2.例子:点咖啡的诡异对话:

你:用"模式A"做咖啡

店员听到"模式A"后,内部执行一套复杂操作:

  1. 先磨豆子

  2. 如果周二就加奶油

  3. 如果下雨就少加冰

  4. ...

你实际上在远程控制店员的大脑决策流程!

3.代码示例:

python 复制代码
# 控制耦合 - 糟糕的设计
def process_order(order, special_mode):
    if special_mode == "MODE_A":
        # 执行10个步骤
    elif special_mode == "MODE_B":
        # 执行另一套逻辑
    # ...

# 调用者必须知道内部逻辑
process_order(order, "MODE_A")

4.控制耦合危害:

调用者需要知道被调用者的内部实现细节

被调用模块的行为难以预测

增加一个模式就要修改代码

改进:应该拆分成不同的函数,如 make_latte()、make_cappuccino()。

外部/通信耦合

模块间通过外部环境[软件之外]联结【如I/O将模块耦合到特定外部系统/设备, 数据库, 配置文件,格式规则,通信协议/接口】,共享输入或者输出。模块本身不更改外部环境,但外部环境的更改将影响所有模块,更改后可能需要重连或者重启动。

公共耦合

1.一组模块共享公共数据环境,各模块任意读写,立刻感知,容易造成数据混乱和程序bug。

2.外部耦合 vs. 公共耦合

这两个概念容易混淆,因为它们都涉及"共享"。按照笔者的理解:外部耦合为共享模块进程之的系统或者设备,或者文件规则。公共耦合为共享模块进程内部的内存数据。以下以办公室的不同共享方式为比喻,来阐述两种耦合的区别。

公共耦合 = 共享的可随意涂改的白板

  • 办公室有一块公共白板,每个人都可以随时上去写字、擦掉别人的字
  • 小明在上面写了个会议时间
  • 小红觉得时间不对,直接擦掉改了
  • 小刚又在上面画了个图表
  • 结果:白板内容乱七八糟,谁也不知道现在哪个信息是准确的

外部耦合 = 共享的打印机

  • 办公室所有人都用同一台打印机
  • 打印机有自己的设置:默认双面打印、A4纸型、特定页边距
  • 如果打印机设置改了(比如变成单面打印),所有人的打印效果都变了
  • 但没人能直接修改打印机硬件本身,只能使用它

本质区别对比表

维度 公共耦合 外部耦合
共享什么 同一个内存中的数据结构 同一个外部系统/环境
谁能修改 所有模块都能直接读写 外部实体控制,模块只能遵守(遵守规则)
修改方式 直接赋值、修改内存 通过配置、协议、接口间接影响
可见性 立即影响所有使用者 改变后,所有使用者下次访问时受影响
典型例子 全局变量、静态类字段、单例 文件格式、数据库模式、API协议、操作系统

公共耦合示例:全局记账本

python 复制代码
# 全局变量 - 公共数据区
company_account = {
    "balance": 10000,      # 公司总余额
    "transactions": []     # 交易记录
}

# 模块A:销售部门
def sales_department(amount):
    company_account["balance"] += amount
    company_account["transactions"].append(f"销售收入: +{amount}")
    # 问题:直接修改了全局数据

# 模块B:采购部门  
def purchase_department(amount):
    company_account["balance"] -= amount
    # 问题:可能和销售部门同时修改,导致数据不一致
    company_account["transactions"].append(f"采购支出: -{amount}")

# 模块C:财务部门
def finance_department():
    # 依赖全局数据,但不知道谁改了它
    print(f"当前余额: {company_account['balance']}")
    # 如果balance被意外修改,这里显示错误数据

问题特征:

  • 所有函数都能直接读写 company_account
  • 没有访问控制
  • 一个部门的错误会影响所有部门

外部耦合示例:共享数据库表结构

python 复制代码
# 所有模块都依赖同一个数据库表结构
# users表结构:
#   id (INT)
#   name (VARCHAR)
#   email (VARCHAR) 
#   created_at (DATETIME)

# 模块A:用户注册
def register_user(name, email):
    # SQL依赖特定的表结构
    sql = "INSERT INTO users (name, email, created_at) VALUES (%s, %s, NOW())"
    # 如果表结构改了,比如删除了email字段,这里就出错
    execute_sql(sql, [name, email])

# 模块B:查询用户
def get_user_report():
    # 同样依赖users表结构
    sql = "SELECT name, email FROM users WHERE created_at > '2024-01-01'"
    # 如果字段名改了,这里也出错
    return execute_sql(sql)

# 模块C:数据导出
def export_users():
    # 还是依赖同一个表结构
    sql = "SELECT * FROM users ORDER BY created_at"
    # 增加新字段可能破坏导出格式
    return execute_sql(sql)

问题特征:

  • 所有模块都依赖同一个外部约定(数据库表结构)
  • 不能直接修改数据库,但数据库的改动会影响所有模块。[数据库表的更改一般由DBA执行DDL语句进行操作]
  • 耦合的是接口/协议,不是内存数据

解决公共耦合

python 复制代码
# 错误:公共耦合
global_data = {"value": 0}

# 方案1:依赖注入(推荐)
class Department:
    def __init__(self, account):
        self.account = account  # 传入依赖,不直接访问全局
    
# 方案2:不可变数据
from frozendict import frozendict
shared_config = frozendict({"version": "1.0"})  # 只能读,不能改

# 方案3:访问控制
class AccountManager:
    def __init__(self):
        self._balance = 10000  # 私有
    
    def get_balance(self):  # 只读接口
        return self._balance
    
    def update_balance(self, amount, reason):  # 受控修改
        # 记录日志、验证等
        self._balance += amount

解决外部耦合

python 复制代码
# 错误:硬编码外部依赖
def process_data():
    data = read_xml("data.xml")  # 硬编码文件格式
    # 处理XML...

# 方案1:抽象接口
class DataReader:
    def read(self, filename):
        pass

class XMLReader(DataReader):
    def read(self, filename):
        # 读取XML

class JSONReader(DataReader):
    def read(self, filename):
        # 读取JSON

# 方案2:配置化
class Config:
    FILE_FORMAT = os.getenv("DATA_FORMAT", "json")  # 从环境变量读取

# 方案3:适配器模式
class DataAdapter:
    def __init__(self, format_type):
        self.format_type = format_type
    
    def load(self, filename):
        if self.format_type == "xml":
            return XMLReader().read(filename)
        elif self.format_type == "json":
            return JSONReader().read(filename)

在实际项目中:

  • 公共耦合 几乎总是设计错误,应该立即重构
  • 外部耦合 有时不可避免(如使用行业标准),但应通过[抽象]来隔离变化

记住这个关键区别:公共耦合是内部数据共享混乱,外部耦合是外部依赖约束太紧。

内容耦合

(1)一个模块直接访问另一个模块的内部数据。

(2)一个模块不通过正常入口转到另一模块内部。

(3)两个模块有一部分程序代码重迭。

(4)一个模块有多个入口。

内容耦合是最糟糕的耦合类型,就像是直接侵入别人大脑进行控制。

情况1:直接访问另一个模块的内部数据。就像直接打开同事的抽屉,拿走他私藏的零食,还修改了他的私人日记。

c 复制代码
// module_a.c - 被侵入的模块
#include <stdio.h>

// 这是模块A的私有内部数据,外界不该知道
static int secret_counter = 42;  // static表示模块私有的
static char private_buffer[100] = "机密信息";

void public_function() {
    printf("正常执行,counter=%d\n", secret_counter);
}

// module_b.c - 侵入者模块
#include <stdio.h>

// 邪恶操作:声明要访问module_a的私有变量
extern int secret_counter;  // 用extern声明外部变量
extern char private_buffer[];

void hack_module_a() {
    printf("我是模块B,我要搞破坏!\n");
    
    // 直接修改module_a的私有数据
    secret_counter = 999;  // 本来应该是module_a内部控制的
    
    // 甚至修改module_a的私有缓冲区
    strcpy(private_buffer, "我被黑了!");
    
    printf("成功侵入,改了counter和buffer\n");
}

// main.c
int main() {
    public_function();  // 输出:正常执行,counter=42
    hack_module_a();    // 模块B侵入修改
    public_function();  // 输出:正常执行,counter=999  (数据被篡改!)
    return 0;
}

破坏性 :模块B完全绕过了模块A的封装,直接操作其内部状态。如果模块A改变内部实现(比如重命名secret_counter),模块B就会崩溃。

情况2:不通过正常入口转到另一模块内部。就像不在商店正门进入,而是翻窗户直接跳到柜台后面开始操作收银机。

代码示例(汇编/GOTO版本)

c 复制代码
// 假设这是模块A
void process_order() {
    start:
        printf("开始处理订单...\n");
        // ... 一些订单处理逻辑 ...
        
    middle_of_function:  // 这是一个标签,不是正常的调用入口
        printf("正在计算价格...\n");
        // ... 价格计算逻辑 ...
        
    end:
        printf("订单完成\n");
}

// 模块B的邪恶操作
void evil_jump() {
    printf("我要直接跳到模块A中间执行!\n");
    
    // 在一些古老/底层的编程方式中,可能这样跳转
    // 这完全破坏了函数调用的堆栈和上下文
    // goto middle_of_function;  // 如果允许的话
    
    // 现代语言通常禁止这种跨函数的goto
    // 但在汇编中完全可能:
    // jmp middle_of_function
}

现代语言中的变种(通过反射/指针黑客)

python 复制代码
# Python中通过修改函数字节码或利用反射进行破坏
import types

# 模块A的正常函数
def calculate_discount(price):
    """计算折扣"""
    print(f"计算价格: {price}")
    discount = price * 0.9  # 打9折
    return discount

# 模块B的邪恶操作
def hijack_function():
    """劫持模块A的函数"""
    # 获取函数的代码对象
    original_code = calculate_discount.__code__
    
    # 创建恶意代码(实际中更复杂)
    # 这里简化表示:直接修改函数行为
    def evil_calculate_discount(price):
        print("哈哈,我劫持了这个函数!")
        return price * 0.5  # 改成5折,完全破坏业务逻辑
    
    # 替换函数实现
    calculate_discount.__code__ = evil_calculate_discount.__code__

# 测试
print("正常调用:", calculate_discount(100))  # 输出: 90.0
hijack_function()
print("被劫持后:", calculate_discount(100))  # 输出: 50.0

破坏性:跳过了函数的初始化代码,可能导致变量未初始化、堆栈混乱等问题。

情况3:两个模块有一部分程序代码重迭。就像两个部门共用同一本工作手册,其中一个部门撕掉了几页,另一个部门就用不了了。内存共享代码。

c 复制代码
// 在早期计算机内存紧张的时代,可能会这样做
// 假设有两个函数共享同一段机器码

// 函数A和函数B共享前10条指令
// 在汇编中可能这样写:
/*
function_a:
    push bp
    mov bp, sp
    ; ... 共享的前10条指令 ...
    ; 然后分支
    jmp unique_part_a

function_b:
    push bp
    mov bp, sp
    ; ... 和function_a完全相同的前10条指令 ...
    jmp unique_part_b

; 内存中实际上只存了一份前10条指令
; function_a和function_b指向同一块内存
*/

// 现代高级语言中很少见,但可以用函数指针模拟这种"共享"
void shared_code_part() {
    printf("这是共享的代码部分\n");
}

void function_a() {
    shared_code_part();
    printf("函数A特有的部分\n");
}

void function_b() {
    shared_code_part();  // 重用同一段代码
    printf("函数B特有的部分\n");
}

// 破坏性的情况:如果shared_code_part被修改
// 两个函数都会受到影响,而且可能互相干扰

破坏性:一个模块的修改会直接影响另一个模块,因为它们共享同一段物理代码。

情况4:一个模块有多个入口。就像一个自动售货机,除了正常的投币口,侧面还有一个维修口,顾客可以从维修口直接拿商品。

c 复制代码
// 模块:计算器
// 不正常的多个入口点

#include <stdio.h>

// 正常的入口点
void calculator() {
    int choice;
    float a, b;
    
    start:  // 入口1:函数开头
    printf("\n=== 计算器 ===\n");
    printf("1. 加法\n2. 减法\n");
    printf("选择: ");
    scanf("%d", &choice);
    
    // 邪恶的第二个入口点
    if (choice == 666) {
        goto secret_entry;  // 跳转到函数中间
    }
    
    printf("输入两个数: ");
    scanf("%f %f", &a, &b);
    
    if (choice == 1) {
        printf("结果: %.2f\n", a + b);
    } else if (choice == 2) {
        printf("结果: %.2f\n", a - b);
    }
    
    return;  // 正常返回
    
secret_entry:  // 入口2:函数中间的标签
    printf("你发现了秘密入口!\n");
    printf("直接执行乘法...\n");
    
    // 跳过输入,使用预设值
    printf("结果: %.2f\n", 10.0 * 20.0);
    
    // 跳回正常流程
    goto start;
}

// 更糟糕的情况:函数指针指向函数中间
void calculator_multi_entry() {
    // 函数体...
}

void secret_functionality() {
    printf("秘密功能\n");
}

int main() {
    // 正常的函数调用
    calculator();
    
    // 邪恶的调用方式:获取函数内部地址并跳转
    // 在一些底层编程中,可以获取标签地址
    // void (*secret_ptr)() = &secret_entry;  // 如果允许获取标签地址
    // secret_ptr();  // 直接跳到函数中间执行
    
    return 0;
}

破坏性:破坏了函数的单一职责原则,使得函数状态难以管理,调用者需要知道函数内部的实现细节。

现实中的内容耦合(虽然少见但存在)

  1. 直接内存修改
c 复制代码
// 游戏外挂:直接修改游戏内存
// 外挂程序找到游戏中的金钱地址,直接写入99999
// 这就是典型的内容耦合:外挂依赖于游戏的内存布局
  1. 通过反射破坏封装
java 复制代码
// Java反射可以访问私有字段
public class BankAccount {
    private double balance = 1000;  // 私有字段
    
    public double getBalance() {
        return balance;
    }
}

// 攻击代码
Field balanceField = BankAccount.class.getDeclaredField("balance");
balanceField.setAccessible(true);  // 破坏封装性!
balanceField.set(account, 999999);  // 直接修改私有字段
  1. 共享内存/消息队列的滥用
python 复制代码
# 两个进程通过共享内存通信
# 进程A直接把内部数据结构指针给进程B
# 进程B直接修改这些数据,绕过所有安全检查

如何避免内容耦合?

  1. 严格遵守封装原则
    • 所有数据私有化
    • 通过公共方法访问数据。

在面向对象中,特征之一即为封装,遵循信息隐蔽原则。这里的'信息隐蔽'不是隐私,而实为了降低模块的耦合,提升代码的维护。

  1. 使用设计模式

    java 复制代码
    // 使用观察者模式而不是直接访问
    class Subject {
        private List<Observer> observers = new ArrayList<>();
        
        // 不暴露内部列表,只提供订阅方法
        public void addObserver(Observer o) {
            observers.add(o);
        }
    }
  2. 避免使用破坏封装的语言特性

    • 慎用反射
    • 避免直接内存操作
    • 不使用goto(特别是跨函数)
  3. 单一入口原则

    python 复制代码
    # 好的设计:单一入口
    class PaymentProcessor:
        def process(self, amount, method):
            # 只有一个公共入口
            if method == "credit":
                return self._process_credit(amount)
            elif method == "paypal":
                return self._process_paypal(amount)
        
        def _process_credit(self, amount):  # 私有方法
            # 实现细节
            
        def _process_paypal(self, amount):  # 私有方法
            # 实现细节

内容耦合的识别标志

标志 例子
使用extern访问其他模块的静态变量 extern int other_module_private;
使用反射访问私有成员 field.setAccessible(true)
函数内部有多个goto标签可作为入口 start: middle: end:
两个函数共享全局变量进行隐式通信 全局变量作为"后门"
使用函数指针指向函数中间地址 void (*p)() = &label_in_middle;

记住:内容耦合是软件工程中的"七宗罪"之一,一旦发现就应该立即重构!

最后

在耦合谱系中的位置

耦合度从低到高通常排列为:

  1. 无直接耦合 -> 2. 数据耦合 -> 3. 标记耦合 -> 4. 控制耦合 -> 5. 外部耦合 -> 6. 公共耦合 -> 7. 内容耦合

标记耦合 处于中低水平。在现代面向对象编程中,它非常普遍(例如,传递一个对象作为参数),通常被认为是一种 "可接受的妥协" ,尤其是在模块边界清晰、且数据结构相对稳定的情况下。但它仍然是代码设计中需要警惕的信号,思考接口是否可以进一步精简。优秀的软件设计应致力于向数据耦合 靠拢,使模块间的连接尽可能简洁、明确。控制耦合 应该遵循单一职责原则进行分离。外部耦合 有时不可避免(如使用行业标准),但应通过[抽象]来隔离变化。公共耦合内容耦合几乎总是设计错误,应该立即重构。


愿你我都能在各自的领域里不断成长,勇敢追求梦想,同时也保持对世界的好奇与善意!

相关推荐
Justice Young17 小时前
软件工程笔记第三章:结构化分析与设计
软件工程
workflower19 小时前
和测试角色相关的问题
软件工程·软件构建·开源软件·uml·软件需求
九成宫20 小时前
计算机网络期末复习——第2章:应用层 Part Two
笔记·计算机网络·软件工程
九成宫20 小时前
计算机网络期末复习——第1章:计算机网络和因特网
笔记·计算机网络·软件工程
simon_skywalker1 天前
软件工程(一) 软件生命周期
软件工程
記億揺晃着的那天1 天前
AI 时代的软件工程:升级,而非消亡
人工智能·ai·软件工程·vibe coding
Justice Young2 天前
软件工程第八章:编码与测试
软件工程
Justice Young2 天前
软件工程第二章:软件生存周期与软件过程
软件工程
行业探路者2 天前
二维码制作工具使用指南:如何利用电脑摄像头轻松扫描和生成图片二维码
大数据·人工智能·学习·产品运营·软件工程