软件开发概念
一、SOLID 原则(重点:S、O、D)
SOLID 是面向对象设计的五个原则,其中三个与依赖注入直接相关。
1. 单一职责原则(SRP)
一个类应该只有一个引起它变化的原因。
通俗讲:一个类只做一件事。
与依赖注入的关系:如果一个类负责创建依赖、又执行业务逻辑、又做日志记录,它的职责就不单一。DI 把“创建依赖”的职责移除,让类只关注核心逻辑。
反面例子:
|
|
改造后:
|
|
每个依赖类自己再遵循 SRP。
2. 开闭原则(OCP)
对扩展开放,对修改关闭。
通俗讲:增加新功能时,不改原有代码,而是新增类。
与依赖注入的关系:依赖注入依赖于抽象接口,新实现类只要实现接口就能被注入,无需改动使用方。
示例:支付方式扩展
|
|
增加 BitcoinPayment 时,Checkout 类代码一行不改。
3. 依赖倒置原则(DIP)
高层模块不应依赖低层模块,二者都应依赖抽象;抽象不应依赖细节,细节应依赖抽象。
这就是依赖注入的理论基础。
没有 DIP,你会写 OrderService 直接依赖 MySqlDatabase;有了 DIP,依赖接口 Database,具体数据库实现类变成“可插拔”。
依赖注入是实现 DIP 的常见手段——把具体依赖通过构造函数或 setter 从外部注入。
二、设计模式(常用:工厂、策略、观察者)
设计模式是常见问题的可复用解决方案。这里只讲三个最实用且与解耦相关的。
1. 工厂模式(Factory Pattern)
解决的问题:对象的创建逻辑复杂(比如需要条件判断、配置读取、对象池),不应该散落在业务代码中。
简单工厂:
|
|
工厂方法:每个产品有一个对应的工厂子类。
抽象工厂:创建一系列相关产品家族。
与 DI 的关系:DI 容器本身就像一个大型工厂,负责创建和管理对象的生命周期。但工厂模式常用于局部创建逻辑,DI 更关注外部注入。
2. 策略模式(Strategy Pattern)
解决的问题:一个行为有多种算法实现,需要动态切换(比如不同的折扣计算、排序算法)。
结构:上下文(Context)持有策略接口,具体策略类实现算法。
|
|
与 DI 的关系:策略对象通常通过 Setter 或构造函数注入到上下文中。这正是依赖注入的典型场景。
3. 观察者模式(Observer Pattern)
解决的问题:一个对象状态变化,需要通知多个其他对象(事件驱动、发布-订阅)。
示例:订单支付成功后需要通知库存系统、积分系统、物流系统。
|
|
与 DI 的关系:观察者列表通过依赖注入(通常是构造函数或集合注入)来组装,这样新增观察者不影响 PaymentProcessor。
三、单元测试与 TDD(测试驱动开发)
为什么单元测试“逼迫解耦”?
如果要测试 OrderService.process(),但内部直接 new DatabaseConnection(),那这个测试就会连真正的数据库。结果:
- 测试慢
- 测试不可重复(数据残留)
- 无法测试异常场景(如数据库断开)
为了可测试,你被迫使用依赖注入:把 DatabaseConnection 改为接口,测试时注入一个 MockDatabaseConnection(模拟对象)。
TDD 流程(红-绿-重构)
- 写一个失败的测试(红):先想好期望的行为,测试用断言,此时代码不存在或未实现。
- 写最少代码让测试通过(绿):任何粗暴实现都可以,只要通过。
- 重构(重构):消除重复、改善设计,确保测试仍然绿。
反推好设计:因为测试必须通过依赖注入来隔离外部资源,你会自然写出低耦合、高内聚、面向接口的代码。
实践建议:从一个小函数开始尝试 TDD,比如一个计算器或字符串处理函数。
四、重构(Martin Fowler《重构》)
核心理念
在不改变软件外部行为的前提下,改善其内部结构。
常用手法(摘几例)
- 提取函数(Extract Method):把一段长代码提取成小函数,命名清晰。
- 搬移函数(Move Method):当一个函数更频繁地使用另一个类的特性,就搬过去。
- 以多态取代条件表达式(Replace Conditional with Polymorphism):上面策略模式就是典型。
- 引入参数对象(Introduce Parameter Object):如果几个参数总是同时出现,封装成对象。
与依赖注入的关系
重构常常导向依赖注入:比如你发现一个类里 new 太多,就“将构造函数改为显式依赖注入”,然后把创建逻辑移到外部(工厂或 DI 容器)。
安全重构的前提:要有充分的单元测试,否则你不知道改坏了哪里。
五、版本控制(Git)工作流
核心概念
- 功能分支(Feature Branch):每个新功能或修复在一个单独分支上开发,从
main或develop拉出。 - Pull Request / Merge Request:分支合并前需要评审,保持代码质量。
- 分支策略:常见的有 Git Flow(主分支
main,开发分支develop,功能分支、发布分支、热修复分支)和 GitHub Flow(只有main,所有功能分支直接合并到main并部署)。
基本工作流示例(GitHub Flow)
|
|
实践细节:
- 提交信息规范:
feat: add login,fix: null pointer - 保持小步提交,每次关注一个逻辑改动
- 使用
git rebase整理提交历史(但要小心公共分支)
六、CI/CD 基础
定义
- CI(Continuous Integration):每次开发者推送代码到共享分支,自动触发构建、测试、静态分析。目标是尽早发现集成问题。
- CD(Continuous Delivery / Deployment):
- Delivery:自动化测试通过后,软件随时可以手动部署到生产。
- Deployment:自动化部署到生产,全自动。
典型流程
- 开发者推送代码到 Git 仓库。
- CI 服务器(Jenkins、GitHub Actions、GitLab CI)检出代码。
- 运行构建命令(编译、打包)。
- 运行单元测试、集成测试。
- 运行代码扫描(SonarQube)。
- 如果成功,构建制品(如 jar、docker 镜像)并推送到制品库。
- CD 环节将制品部署到测试环境,通过后自动或手动部署到生产。
与解耦的关系
CI/CD 要求代码能够被自动构建和测试,这倒逼你使用依赖注入来隔离环境(如数据库、文件系统、外部 API)以便在 CI 中运行快速且可靠的测试。
七、领域驱动设计(DDD)基础概念
DDD 是一种复杂业务系统的建模方法,通过将业务逻辑映射到代码中来管理复杂度。
核心概念
| 概念 | 说明 | 示例 |
|---|---|---|
| 实体(Entity) | 有唯一标识,且标识在其生命周期中不变,属性可变 | 订单(Order),ID 不变,但地址、状态可改 |
| 值对象(Value Object) | 无唯一标识,由属性值整体定义;不可变 | 地址(Address)、金额(Money)、颜色 |
| 聚合(Aggregate) | 一组相关对象的集合,聚合根(Aggregate Root)是外部访问的唯一入口 | 订单聚合:订单(根)+ 订单条目(实体)+ 地址(值对象) |
| 领域服务(Domain Service) | 处理无法放在实体或值对象中的业务逻辑 | 转账服务(跨账户操作) |
| 仓储(Repository) | 用于聚合的持久化,类似集合的接口 | OrderRepository 负责保存和查找订单 |
| 工厂(Factory) | 负责复杂聚合的创建逻辑 | OrderFactory 创建带有默认状态的订单 |
与依赖注入的关系
- 仓储、工厂、领域服务通常通过 DI 注入到应用服务(Application Service)中。
- DDD 推崇“依赖倒置”:领域层定义仓储接口,基础设施层(数据库)实现接口,然后通过 DI 将具体实现注入领域或应用层。
八、RESTful API 设计
核心原则
- 资源(Resource)导向:用名词表示资源,如
/users,/orders/{id}。 - HTTP 方法语义:
- GET:读取资源
- POST:创建资源
- PUT / PATCH:完整/部分更新
- DELETE:删除资源
- 状态无关(Stateless):每个请求包含所需全部信息(如认证 token)。
- 使用 HTTP 状态码:
- 200 OK
- 201 Created
- 400 Bad Request
- 401 Unauthorized
- 404 Not Found
- 500 Internal Server Error
- 超媒体(HATEOAS):可选高级原则,响应中包含能执行下一步操作的链接。
与依赖注入的关系
一个 REST Controller 通常会依赖 Service 或 Repository,这些依赖通过 DI 注入到 Controller 中(如 Spring 的 @Autowired)。
示例
|
|
九、数据库事务与隔离级别
事务(ACID)
- 原子性:要么全做,要么全不做
- 一致性:事务前后数据约束不被破坏
- 隔离性:并发事务之间互不干扰(通过隔离级别控制)
- 持久性:提交后数据永久保存
隔离级别(从低到高,性能逐降,但问题越少)
| 级别 | 脏读 | 不可重复读 | 幻读 | 说明 |
|---|---|---|---|---|
| Read Uncommitted | 可能 | 可能 | 可能 | 读未提交,极少用 |
| Read Committed | 不可能 | 可能 | 可能 | Oracle、SQL Server 默认,只读已提交数据 |
| Repeatable Read | 不可能 | 不可能 | 可能(MySQL InnoDB 默认,通过 MVCC 基本避免幻读) | 保证同一事务多次读相同行一致 |
| Serializable | 不可能 | 不可能 | 不可能 | 最高隔离,完全串行化,性能极低 |
常见问题说明:
- 脏读:读到其他事务未提交的数据
- 不可重复读:同一事务两次读同一行,结果不同(因为被其他事务修改并提交)
- 幻读:范围查询时,其他事务插入了新行,导致两次查询记录数不同
与依赖注入的关系
在应用代码中,事务边界通常由框架管理(如 Spring @Transactional)。依赖注入可以让事务管理器(TransactionManager)轻松被替换,比如从 JDBC 事务切换到 JTA 分布式事务。
十、MVC 等架构模式
MVC(Model-View-Controller)
- Model:数据和业务逻辑(通常包含 Service、Repository)
- View:用户界面(HTML、JSON 视图)
- Controller:接收请求,调用 Model,返回 View
流行实现:Spring MVC、ASP.NET Core MVC。
依赖注入的角色:Controller 依赖的 Service 通过 DI 注入,Service 依赖的 Repository 也通过 DI 注入,形成链式注入。
其他架构模式(简要)
- 三层架构(3-tier):表示层、业务逻辑层、数据访问层。MVC 的 Controller+View 属于表示层。
- 整洁架构(Clean Architecture):强调依赖规则——外层(框架、数据库)依赖内层(业务实体),内层不知道外层。这是 DIP 的终极体现,需要依赖注入来“反转”依赖方向。
- CQRS(命令查询职责分离):读写分离,命令和查询使用不同模型。常与事件溯源组合。
依赖注入
Dependency Injection,简称 DI。是一种实现控制反转的设计模式,其核心目的确实就是解耦
简单来说,它让一个对象不再自己主动去创建或查找它所依赖的其他对象,而是由外部(比如一个容器或调用者)把这些依赖“注入”进来
举个例子,一个“订单服务”需要依赖“数据库连接”。在传统写法中,订单服务内部会自己 new 一个数据库连接
这就好比你需要用电,自己先建个发电厂——代码紧密耦合,难以测试和修改
|
|
如果用依赖注入,订单服务只需要声明“我需要一个数据库连接接口”,而具体用哪种数据库连接(MySQL、Oracle 等),则由外部决定并“给”它
就像你家里只留好电源插座,具体的电由电厂通过电网提供
|
|
依赖注入如何达成解耦?
- 分离创建与使用:对象不再负责创建依赖,职责单一,只需关注核心业务。
- 面向接口编程:依赖通常定义为接口引用,具体实现可以随时替换而不改变使用方代码。
- 提高可测试性:可以轻松注入模拟(Mock)对象,不必连接真实数据库。
- 便于管理生命周期:依赖对象(如数据库连接池)可由统一容器管理,避免资源泄露。
常见的注入方式有三种:
- 构造函数注入(最推荐):依赖在对象创建时通过参数传入,保证对象构造完成后完全可用、不可变。
- Setter 注入:通过 setter 方法注入,允许可选依赖或后续更改,但对象可能处于不完整状态。
- 接口注入:实现特定接口,让容器通过接口提供依赖(较少使用)。
在实际开发中,Spring、Dagger、Guice 等框架就是依赖注入的典型案例。比如 Spring 的 @Autowired 注解,配合容器管理的 Bean,能极大降低组件之间的耦合度。
解耦后的实际好处:当需要从 MySQL 换成 MongoDB 时,你只需修改一处配置(或者创建新的实现类),所有使用数据库连接的 OrderService、UserService 等代码一行都不用改。这就是依赖注入带来的真正的“可插拔”架构。
能否请你设计一个“每周末练习一个概念”的具体任务清单。用编程语言和技术栈:Python/FastAPI