3、模块化与分层架构:经典三层架构
聊到企业级桌面应用的架构,我第一个想跟你聊的就是分层。为什么?因为我在项目里见过太多「一锅粥」式的代码了。UI 里塞着 SQL 查询,业务逻辑里弹着消息框…… 改一个需求,牵一发而动全身,那叫一个痛苦。
分层架构,说白了就是「各司其职」。把代码按职责切分成不同的层,层与层之间通过约定好的接口通信。这样做的好处,你想想看:
- 职责单一:每一层只关心自己的事,UI 只管展示,业务逻辑只管算,数据访问只管存。
- 易于替换:想换 UI 框架?换数据库?只要接口不变,改一层就行。
- 便于测试:每一层都可以独立测试,不用启动整个应用。
3.1 经典三层架构
最经典的分层方式,就是三层架构。我习惯把它画成三块积木:
| 层 | 职责 | 常见技术 |
|---|---|---|
| UI 层(表现层) | 用户交互、数据展示、事件处理 | WPF、WinForms、Qt、Electron |
| 业务逻辑层(BLL) | 核心业务规则、流程编排、数据校验 | C#、Java、TypeScript 服务类 |
| 数据访问层(DAL) | 数据库读写、文件操作、外部 API 调用 | Entity Framework、Dapper、ADO.NET |
举个例子。假设你要做一个订单管理系统。用户点击「提交订单」按钮:
- UI 层:收集表单数据,调用业务逻辑层的
SubmitOrder()方法。 - 业务逻辑层:校验库存、计算总价、生成订单号。然后调用数据访问层的
SaveOrder()。 - 数据访问层:把订单数据写入数据库。
你看,每一层只做自己的事。UI 层不需要知道数据库是 MySQL 还是 SQLite,业务逻辑层也不需要关心按钮长什么样。
核心原则:上层依赖下层,下层不依赖上层。UI 层依赖业务逻辑层,业务逻辑层依赖数据访问层。反过来不行。
3.2 依赖注入与控制反转
说到分层,就绕不开依赖注入(DI)和控制反转(IoC)。这两个概念,我刚开始接触时也绕晕过。其实很简单:
- 控制反转:把「创建对象」的控制权,从你的代码里交出去。你不 new 了,让框架帮你 new。
- 依赖注入:是实现 IoC 的一种方式。框架把依赖的对象「注入」到你的类里。
为什么要这么做?我在项目里遇到过这样的坑:业务逻辑层直接 new 了一个数据访问层的对象。后来要换数据库,得改业务逻辑层的代码。这违反了「开闭原则」——对扩展开放,对修改关闭。
用依赖注入,代码会变成这样:
// 定义接口
public interface IOrderRepository
{
void Save(Order order);
}
// 实现接口
public class SqlOrderRepository : IOrderRepository
{
public void Save(Order order) { /* SQL 实现 */ }
}
// 业务逻辑层只依赖接口
public class OrderService
{
private readonly IOrderRepository _repository;
// 依赖通过构造函数注入
public OrderService(IOrderRepository repository)
{
_repository = repository;
}
public void SubmitOrder(Order order)
{
// 业务逻辑...
_repository.Save(order);
}
}
嗯,这里要注意:依赖注入的核心是「面向接口编程」。你的业务逻辑层只依赖接口,不依赖具体实现。这样,换数据库时,你只需要写一个新的 MongoOrderRepository,然后通过 DI 容器注入进去就行。业务逻辑层一行代码都不用改。
我的习惯:在项目初期就引入 DI 容器。别等到代码写了一半再重构,那代价太大了。常用的 DI 容器有 Autofac、Unity、.NET Core 自带的 DI 容器等。
3.3 模块间通信机制
分层之后,模块之间怎么通信?这是个好问题。我见过不少团队,分层分得挺好,但模块之间直接调用,耦合得死死的。
常见的通信机制有几种:
| 方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 直接方法调用 | 同层模块、紧密耦合 | 简单、性能高 | 耦合度高 |
| 事件/消息机制 | 解耦、异步通知 | 松耦合、扩展性好 | 调试困难、性能开销 |
| 服务总线 | 大型应用、跨进程 | 统一管理、可监控 | 复杂度高 |
我个人比较推荐「事件驱动」的方式。举个例子:订单提交成功后,需要通知库存模块扣减库存,通知财务模块生成账单,通知物流模块准备发货。
如果用直接调用,代码会变成:
public void SubmitOrder(Order order)
{
// 保存订单
_orderRepository.Save(order);
// 直接调用其他模块
_inventoryService.DeductStock(order);
_financeService.GenerateBill(order);
_logisticsService.PrepareShipping(order);
}
这样写,SubmitOrder 方法就耦合了三个模块。以后加一个「发送短信通知」,又得改这个方法。
用事件机制,代码会清爽很多:
public void SubmitOrder(Order order)
{
_orderRepository.Save(order);
// 发布事件,谁关心谁订阅
_eventBus.Publish(new OrderSubmittedEvent(order));
}
// 库存模块订阅事件
public class InventoryHandler : IEventHandler<OrderSubmittedEvent>
{
public void Handle(OrderSubmittedEvent e)
{
// 扣减库存
}
}
// 财务模块订阅事件
public class FinanceHandler : IEventHandler<OrderSubmittedEvent>
{
public void Handle(OrderSubmittedEvent e)
{
// 生成账单
}
}
我曾经踩过的坑:事件处理一定要考虑异常情况。如果库存扣减失败,订单已经保存了怎么办?建议引入「补偿机制」或「事务性发件箱模式」,确保数据一致性。
说白了,模块间通信的核心原则就一条:能松耦合,就别紧耦合。事件机制虽然增加了一点复杂度,但换来的是模块的独立性和可扩展性。你想想看,以后要加一个新模块,只需要订阅对应的事件就行,完全不用改现有代码。这才是企业级应用该有的样子。