Contents

软件开发概念

一、SOLID 原则(重点:S、O、D)

SOLID 是面向对象设计的五个原则,其中三个与依赖注入直接相关。

1. 单一职责原则(SRP)

一个类应该只有一个引起它变化的原因。

通俗讲:一个类只做一件事。
与依赖注入的关系:如果一个类负责创建依赖、又执行业务逻辑、又做日志记录,它的职责就不单一。DI 把“创建依赖”的职责移除,让类只关注核心逻辑。

反面例子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class OrderService {
    void process(Order order) {
        // 验证
        if (order.getTotal() <= 0) throw new Exception();
        // 存数据库
        Connection conn = DriverManager.getConnection(...);
        // 发邮件
        MailSender.send(...);
    }
}

改造后

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class OrderService {
    private Validator validator;
    private Repository repository;
    private EmailSender emailSender;

    void process(Order order) {
        validator.validate(order);
        repository.save(order);
        emailSender.notify(order);
    }
}

每个依赖类自己再遵循 SRP。

2. 开闭原则(OCP)

对扩展开放,对修改关闭。

通俗讲:增加新功能时,不改原有代码,而是新增类。
与依赖注入的关系:依赖注入依赖于抽象接口,新实现类只要实现接口就能被注入,无需改动使用方。

示例:支付方式扩展

1
2
3
4
5
6
7
8
interface PaymentMethod { void pay(double amount); }
class CreditCard implements PaymentMethod {...}
class PayPal implements PaymentMethod {...}

class Checkout {
    private PaymentMethod payment; // 通过 DI 注入
    void checkout(double amount) { payment.pay(amount); }
}

增加 BitcoinPayment 时,Checkout 类代码一行不改。

3. 依赖倒置原则(DIP)

高层模块不应依赖低层模块,二者都应依赖抽象;抽象不应依赖细节,细节应依赖抽象。

这就是依赖注入的理论基础
没有 DIP,你会写 OrderService 直接依赖 MySqlDatabase;有了 DIP,依赖接口 Database,具体数据库实现类变成“可插拔”。

依赖注入是实现 DIP 的常见手段——把具体依赖通过构造函数或 setter 从外部注入。


二、设计模式(常用:工厂、策略、观察者)

设计模式是常见问题的可复用解决方案。这里只讲三个最实用且与解耦相关的。

1. 工厂模式(Factory Pattern)

解决的问题:对象的创建逻辑复杂(比如需要条件判断、配置读取、对象池),不应该散落在业务代码中。

简单工厂

1
2
3
4
5
6
7
class PaymentFactory {
    PaymentMethod create(String type) {
        if ("credit".equals(type)) return new CreditCard();
        if ("paypal".equals(type)) return new PayPal();
        throw new IllegalArgumentException();
    }
}

工厂方法:每个产品有一个对应的工厂子类。
抽象工厂:创建一系列相关产品家族。

与 DI 的关系:DI 容器本身就像一个大型工厂,负责创建和管理对象的生命周期。但工厂模式常用于局部创建逻辑,DI 更关注外部注入。

2. 策略模式(Strategy Pattern)

解决的问题:一个行为有多种算法实现,需要动态切换(比如不同的折扣计算、排序算法)。

结构:上下文(Context)持有策略接口,具体策略类实现算法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
interface DiscountStrategy { double apply(double price); }

class NoDiscount implements DiscountStrategy {...}
class BlackFridayDiscount implements DiscountStrategy {...}

class PriceCalculator {
    private DiscountStrategy discount;
    void setDiscount(DiscountStrategy discount) { this.discount = discount; }
    double calculate(double price) { return discount.apply(price); }
}

与 DI 的关系:策略对象通常通过 Setter 或构造函数注入到上下文中。这正是依赖注入的典型场景。

3. 观察者模式(Observer Pattern)

解决的问题:一个对象状态变化,需要通知多个其他对象(事件驱动、发布-订阅)。

示例:订单支付成功后需要通知库存系统、积分系统、物流系统。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
interface PaymentObserver { void onPaymentSuccess(Order order); }

class StockObserver implements PaymentObserver {...}
class PointsObserver implements PaymentObserver {...}

class PaymentProcessor {
    private List<PaymentObserver> observers = new ArrayList<>();
    void addObserver(PaymentObserver o) { observers.add(o); }   // 注入观察者
    void pay(Order order) {
        // 支付逻辑...
        observers.forEach(o -> o.onPaymentSuccess(order));
    }
}

与 DI 的关系:观察者列表通过依赖注入(通常是构造函数或集合注入)来组装,这样新增观察者不影响 PaymentProcessor


三、单元测试与 TDD(测试驱动开发)

为什么单元测试“逼迫解耦”?

如果要测试 OrderService.process(),但内部直接 new DatabaseConnection(),那这个测试就会连真正的数据库。结果:

  • 测试慢
  • 测试不可重复(数据残留)
  • 无法测试异常场景(如数据库断开)

为了可测试,你被迫使用依赖注入:把 DatabaseConnection 改为接口,测试时注入一个 MockDatabaseConnection(模拟对象)。

TDD 流程(红-绿-重构)

  1. 写一个失败的测试(红):先想好期望的行为,测试用断言,此时代码不存在或未实现。
  2. 写最少代码让测试通过(绿):任何粗暴实现都可以,只要通过。
  3. 重构(重构):消除重复、改善设计,确保测试仍然绿。

反推好设计:因为测试必须通过依赖注入来隔离外部资源,你会自然写出低耦合、高内聚、面向接口的代码。

实践建议:从一个小函数开始尝试 TDD,比如一个计算器或字符串处理函数。


四、重构(Martin Fowler《重构》)

核心理念

在不改变软件外部行为的前提下,改善其内部结构。

常用手法(摘几例)

  • 提取函数(Extract Method):把一段长代码提取成小函数,命名清晰。
  • 搬移函数(Move Method):当一个函数更频繁地使用另一个类的特性,就搬过去。
  • 以多态取代条件表达式(Replace Conditional with Polymorphism):上面策略模式就是典型。
  • 引入参数对象(Introduce Parameter Object):如果几个参数总是同时出现,封装成对象。

与依赖注入的关系

重构常常导向依赖注入:比如你发现一个类里 new 太多,就“将构造函数改为显式依赖注入”,然后把创建逻辑移到外部(工厂或 DI 容器)。

安全重构的前提:要有充分的单元测试,否则你不知道改坏了哪里。


五、版本控制(Git)工作流

核心概念

  • 功能分支(Feature Branch):每个新功能或修复在一个单独分支上开发,从 maindevelop 拉出。
  • Pull Request / Merge Request:分支合并前需要评审,保持代码质量。
  • 分支策略:常见的有 Git Flow(主分支 main,开发分支 develop,功能分支、发布分支、热修复分支)和 GitHub Flow(只有 main,所有功能分支直接合并到 main 并部署)。

基本工作流示例(GitHub Flow)

1
2
3
4
5
6
git checkout -b feature/login     # 创建分支
# 写代码,多次 commit
git push -u origin feature/login  # 推送到远程
# 在 GitHub 上开 Pull Request
# 团队评审、讨论
# 合并到 main,并删除分支

实践细节

  • 提交信息规范:feat: add login, fix: null pointer
  • 保持小步提交,每次关注一个逻辑改动
  • 使用 git rebase 整理提交历史(但要小心公共分支)

六、CI/CD 基础

定义

  • CI(Continuous Integration):每次开发者推送代码到共享分支,自动触发构建、测试、静态分析。目标是尽早发现集成问题。
  • CD(Continuous Delivery / Deployment)
    • Delivery:自动化测试通过后,软件随时可以手动部署到生产。
    • Deployment:自动化部署到生产,全自动。

典型流程

  1. 开发者推送代码到 Git 仓库。
  2. CI 服务器(Jenkins、GitHub Actions、GitLab CI)检出代码。
  3. 运行构建命令(编译、打包)。
  4. 运行单元测试、集成测试。
  5. 运行代码扫描(SonarQube)。
  6. 如果成功,构建制品(如 jar、docker 镜像)并推送到制品库。
  7. 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 设计

核心原则

  1. 资源(Resource)导向:用名词表示资源,如 /users, /orders/{id}
  2. HTTP 方法语义
    • GET:读取资源
    • POST:创建资源
    • PUT / PATCH:完整/部分更新
    • DELETE:删除资源
  3. 状态无关(Stateless):每个请求包含所需全部信息(如认证 token)。
  4. 使用 HTTP 状态码
    • 200 OK
    • 201 Created
    • 400 Bad Request
    • 401 Unauthorized
    • 404 Not Found
    • 500 Internal Server Error
  5. 超媒体(HATEOAS):可选高级原则,响应中包含能执行下一步操作的链接。

与依赖注入的关系

一个 REST Controller 通常会依赖 Service 或 Repository,这些依赖通过 DI 注入到 Controller 中(如 Spring 的 @Autowired)。

示例

1
2
3
4
5
6
7
8
@RestController
class UserController {
    private final UserService service; // DI 注入
    @GetMapping("/users/{id}")
    public UserDto getUser(@PathVariable Long id) {
        return service.findById(id);
    }
}

九、数据库事务与隔离级别

事务(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 一个数据库连接
这就好比你需要用电,自己先建个发电厂——代码紧密耦合,难以测试和修改

1
2
3
4
5
// 紧耦合的例子
class OrderService {
    private DatabaseConnection connection = new DatabaseConnection();
    // OrderService 直接依赖了具体的 DatabaseConnection 类
}

如果用依赖注入,订单服务只需要声明“我需要一个数据库连接接口”,而具体用哪种数据库连接(MySQL、Oracle 等),则由外部决定并“给”它
就像你家里只留好电源插座,具体的电由电厂通过电网提供

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 解耦的例子
class OrderService {
    private DatabaseConnection connection;

    // 通过构造函数注入依赖
    public OrderService(DatabaseConnection connection) {
        this.connection = connection;
    }
    // 现在 OrderService 只依赖于接口,不依赖于具体实现
}

依赖注入如何达成解耦?

  • 分离创建与使用:对象不再负责创建依赖,职责单一,只需关注核心业务。
  • 面向接口编程:依赖通常定义为接口引用,具体实现可以随时替换而不改变使用方代码。
  • 提高可测试性:可以轻松注入模拟(Mock)对象,不必连接真实数据库。
  • 便于管理生命周期:依赖对象(如数据库连接池)可由统一容器管理,避免资源泄露。

常见的注入方式有三种:

  1. 构造函数注入(最推荐):依赖在对象创建时通过参数传入,保证对象构造完成后完全可用、不可变。
  2. Setter 注入:通过 setter 方法注入,允许可选依赖或后续更改,但对象可能处于不完整状态。
  3. 接口注入:实现特定接口,让容器通过接口提供依赖(较少使用)。

在实际开发中,Spring、Dagger、Guice 等框架就是依赖注入的典型案例。比如 Spring 的 @Autowired 注解,配合容器管理的 Bean,能极大降低组件之间的耦合度。

解耦后的实际好处:当需要从 MySQL 换成 MongoDB 时,你只需修改一处配置(或者创建新的实现类),所有使用数据库连接的 OrderService、UserService 等代码一行都不用改。这就是依赖注入带来的真正的“可插拔”架构。


能否请你设计一个“每周末练习一个概念”的具体任务清单。用编程语言和技术栈:Python/FastAPI