在多年的开发过程中,我经常遇到一些反复出现的问题。这些问题不仅影响代码质量,还会增加开发成本,浪费大量时间。本文将从实战经验出发,分析这些常见反模式及其解决方案。
一、职责边界模糊 Link to heading
这是我在反复遇到的一种问题类型。很多人为了快速实现功能,把业务逻辑、UI交互、数据存储等各种职责混杂在一起。举个例子:
// 问题代码:职责混乱的通讯类
public class SerialCommunication
{
private SerialPort _serial;
private List<ProductionData> _businessData; // 错误:业务数据
private Dictionary<string, object> _deviceState; // 错误:状态管理
private bool _isCalibrating; // 错误:业务状态
public void ProcessProductionData()
{
var data = ReadData();
// 直接在这里处理业务逻辑
if (data.Contains("ERROR"))
{
UpdateUIStatus("设备错误"); // 错误:UI操作
SaveErrorLog(data); // 错误:日志操作
}
_businessData.Add(new ProductionData(data)); // 错误:业务数据处理
if (NeedsCalibration()) // 错误:校准逻辑
{
StartCalibration();
}
}
}
这种设计的问题: Link to heading
-
耦合度过高
- 通讯逻辑和业务处理耦合在一起
- 修改业务流程不得不动通讯代码
- 测试单一功能几乎不可能
- 维护时牵一发而动全身
-
职责不清
- 单个类承担了通讯、业务、UI和日志等多重职责
- Bug定位困难,改一处可能影响多个功能
- 代码理解成本高,新人难以接手
- 类越来越臃肿,最终无法维护
-
扩展困难
- 增加新功能必须修改核心代码
- 替换底层实现(如网络通讯替代串口)困难
- 无法进行有效的单元测试
- 依赖注入几乎不可能实现
改进后的分层设计: Link to heading
// 1. 通讯接口定义
public interface ISerialDevice : IDisposable
{
bool IsConnected { get; }
SerialSettings Settings { get; }
Task<byte[]> ReadDataAsync(CancellationToken cancellationToken = default);
Task<bool> SendDataAsync(byte[] data, CancellationToken cancellationToken = default);
Task<bool> ConnectAsync(CancellationToken cancellationToken = default);
Task DisconnectAsync();
}
// 2. 基础设施层:串口通讯实现
public class SerialDevice : ISerialDevice
{
private readonly SerialPort _serial;
private readonly ILogger<SerialDevice> _logger;
public SerialDevice(SerialSettings settings, ILogger<SerialDevice> logger)
{
_serial = new SerialPort(settings.PortName, settings.BaudRate);
_logger = logger;
}
public async Task<byte[]> ReadDataAsync(CancellationToken cancellationToken = default)
{
if (!IsConnected)
{
throw new DeviceCommunicationException("设备未连接");
}
try
{
return await Task.Run(() =>
{
if (_serial.BytesToRead == 0)
{
return Array.Empty<byte>();
}
var buffer = new byte[_serial.BytesToRead];
_serial.Read(buffer, 0, buffer.Length);
return buffer;
}, cancellationToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "Serial read error");
throw new DeviceCommunicationException("读取数据失败", ex);
}
}
}
// 3. 业务层:设备控制
public class DeviceController
{
private readonly ISerialDevice _device;
private readonly IDeviceStateManager _stateManager;
private readonly ICalibrationService _calibrationService;
private readonly ILogger<DeviceController> _logger;
public async Task ProcessDataAsync(CancellationToken cancellationToken = default)
{
try
{
var data = await _device.ReadDataAsync(cancellationToken);
if (data.Length == 0)
{
_logger.LogDebug("No data received");
return;
}
var productionData = await ParseDataAsync(data);
await _stateManager.UpdateStateAsync(productionData, cancellationToken);
if (await _calibrationService.NeedsCalibrationAsync(cancellationToken))
{
await _calibrationService.RequestCalibrationAsync(cancellationToken);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing data");
throw new DeviceOperationException("数据处理错误", ex);
}
}
}
为什么这样设计更好: Link to heading
-
职责分离
- 通讯层只负责数据传输,不涉及业务
- 业务层专注于业务逻辑实现
- 每个组件都有明确的边界,职责清晰
-
依赖倒置
- 高层模块依赖抽象接口
- 实现细节被隐藏在接口背后
- 可以轻松替换实现(如用于测试的模拟设备)
-
可测试性提高
- 业务逻辑可以独立测试
- 通讯层可以单独验证
- 可以使用模拟对象替代真实依赖
-
异常处理更清晰
- 每一层都有针对性的异常处理
- 错误信息更加明确和有针对性
- 日志记录更加结构化
二、过度设计与简单问题复杂化 Link to heading
我遇到一个典型的案例:客户提出了一个简单的需求 —— 设备每天需要进行两次校准。然而,外包团队在分析这个需求时,把它想得无比复杂,甚至已经复杂到他们认为无法实现了。
外包团队的思考方向: Link to heading
- 如果用户关闭了软件怎么办?
- 如何精确计算是否到了校准时间?
- 软件关闭期间如何继续计时?
- 如果系统时间被修改了怎么处理?
这种过度思考不仅没有解决问题,反而把简单的需求复杂化了。
实际上,这个需求非常简单: Link to heading
-
核心需求
- 每天校准两次
- 两次间隔X小时
-
我是如何分析和确认需求的
- 了解这个需求的目的
- 确认了操作员工作的时间
- 了解客户想要的是什么效果
- 每次操作之前都校准,太浪费时间,所以他们建议每天两次
- 客户实际上根本不关心几点校准,只是必须X小时校准一次
-
我给出的方案
- 如果是早上操作员上班的时间,要求必须校准一次
- 保存一个文件来记录上一次校准的时间
- 启动软件读取这个时间来判断是否需要校准
- 要求客户不要乱改系统时间
就这么简单,需求就实现了。
这个案例的启示: Link to heading
-
从业务角度思考
- 理解用户真正的需求是什么
- 关注实际的业务场景
- 不要被技术细节带偏
-
避免过度思考
- 系统时间被修改是用户的责任
- 不是所有边界情况都需要处理
- 有些问题根本不需要技术解决
-
保持方案简单
- 简单的问题用简单的方法解决
- 不要为了技术而技术
- 可维护性比技术先进性更重要
这个例子很好地说明了为什么我们需要"跳出技术思维",从业务角度思考问题。有时候,最好的解决方案就是最简单的方案。过度设计不仅会浪费开发时间,还会增加维护成本,最终可能导致项目失败。
三、不当的抽象层次 Link to heading
在一个制造执行系统中,我曾见过这样的代码:
// 错误的抽象方式
public class ProductionLineController
{
private readonly SqlConnection _connection;
private readonly SerialPort _serialPort;
private readonly Form _mainForm;
public void ProcessProduct()
{
// 直接操作底层细节
_serialPort.Write("GET_DATA");
var data = _serialPort.ReadLine();
// 混合了UI操作
_mainForm.BeginInvoke(() =>
_mainForm.StatusLabel.Text = "Processing...");
// 直接的数据库操作
using var command = _connection.CreateCommand();
command.CommandText = "INSERT INTO Products ...";
command.ExecuteNonQuery();
}
}
这种设计的问题: Link to heading
-
关注点混乱
- 同时处理UI、数据库和设备通讯
- 违反单一职责原则
- 测试几乎不可能
-
难以维护
- 修改一处功能需要理解整体逻辑
- 容易引入副作用
- 很难重用代码片段
正确的分层抽象: Link to heading
// 1. 领域模型
public record ProductionData
{
public string ProductId { get; init; }
public DateTime Timestamp { get; init; }
public ProductionStatus Status { get; init; }
}
// 2. 设备通讯抽象
public interface IProductionDevice
{
Task<ProductionData> GetProductionDataAsync();
}
// 3. 数据访问抽象
public interface IProductionRepository
{
Task SaveProductionDataAsync(ProductionData data);
}
// 4. 业务服务
public class ProductionService
{
private readonly IProductionDevice _device;
private readonly IProductionRepository _repository;
private readonly ILogger<ProductionService> _logger;
public ProductionService(
IProductionDevice device,
IProductionRepository repository,
ILogger<ProductionService> logger)
{
_device = device;
_repository = repository;
_logger = logger;
}
public async Task ProcessProductAsync()
{
try
{
var data = await _device.GetProductionDataAsync();
await _repository.SaveProductionDataAsync(data);
// 发布领域事件
await _eventPublisher.PublishAsync(
new ProductionCompletedEvent(data));
}
catch (Exception ex)
{
_logger.LogError(ex, "Production process failed");
throw;
}
}
}
// 5. UI层
public class ProductionViewModel : INotifyPropertyChanged
{
private readonly IProductionService _productionService;
private string _status;
public string Status
{
get => _status;
private set
{
_status = value;
OnPropertyChanged();
}
}
public async Task StartProductionAsync()
{
Status = "Processing...";
try
{
await _productionService.ProcessProductAsync();
Status = "Completed";
}
catch
{
Status = "Error";
throw;
}
}
}
为什么这样的抽象更好: Link to heading
-
层次清晰
- 领域模型承载业务概念
- 各层职责明确,边界分明
- 关注点分离,便于独立开发
-
领域驱动设计思想
- 通过领域模型表达核心业务
- 领域事件处理副作用和集成
- 业务规则集中在领域层
-
测试友好
- 每一层可以独立测试
- 可以轻松模拟依赖组件
- 便于进行集成测试
四、代码重复与堆砌 Link to heading
在一个生产线管理系统中,代码重复是最常见的问题:
// 问题代码:重复的业务逻辑
public class ProductionLine1Controller
{
public void ProcessData()
{
// 复制粘贴的代码
var data = ReadSerialData();
if (data.Contains("ERROR"))
{
LogError();
return;
}
SaveToDatabase(data);
}
}
public class ProductionLine2Controller
{
public void ProcessData()
{
// 完全相同的逻辑
var data = ReadSerialData();
if (data.Contains("ERROR"))
{
LogError();
return;
}
SaveToDatabase(data);
}
}
优化方案: Link to heading
// 1. 抽象共同的接口
public interface IProductionLine
{
Task<ProcessResult> ProcessDataAsync();
string LineId { get; }
}