ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

三层架构与依赖注入:解耦实践与项目落地指南

三层架构与依赖注入:解耦实践与项目落地指南 三层架构这个概念相信每个做后端开发的朋友都听过但我发现很多人对这种架构的理解停留在“三层就是三层分开写就完了”的层面。最近我把三层架构和依赖注入放在一起认真过了一遍发现这对组合才是真正让代码活得舒服的关键。这篇笔记是“学习笔记”系列的第二篇主要结合自己动手写的示例和踩过的坑把思路完整梳理一遍不管你是刚接触分层的初级工程师还是想把项目结构理顺的资深开发都可以跟着走一遍。1. 三层架构为什么是三层而不是两层要说三层架构得先从两层架构的痛点下手。早期开发经常把界面代码和数据库操作堆在一起写一个小工具倒是无所谓业务一复杂就全拧了改一个字段要顺着界面层一直找到SQL逻辑只存在某一个按钮后面换数据库更是想都不敢想。后来大家开始强调“高内聚、低耦合”于是把用户交互、业务规则、数据存取这三类最典型的关注点拆开形成了呈现层UI、业务逻辑层BLL、数据访问层DAL的经典结构。1.1 分层的源头与各层职责这三层不是随便拍脑袋分的背后是关注点分离即让每一层只回答一个问题。呈现层负责展示界面、接收用户输入和输出结果业务逻辑层处理业务规则、状态流转和计算数据访问层负责对数据库或外部存储的读写。注意业务逻辑层是核心也是最容易被写错的一层很多新手喜欢把SQL写在UI层或者把业务判断写在存储过程里这会直接让分层名存实亡。我用一个下单场景来串一遍。用户在前端点“提交订单”呈现层拿到订单数据后会调用业务层的某个方法业务层先校验库存、计算总价、判断优惠是否可用然后再转入数据访问层去保存订单和扣减库存。整个过程里呈现层不知道数据库长什么样数据访问层也不知道页面怎么展示彼此之间通过方法调用和返回值协作没有多余的动作。1.2 分层之间的依赖关系与边界分层架构的一个核心约定是上层依赖下层但下层不能反向依赖上层。换句话说UI层可以引用BLL和DALBLL可以引用DAL可DAL一旦反过来引用BLL那一刻架构就塌了。实际项目里我见到过很多因为图省事导致的反向引用比如在DAL里往日志表写错误信息时顺手调了一个BLL层的方法短期内能跑长期看谁也说不清模块边界了。更细一层层与层之间传递的数据也值得规范。最早的写法是各层直接把实体对象传来传去甚至把DataSet直接从DAL扔到UI看着方便其实把数据访问细节暴露了出去。现在更推荐用DTO也就是数据传输对象把层间交互要用的字段单独定义少传一个字段就能少一次偷偷访问其他层的机会。这一点在团队协作的时候尤其明显因为谁都不想在分层架构里看到一把梭穿到底的万能对象。1.3 为什么会有“三层架构过时了”的声音有时候能在技术群里看到“三层架构早就过时了”这样的说法这句话要分两半听。如果说的是经典的三层项目循环依赖严重、每层都堆成上帝类那确实说明分层没落对如果说的是分层本身没有价值那多半是把架构演进和工程管理混为一谈。微服务、DDD、整洁架构它们在底层依然延续着“表现层、用例层、领域层、基础设施层”这种关注点隔离的思路称呼变了核心逻辑没变。所以我给新人的建议一直是先把经典三层吃透再谈更复杂的架构。2. 依赖注入实现“解耦”的那根风筝线聊完分层自然要聊怎么让层与层之间的依赖显得不那么生硬。三层架构本身只解决了“分模块”的问题但如果你在业务层里直接new一个DAL业务层就无法摆脱对DAL具体实现的依赖也就谈不上真正的解耦。依赖注入的思想恰好能把这根紧绷的线松开。2.1 控制反转到底反转了什么控制反转IoC听起来是个吓人的词本质上很简单原来由自己控制的依赖创建过程交给外部容器在合适的时间注入进来。传统写法是业务层里写一句DAL层的具体实现这是“正向”依赖反转之后业务层只依赖DAL的抽象接口容器负责把具体的实现塞进构造函数。控制从“自己主动去拿”变成了“等着被给”这就是反转的含义。拿做饭举例子。传统方式是你自己洗菜、切菜、炒菜全程亲力亲为依赖注入则像是请了一个厨师团队你只负责说“今天想吃鱼香肉丝”后厨容器替你完成备菜和烹饪把成品放到你面前。业务逻辑层就是那个点菜的顾客它不需要知道食材从哪来只需要知道端上来的菜满足自己的要求。2.2 构造函数注入为什么最稳妥依赖注入有构造函数注入、属性注入、方法注入几种姿势我在实践中基本只用构造函数注入。原因很简单构造函数注入能保证依赖一旦被创建就不可变后续你想偷偷换掉都不行这使得对象的生命周期清晰可见。属性注入容易埋雷因为对象建出来之后依赖可能还没设置代码里就会出现空引用问题方法注入虽然灵活但会让方法签名变得冗长而且难以保证每个调用者都传对依赖。在实现上通常是让Controller或Service的构造函数接收一个接口参数接口背后是具体实现。比如订单模块里我的OrderService构造函数是这样的public class OrderService { private readonly IOrderRepository _orderRepository; private readonly IUserService _userService; public OrderService(IOrderRepository orderRepository, IUserService userService) { _orderRepository orderRepository; _userService userService; } }在这个地方“依赖注入”最直观的体现就是OrderService从来不关心OrderRepository是谁实现的它只依赖IOrderRepository这个契约。后面真要换数据库只要写一个实现该接口的新类再改一下容器注册整个业务层一行都不用动。3. 三层架构 依赖注入一版可直接上手的代码骨架前面两章把概念拆清楚了最关键的还是怎么把它们放到一个真实项目里。接下来我以一个最小化的订单系统为例把涉及的主要项目结构、接口定义和容器注册一步步写出来。你可以照着这个骨架去套自己的业务也可以先跑通再慢慢细化。3.1 先从接口开始先把依赖的“契约”定下来项目结构上我习惯分成Controller、Service、Repository三层同时补充DTO和单独的领域实体文件夹。这里的Controller对应呈现层Service对应业务逻辑层Repository对应数据访问层。每个层级先定义接口再写实现类理由前面已经说过就是要让上层只依赖抽象不依赖具体。订单这个例子里我会先写一个IOrderRepositorypublic interface IOrderRepository { TaskOrder GetByIdAsync(int id); Taskint CreateAsync(Order order); }然后写实现类OrderRepository去操作数据库里面的细节无非是上下文、SQL语句或者仓储封装不必在这篇笔记里展开。重点是Service里写调用的时候只面向接口public class OrderService : IOrderService { private readonly IOrderRepository _orderRepository; public OrderService(IOrderRepository orderRepository) { _orderRepository orderRepository; } public async TaskOrderResult PlaceOrder(OrderRequest request) { // 先校验再计算最后存库 var order new Order { UserId request.UserId, TotalAmount 100 }; var id await _orderRepository.CreateAsync(order); return new OrderResult { OrderId id, Success true }; } }这样看OrderService的行为完全落在一层里没有去关心数据库里有没有这张表也没有去渲染任何界面。依赖注入介入后Controller同样只依赖IOrderService接口整个调用链干净利落。3.2 容器注册让依赖真正“活起来”接口和实现写好了还要告诉依赖注入容器“谁来接谁的活”。这里以ASP.NET Core里的IServiceCollection为例注册代码如下services.AddScopedIOrderRepository, OrderRepository(); services.AddScopedIOrderService, OrderService();关键点是生命周期。AddScoped表示同一个请求范围内是同一个实例AddTransient表示每次获取都是新实例AddSingleton表示整个应用共享一个实例。我在数据库仓储上通常用Scoped因为它和事务、连接的作用范围比较匹配日志、配置这类无状态组件用它不多常用Singleton。初学者最容易犯错的地方就是到处用Singleton结果发现并发请求下来数据串了或者某个类内部持有了一个作用域受限的依赖直接在运行期报错。3.3 分层之后的调用链路与边界控制整个调用链可以概括为Controller 调用 IService 方法Service 调用 IRepository 方法Repository 再访问数据库。每一层之间只通过接口和方法调用进行交互不直接去操作其他层中的实现类对象。实际开发时我会顺手给每个项目模块加一条“禁止违反调用方向”的约定具体体现为代码评审时重点看三件事有没有跨越Service直接调用Repository的情况、有没有在Controller里写业务逻辑、有没有在Repository里处理了本应由Service完成的规则判断。边界控制还有个容易被忽略的细节就是异常处理尽量在Service层统一不要让Repository把自己的数据库异常细节抛到UI层。经常能见到异常里的SQL片段直接打到前端这既不安全也让用户看到一个莫名的报错。更好的做法是Service捕获原始异常转成业务异常再让Controller统一包装成响应结果最终用户只看到友好的提示日志里保留完整堆栈。4. 跳出后端聊一聊物联网三层架构的现实应用最近搜索“物联网三层架构”的朋友明显变多了很多项目甚至直接要求把物联网系统的设计文档写成三层结构。我第一次听到物联网三层的时候第一反应是它跟软件三层似乎没有直接关系仔细一看才发现两者在思想上一脉相承但各自指的三层实体完全不同。4.1 物联网的感知层、网络层与应用层到底在管什么物联网的三层架构一般指感知层、网络层、应用层。感知层是设备端负责采集数据比如温度传感器、烟雾报警器、摄像头网络层是传输通道负责把感知层的数据送到后端Wi-Fi、NB-IoT、运营商网络都属于这个范畴应用层则是最终面向用户的业务处理比如告警短信、能耗报表、设备远程控制。这么一看感知层相当于软件里的数据源头网络层相当于通信基础设施应用层做的事情和web系统里的业务层高度相似。举个例子小区里的智能水泵监控系统感知层的压力计每秒回传水压和流量网络层通过窄带物联网把数据上报到云端应用层再根据水压阈值判断是否需要推送维修通知。某个环节断了问题就会暴露在整体链条上所以物联网项目同样讲究分层明确只是分层的落脚点不再是代码里的项目文件而是系统里的硬件模块和网络协议。4.2 把软件架构里的分层思想投射到物联网项目上把软件里的依赖注入想法挪到物联网上不是说要在单片机和传感器之间搞容器而是强调“设备逻辑与业务规则分离接口契约清晰可控”。比如一个智能灯控项目设备层只负责上报开关状态和接收指令具体的联动规则像“人走灯灭”“亮度超过阈值就调低”全部放到服务端应用层去实现。这样硬件只做硬件该做的事固件更新时不用反复改业务规则这正是分层带来的维护性好处。反过来物联网应用层往往就是一个微服务系统这时候前面写的三层架构和依赖注入整套方法论都能直接复用。设备接入网关负责把海量消息转换成标准事件业务服务面向领域模型处理规则数据服务负责写时序数据库或消息队列各层之间同样可以使用接口和依赖注入来保持灵活。可以说物联网让“分层”不只停留在代码层面而是延伸到了整条物理链路。5. 常见问题与排查技巧实录再好的架构落到代码里总会有几个反复出现的坑。我把实际项目里踩过、处理过的典型问题整理成一份速查表顺便说说我自己的排查思路。你会发现在大多数情况下架构问题不是突然冒出来的而是设计时埋下的线头。问题现象 → 排查重点 容器报循环依赖 → 检查两个类是否真的平级 Singleton中出现Scoped依赖 → 检查注册生命周期 Controller里悄悄写了SQL → 检查边界是否被绕开 Repository抛原始数据库异常 → 检查是否有统一的异常翻译层 同一请求内多次创建上下文 → 检查是否错误使用了Transient5.1 循环依赖不是死循环是设计信号循环依赖是依赖注入里最常见的坑。比如A服务要用B服务B服务又要用A服务容器在建对象时怎么造都会绕圈。遇到这种问题我的第一反应不是去怎么调配置让容器“跳过校验”而是重新审视这两个类是不是真的平级。通常可以做一层拆解公共部分下沉到另一个服务或者把其中一个依赖改成事件订阅等方式让依赖变成单向的。多数的循环依赖本质上是设计问题硬解不如重构。5.2 生命周期选错运行期才爆雷生命周期和无穷依赖的另一个坑紧密相关。Scoped服务注入到Singleton服务里在ASP.NET Core里会直接抛“Cannot consume scoped service from singleton”的异常这类问题靠日志很容易定位。更隐蔽的是在你迁移老代码时把原本以为的无状态类注册成Singleton结果发现它内部包含DbContext对象一瞬间所有请求共用同一个数据上下文数据错乱、并发异常全来了。我的习惯是注册之前先问一句“这个类有没有共享可变状态”有就老实Scoped没有才考虑Singleton。5.3 三个我实践后很想提醒的细节第一接口命名别太随意最好按职责来像IOrderService、IOrderRepository这种约定后续找依赖和调注入都省事。第二层间尽量不要互相传递过于庞大的实体要定义精简的DTO否则依赖注入再漂亮数据边界乱照样难维护。第三依赖注入不是银弹别抱着“反正可以用注入换实现”的心态滥用装饰器比如为了加一层缓存写五六个工厂反而把小项目变得复杂。适度分层保持依赖方向单向才是实际开发中最稳的状态。我在实际项目中做模块划分时最强烈的体会是三层架构和依赖注入不是两个孤立的技术点它们搭配起来才能让团队在改需求的时候少一点“惊动全楼”的感觉。前几天有一个新同事问我要不要给Repository加接口我说关键不是接口数量而是依赖方向。这个标准我建议你牢记。
返回列表