3、模块化与分层架构:经典三层架构

聊到企业级桌面应用的架构,我第一个想跟你聊的就是分层。为什么?因为我在项目里见过太多「一锅粥」式的代码了。UI 里塞着 SQL 查询,业务逻辑里弹着消息框…… 改一个需求,牵一发而动全身,那叫一个痛苦。

分层架构,说白了就是「各司其职」。把代码按职责切分成不同的层,层与层之间通过约定好的接口通信。这样做的好处,你想想看:

  • 职责单一:每一层只关心自己的事,UI 只管展示,业务逻辑只管算,数据访问只管存。
  • 易于替换:想换 UI 框架?换数据库?只要接口不变,改一层就行。
  • 便于测试:每一层都可以独立测试,不用启动整个应用。

3.1 经典三层架构

最经典的分层方式,就是三层架构。我习惯把它画成三块积木:

职责 常见技术
UI 层(表现层) 用户交互、数据展示、事件处理 WPF、WinForms、Qt、Electron
业务逻辑层(BLL) 核心业务规则、流程编排、数据校验 C#、Java、TypeScript 服务类
数据访问层(DAL) 数据库读写、文件操作、外部 API 调用 Entity Framework、Dapper、ADO.NET

举个例子。假设你要做一个订单管理系统。用户点击「提交订单」按钮:

  1. UI 层:收集表单数据,调用业务逻辑层的 SubmitOrder() 方法。
  2. 业务逻辑层:校验库存、计算总价、生成订单号。然后调用数据访问层的 SaveOrder()
  3. 数据访问层:把订单数据写入数据库。

你看,每一层只做自己的事。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)
    {
        // 生成账单
    }
}

我曾经踩过的坑:事件处理一定要考虑异常情况。如果库存扣减失败,订单已经保存了怎么办?建议引入「补偿机制」或「事务性发件箱模式」,确保数据一致性。

说白了,模块间通信的核心原则就一条:能松耦合,就别紧耦合。事件机制虽然增加了一点复杂度,但换来的是模块的独立性和可扩展性。你想想看,以后要加一个新模块,只需要订阅对应的事件就行,完全不用改现有代码。这才是企业级应用该有的样子。