目录
在软件设计中,耦合指的是不同模块或组件之间的依赖程度。降低耦合是软件设计中的一个重要目标,因为它有助于提高系统的可维护性、可扩展性和重用性。
根据模块间依赖的紧密程度,耦合可以分为七个不同的等级。
一、无直接耦合
1.指两个模块之间没有直接的关系,它们分别从属于不同模块的控制与调用,它们之间不传递任何信息。
2.模块间耦合性最弱,模块独立性最高。
二、数据耦合
1.指两个模块之间有调用关系,传递的是简单的数据值,相当于高级语言中的值传递
2.例如:某企业管理信息系统中,采购子系统根据材料价格、数量等信息计算采购的金额,并给财务子系统传递采购金额、收款方和采购日期等信息,则这两个子系统之间的耦合类型为数据耦合。
示例代码:
下面这代码描述了采购系统与财务系统之间通过简单类型数据进行交互的场景,这就属于数据耦合。
using System;
namespace SimpleDataCouplingExample
{
// 采购子系统
public class PurchaseSubsystem
{
// 计算采购金额
public void CalculatePurchaseAmount(decimal price, int quantity, string payee, out decimal amount, out DateTime purchaseDate)
{
amount = price * quantity;
purchaseDate = DateTime.Now;
Console.WriteLine("采购子系统:计算采购金额完成。");
Console.WriteLine($"采购金额:{amount}, 收款方:{payee}, 采购日期:{purchaseDate}");
}
}
// 财务子系统
public class FinanceSubsystem
{
// 处理采购信息
public void ProcessPurchaseInfo(decimal amount, string payee, DateTime purchaseDate)
{
Console.WriteLine("财务子系统:接收到采购信息并处理。");
Console.WriteLine($"采购金额:{amount}, 收款方:{payee}, 采购日期:{purchaseDate}");
// 这里可以添加更多财务处理逻辑,例如生成账单或记录到数据库
}
}
// 主程序
class Program
{
static void Main(string[] args)
{
// 初始化采购子系统和财务子系统
PurchaseSubsystem purchaseSubsystem = new PurchaseSubsystem();
FinanceSubsystem financeSubsystem = new FinanceSubsystem();
// 模拟采购操作
decimal price = 100.0m; // 材料单价
int quantity = 5; // 采购数量
string payee = "供应商A"; // 收款方
// 定义变量以接收计算结果
decimal amount;
DateTime purchaseDate;
// 调用采购子系统计算采购金额
purchaseSubsystem.CalculatePurchaseAmount(price, quantity, payee, out amount, out purchaseDate);
// 将采购信息传递给财务子系统进行处理
financeSubsystem.ProcessPurchaseInfo(amount, payee, purchaseDate);
Console.WriteLine("数据耦合示例运行完毕!");
}
}
}
三、标记耦合(也叫特征耦合)
指两个模块之间传递的是数据结构,如:数组、结构、对象等复杂数据类型。
示例代码:
下面这代码描述了采购系统与财务系统之间通过传递对象进行交互的场景,这就属于标记耦合。
using System;
namespace StampCouplingExample
{
// 定义一个采购信息的类
public class PurchaseInfo
{
public decimal Amount { get; set; } // 采购金额
public string Payee { get; set; } // 收款方
public DateTime PurchaseDate { get; set; } // 采购日期
}
// 采购系统
public class PurchaseSystem
{
// 计算采购金额并返回采购信息
public PurchaseInfo CalculatePurchase(decimal price, int quantity, string payee)
{
Console.WriteLine("采购系统:计算采购金额...");
var purchaseInfo = new PurchaseInfo
{
Amount = price * quantity,
Payee = payee,
PurchaseDate = DateTime.Now
};
Console.WriteLine($"采购金额: {purchaseInfo.Amount}, 收款方: {purchaseInfo.Payee}, 采购日期: {purchaseInfo.PurchaseDate}");
return purchaseInfo; // 返回包含采购信息的复杂数据结构
}
}
// 财务系统
public class FinanceSystem
{
// 处理采购信息
public void ProcessPurchase(PurchaseInfo purchaseInfo)
{
Console.WriteLine("\n财务系统:处理采购信息...");
if (purchaseInfo == null)
{
Console.WriteLine("没有采购信息可供处理。");
return;
}
Console.WriteLine($"生成账单:采购金额为 {purchaseInfo.Amount},收款方为 {purchaseInfo.Payee},采购日期为 {purchaseInfo.PurchaseDate}");
// 这里可以添加更多财务处理逻辑,例如记录到数据库或生成报表
}
}
// 主程序
class Program
{
static void Main(string[] args)
{
// 初始化采购系统和财务系统
PurchaseSystem purchaseSystem = new PurchaseSystem();
FinanceSystem financeSystem = new FinanceSystem();
// 模拟采购操作
decimal price = 100.0m; // 材料单价
int quantity = 5; // 采购数量
string payee = "供应商A"; // 收款方
// 调用采购系统计算采购金额
PurchaseInfo purchaseInfo = purchaseSystem.CalculatePurchase(price, quantity, payee);
// 将采购信息传递给财务系统进行处理
financeSystem.ProcessPurchase(purchaseInfo);
Console.WriteLine("标记耦合示例运行完毕!");
}
}
}
在这个例子中,采购系统与财务系统之间传递的是对象purchaseInfo,而不是简单类型,这就导致财务系统对采购系统形成了依赖,增加了耦合度。
四、控制耦合
1.指一个模块调用另一个模块时,传递的是控制变量而不是数据信息,被调用模块通过该控制变量的值有选择地执行模块内的某一功能。
2.被调用模块应具有多个功能,哪个功能起作用受调控模块控制。
模块之间传递信息中包含用于控制模块内部的信息被称为控制耦合。控制耦合可能会导致模块之间控制逻辑相互交织,逻辑之间相互影响,非常不利于代码维护。
示例代码:
下面这段代码描述了 processOrder 方法如何根据传入的订单状态参数 (orderState) 调用不同的私有方法来处理订单。这是一个典型的控制耦合的例子,因为 processOrder 方法的行为完全取决于传入的 orderState 参数。
using System;
public class OrderProcessor
{
public void processOrder(String orderState, Order order)
{
if ("paid".equals(orderState))
{
handlePaidOrder(order);
}
else if ("pending".equals(orderState))
{
handlePendingOrder(order);
}
else if ("shipped".equals(orderState))
{
handleShippedOrder(order);
}
else
{
handleErrorOrder(order);
}
}
private void handlePaidOrder(Order order)
{
// 处理已支付订单的逻辑
System.out.println("Processing paid order: " + order.getId());
}
private void handlePendingOrder(Order order)
{
// 处理待支付订单的逻辑
System.out.println("Processing pending order: " + order.getId());
}
private void handleShippedOrder(Order order)
{
// 处理已发货订单的逻辑
System.out.println("Processing shipped order: " + order.getId());
}
private void handleErrorOrder(Order order)
{
// 处理错误状态订单的逻辑
System.out.println("Handling error order: " + order.getId());
}
}
在这个例子中,processOrder 方法接收了一个 orderState 参数,它是一个字符串,表示订单的状态。根据这个状态参数的不同值,processOrder 方法会调用不同的私有方法来处理订单。这就是典型的控制耦合,因为 processOrder 方法的行为完全取决于传入的 orderState 参数。
五、外部耦合
外部耦合指的是多个模块共同与同一个外部环境因素(如共享的通信协议、设备接口、硬件设备、文件系统、数据库等)发生关联,这些模块通过这个外部因素进行数据交互或者相互影响。
注:有资料将多个模块共同依赖一个全局变量也归于外部耦合,这种情况我更倾向于公共耦合。
示例代码:
假设我们正在设计一个简单的 日志记录系统,其中包含两个模块:
日志写入模块:负责将日志信息写入到一个全局的日志文件中。
日志读取模块:负责从同一个全局的日志文件中读取日志信息。
在这个例子中,两个模块都依赖于外部的全局文件(log.txt),并通过该文件进行交互,从而形成外部耦合。
using System;
using System.IO;
namespace ExternalCouplingExample
{
// 日志写入模块
public class LogWriter
{
private static string logFilePath = "log.txt"; // 全局日志文件路径
// 写入日志信息到文件
public void WriteLog(string message)
{
Console.WriteLine("日志写入模块:写入日志...");
using (StreamWriter writer = new StreamWriter(logFilePath, true)) // 追加模式写入
{
writer.WriteLine($"{DateTime.Now}: {message}");
}
Console.WriteLine("日志已写入。\n");
}
}
// 日志读取模块
public class LogReader
{
private static string logFilePath = "log.txt"; // 全局日志文件路径
// 从文件中读取日志信息
public void ReadLogs()
{
Console.WriteLine("日志读取模块:读取日志...");
if (!File.Exists(logFilePath))
{
Console.WriteLine("日志文件不存在。\n");
return;
}
using (StreamReader reader = new StreamReader(logFilePath))
{
string line;
while ((line = reader.ReadLine()) != null)
{
Console.WriteLine(line);
}
}
Console.WriteLine("日志读取完成。\n");
}
}
// 主程序
class Program
{
static void Main(string[] args)
{
// 初始化日志写入模块和日志读取模块
LogWriter logWriter = new LogWriter();
LogReader logReader = new LogReader();
// 模拟写入日志
logWriter.WriteLog("用户登录系统。");
logWriter.WriteLog("用户提交了一个订单。");
// 模拟读取日志
logReader.ReadLogs();
Console.WriteLine("外部耦合示例运行完毕!");
}
}
}
六、公共耦合
公共耦合(Common Coupling)是指多个软件模块共享同一个全局数据区(如全局变量、公共内存等),并通过该共享数据直接交互。这种耦合方式的特点是:
- 多个模块读写同一数据,导致模块间存在隐式依赖。
- 一个模块对数据的修改会影响其他模块的行为,增加了系统的复杂性和维护难度。
应尽量避免此类设计(尤其是可写全局变量),可通过封装、依赖注入等方式降低耦合度。
示例代码:
下面示例演示了多个模块共同依赖一个全局性的数据结构(全局参数 globalDiscountRate(折扣率)),这属于典型的公共耦合。
using System;
namespace GlobalParameterExample
{
// 定义全局参数作为公共数据环境
public static class GlobalSettings
{
public static double GlobalDiscountRate = 0.1; // 默认折扣率为 10%
}
// 管理员模块:用于更新折扣率
public class AdminModule
{
public void UpdateDiscountRate(double newDiscountRate)
{
if (newDiscountRate >= 0 && newDiscountRate <= 1)
{
GlobalSettings.GlobalDiscountRate = newDiscountRate; // 更新全局折扣率
Console.WriteLine($"管理员已将折扣率更新为 {newDiscountRate * 100}%");
}
else
{
Console.WriteLine("折扣率必须在 0 到 1 之间!");
}
}
}
// 商品价格计算模块:根据折扣率计算最终价格
public class PriceCalculator
{
public double CalculateFinalPrice(double originalPrice)
{
double discountRate = GlobalSettings.GlobalDiscountRate; // 获取全局折扣率
double finalPrice = originalPrice * (1 - discountRate); // 计算折扣后的价格
Console.WriteLine($"原价:{originalPrice:C}, 折扣后价格:{finalPrice:C}(折扣率:{discountRate * 100}%)");
return finalPrice;
}
}
// 日志记录模块:记录当前折扣率
public class Logger
{
public void LogCurrentDiscountRate()
{
double discountRate = GlobalSettings.GlobalDiscountRate; // 获取全局折扣率
Console.WriteLine($"当前系统的全局折扣率为:{discountRate * 100}%");
}
}
// 主程序
class Program
{
static void Main(string[] args)
{
Console.WriteLine("欢迎使用电商系统!\n");
// 初始化模块
AdminModule adminModule = new AdminModule();
PriceCalculator priceCalculator = new PriceCalculator();
Logger logger = new Logger();
// 管理员更新折扣率
adminModule.UpdateDiscountRate(0.2); // 将折扣率更新为 20%
// 计算商品价格
priceCalculator.CalculateFinalPrice(100); // 原价 100 的商品
// 记录当前折扣率
logger.LogCurrentDiscountRate();
// 再次更新折扣率
adminModule.UpdateDiscountRate(0.15); // 将折扣率更新为 15%
// 再次计算商品价格
priceCalculator.CalculateFinalPrice(100);
// 再次记录当前折扣率
logger.LogCurrentDiscountRate();
Console.WriteLine("\n电商系统演示结束!");
}
}
}
在这个例子中,管理员模块、商品价格计算模块、日志记录模块都直接访问和修改同一个全局变量 globalDiscountRate,从而形成公共耦合。
七、内容耦合
当一个模块直接使用另一个模块的内部数据,或通过非正常入口转入另一个模块内部时,这种模块之间的耦合称为内容耦合。
假设模块A是订单模块,模块B是支付模块,如果支付模块可以直接访问订单数据表,那么至少会带来以下问题。
第一个问题是存在重复的数据访问层代码,支付和订单模块都要写订单数据访问代码。
第二个问题是如果订单业务变动,需要变更订单数据字段,如果支付模块没有跟着及时 变更,那么可能会造成业务错误。
第三个问题是如果订单业务变动,需要分库分表拆分数据,如果支付模块没有跟着及时变更,例如没有使用shardingKey进行查询或者旧库表停写,那么可能会造成支付模块严重错误。
第四个问题是业务入口没有收敛,访问入口到处散落,如果想要业务变更则需要多处修改,非常不利于维护。
代码示例:
下面代码演示了在一个电商系统中,支付模块直接操作了订单模块的内部数据,违反了模块间的隔离性原则,造成内容耦合。
using System;
using System.Collections.Generic;
namespace ContentCouplingExample
{
// 定义全局变量作为订单数据表
public static class OrderData
{
public static Dictionary<int, Order> Orders = new Dictionary<int, Order>(); // 模拟订单数据表
}
// 订单类
public class Order
{
public string ProductName { get; set; }
public decimal TotalPrice { get; set; }
public string Status { get; set; } // 订单状态:pending(待支付)、paid(已支付)
}
// 订单模块
public class OrderModule
{
public void CreateOrder(int orderId, string productName, decimal totalPrice)
{
var order = new Order
{
ProductName = productName,
TotalPrice = totalPrice,
Status = "pending" // 初始状态为待支付
};
OrderData.Orders[orderId] = order;
Console.WriteLine($"订单 {orderId} 已创建:商品={productName}, 总价={totalPrice}");
}
public void UpdateOrderStatus(int orderId, string status)
{
if (OrderData.Orders.ContainsKey(orderId))
{
OrderData.Orders[orderId].Status = status;
Console.WriteLine($"订单 {orderId} 状态已更新为:{status}");
}
else
{
Console.WriteLine($"订单 {orderId} 不存在!");
}
}
}
// 支付模块
public class PaymentModule
{
public void ProcessPayment(int orderId, decimal amountPaid)
{
if (OrderData.Orders.ContainsKey(orderId))
{
var order = OrderData.Orders[orderId];
if (order.Status == "pending")
{
if (amountPaid >= order.TotalPrice)
{
// 直接修改订单模块的内部数据
order.Status = "paid";
Console.WriteLine($"订单 {orderId} 支付成功!支付金额:{amountPaid}");
}
else
{
Console.WriteLine($"支付失败:支付金额不足。应付金额:{order.TotalPrice}");
}
}
else
{
Console.WriteLine($"订单 {orderId} 已经支付或状态无效!");
}
}
else
{
Console.WriteLine($"订单 {orderId} 不存在!");
}
}
}
// 主程序
class Program
{
static void Main(string[] args)
{
Console.WriteLine("欢迎使用电商系统!\n");
// 初始化模块
OrderModule orderModule = new OrderModule();
PaymentModule paymentModule = new PaymentModule();
// 创建订单
orderModule.CreateOrder(1, "笔记本电脑", 5000);
orderModule.CreateOrder(2, "无线鼠标", 200);
// 处理支付
paymentModule.ProcessPayment(1, 5000); // 成功支付
paymentModule.ProcessPayment(2, 100); // 支付失败
// 更新订单状态
orderModule.UpdateOrderStatus(2, "paid"); // 手动更新状态
// 再次尝试支付
paymentModule.ProcessPayment(2, 300); // 已支付,无法重复支付
Console.WriteLine("\n电商系统演示结束!");
}
}
}
改进建议:
为了降低内容耦合带来的问题,可以采取以下措施:
封装数据访问逻辑:
将订单数据的访问逻辑封装在订单模块中,提供明确的接口供其他模块调用。
支付模块不再直接访问 OrderData.Orders,而是通过订单模块提供的方法 (如 GetOrderById 和 UpdateOrderStatus)来操作订单数据。
依赖注入:
使用依赖注入的方式将订单模块的服务注入到支付模块中,避免直接依赖全局变量。
分层架构:
引入服务层或数据访问层,将数据操作与业务逻辑分离,确保模块之间的独立性。
延伸问题:在分层开发实践中(UI层、业务层、数据层),支付完成后,支付模块应该调用订单模块的数据层方法还是业务层方法来更新订单状态?
在分层开发实践中,通常会遵循“单一职责原则”和“依赖倒置原则”,确保每一层只处理与其职责相关的任务。对于支付完成后更新订单状态的需求,最佳实践是通过调用业务层方法来完成,而不是直接调用数据层方法。
为什么选择业务层?
业务逻辑封装:
- 业务层负责处理所有的业务规则和逻辑。更新订单状态不仅仅是简单的数据库操作,它可能涉及到复杂的业务逻辑,例如检查订单是否已经支付、计算积分、触发某些事件(如发送确认邮件)等。
- 如果直接在支付模块中调用数据层的方法,会导致业务逻辑泄露到其他模块中,违反了单一职责原则,并且增加了系统的耦合度。
事务管理:
- 在许多情况下,支付成功后的订单状态更新需要在一个事务中完成,以确保数据的一致性。业务层可以更好地管理和协调这些事务。
- 数据层通常只提供基本的CRUD操作,不具备处理复杂事务的能力。
灵活性与可扩展性:
- 使用业务层方法可以在不改变底层数据结构的情况下,轻松地调整或扩展业务逻辑。例如,如果未来需要在订单状态更新时执行额外的操作(如通知物流系统),只需修改业务层代码即可,而不需要改动支付模块或其他相关模块。
安全性与权限控制:
- 业务层还可以包含一些权限控制逻辑,确保只有经过验证的请求才能修改订单状态。这有助于防止非法操作和提高系统的安全性。
示例代码:
using System;
using System.Collections.Generic;
namespace LayeredArchitectureExample
{
// 数据层接口
public interface IOrderRepository
{
Order GetOrderById(int orderId);
void UpdateOrder(Order order);
}
// 订单类
public class Order
{
public int OrderId { get; set; }
public string ProductName { get; set; }
public decimal TotalPrice { get; set; }
public string Status { get; set; } // 订单状态:pending(待支付)、paid(已支付)
}
// 订单服务类(业务层)
public class OrderService
{
private readonly IOrderRepository _orderRepository;
public OrderService(IOrderRepository orderRepository)
{
_orderRepository = orderRepository;
}
public void CompleteOrderPayment(int orderId)
{
var order = _orderRepository.GetOrderById(orderId);
if (order != null && order.Status == "pending")
{
order.Status = "paid";
_orderRepository.UpdateOrder(order);
Console.WriteLine($"订单 {orderId} 支付成功并更新状态为 paid");
}
else
{
Console.WriteLine($"订单 {orderId} 状态无法更新或不存在!");
}
}
}
// 支付服务类(业务层)
public class PaymentService
{
private readonly OrderService _orderService;
public PaymentService(OrderService orderService)
{
_orderService = orderService;
}
public void ProcessPayment(int orderId, decimal amountPaid)
{
// 假设这里有一些支付处理逻辑...
Console.WriteLine($"正在处理订单 {orderId} 的支付...");
// 模拟支付成功
bool paymentSuccess = true;
if (paymentSuccess)
{
// 调用订单服务来更新订单状态
_orderService.CompleteOrderPayment(orderId);
}
else
{
Console.WriteLine("支付失败!");
}
}
}
// 主程序
class Program
{
static void Main(string[] args)
{
// 假设这里是依赖注入容器初始化的地方...
var orderRepository = new InMemoryOrderRepository(); // 这里只是一个示例实现
var orderService = new OrderService(orderRepository);
var paymentService = new PaymentService(orderService);
// 创建订单
orderRepository.UpdateOrder(new Order { OrderId = 1, ProductName = "笔记本电脑", TotalPrice = 5000, Status = "pending" });
// 处理支付
paymentService.ProcessPayment(1, 5000); // 成功支付
Console.WriteLine("\n演示结束!");
}
}
// 简单的内存订单存储实现(仅用于示例)
public class InMemoryOrderRepository : IOrderRepository
{
private Dictionary<int, Order> _orders = new Dictionary<int, Order>();
public Order GetOrderById(int orderId)
{
_orders.TryGetValue(orderId, out var order);
return order;
}
public void UpdateOrder(Order order)
{
if (_orders.ContainsKey(order.OrderId))
{
_orders[order.OrderId] = order;
}
else
{
_orders.Add(order.OrderId, order);
}
}
}
}
八、外部耦合与公共耦合的区别
1. 核心定义
耦合类型 | 定义 | 关键特征 |
公共耦合(Common Coupling) | 多个模块直接共享并读写同一内部数据(如全局变量、数据库表、共享内存)。 | - 数据在程序内部显式定义(如 globalVar)。 |
外部耦合(External Coupling) | 多个模块依赖同一外部系统或资源(如文件、API、硬件、消息队列)。 | - 数据或服务来自程序外部(如 config.json)。 |
2. 本质区别
对比维度 | 公共耦合 | 外部耦合 |
数据位置 | 程序内部(如全局变量、数据库表) | 程序外部(如文件、第三方API、消息队列) |
依赖方式 | 直接读写共享数据 | 通过外部接口或协议交互 |
耦合强度 | 强(修改数据会影响所有依赖模块) | 弱(外部系统变更可能不影响内部逻辑) |
典型例子 | - 多个函数读写 globalCounter | - 多个模块读取 config.ini |
3. 如何选择?
避免公共耦合:
用参数传递、封装(如单例模式)、事件驱动替代全局数据共享。
接受外部耦合:
当必须依赖外部系统时(如数据库、云服务),通过接口隔离(如DAO层、API客户端)降低影响。
4. 一句话总结
公共耦合:兄弟模块共用自家冰箱(直接拿食物,容易冲突)。
外部耦合:兄弟模块点外卖(通过第三方服务交互,互不干扰)。
设计目标:尽量将公共耦合转化为外部耦合(如用消息队列替代共享数据库表)。
九、开发建议
日常开发过程中,应尽量使用数据耦合、少用标记耦合和控制耦合、限制公共耦合的范围、完全不用内容耦合。