0%

代码整洁之道 - 系统

在阅读本章时,不是太明白,遂借助 deepseek 理解本章:

核心思想:构造与使用分离

这是本章最根本、最重要的理念。作者认为,系统混乱的主要根源在于:

构造(Construction) 和 使用(Use) 这两件事被混在了一起。

  • 构造:创建对象、定义依赖关系、组装应用程序结构的过程。(如何将零件组装成引擎

  • 使用:系统运行时,对象之间协作执行业务逻辑的过程。(如何踩油门让汽车跑起来

将这两者混杂,会导致代码职责不清、难以测试和维护。一个整洁的系统架构应该将这两者清晰地分离开。


关键策略与模式

1. 依赖注入(Dependency Injection, DI)

是什么:实现“构造与使用分离”的核心技术。对象不应自己创建其依赖,而应由外部容器将依赖“注入”给它(通常通过构造函数或属性)。

为什么

  • 控制反转(IoC):将创建依赖的控制权从类内部反转到了外部容器,降低耦合度

  • 极致可测试性:可以轻松注入“模拟对象(Mock)”进行单元测试。

  • 灵活性:更换依赖实现(如换数据库)只需修改容器配置,而无需修改业务代码。

示例

糟糕的写法(构造与使用混合):

1
2
3
4
5
6
7
8
9
public class MyService {
// MyService 自己创建了它所依赖的 Repository,紧密耦合
private MyRepository repository = new MyRepository();

public void doBusinessLogic() {
// 使用依赖
repository.getData();
}
}

问题:无法轻松测试 MyService,因为它强耦合于真实的 MyRepository

整洁的写法(依赖注入):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MyService {
// MyService 只声明它需要什么依赖,而不创建它
private MyRepository repository;

// 依赖通过构造函数"注入"进来
public MyService(MyRepository repository) {
this.repository = repository;
}

public void doBusinessLogic() {
// 使用注入的依赖
repository.getData();
}
}

好处MyService 不再关心 MyRepository 如何被创建。

  • 测试时:可以注入一个 MockMyRepository

  • 生产时:由DI容器(如Spring)负责创建 MyRepository 实例并注入。


2. 面向接口编程

是什么:依赖注入应依赖于接口(Interface),而非具体实现(Concrete Implementation)

为什么:提供抽象和多态能力,让依赖的具体实现可以像插件一样被轻松替换。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 1. 定义接口(抽象)
public interface Repository {
Data getData();
}

// 2. 提供不同实现
public class SqlRepository implements Repository { ... } // 数据库实现
public class InMemoryRepository implements Repository { ... } // 内存实现(用于测试)

// 3. 依赖接口,而非实现
public class MyService {
private Repository repository; // 类型是接口!

public MyService(Repository repository) { // 参数也是接口
this.repository = repository;
}
...
}

好处:不修改 MyService 的任何代码,即可切换不同的数据源实现。


3. 使用DI框架作为”主组件”

是什么:使用如Spring之类的框架作为应用程序的”主组件”(Main Component),由其负责整个系统的构造过程。

为什么:将构造系统的职责完全从业务代码中剥离,让业务代码只关注”使用”(即业务逻辑本身)。

工作流程

  1. 声明:使用注解(如 @Component@Service)标记你的类。

  2. 装配:使用注解(如 @Autowired)声明依赖关系。

  3. 构造:DI容器在启动时读取所有元数据,承担所有”构造”责任,组装好整个对象图。

  4. 运行:你的业务代码直接使用已组装好的对象,纯粹地执行业务逻辑。

示例(简化Spring Boot):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 1. 定义组件(构造的零件)
@Service // 告诉Spring:请管理这个服务
public class CreditCardProcessor implements PaymentProcessor {
public void process(Payment p) { ... }
}

// 2. 使用服务的类(使用者)
@Service
public class ShoppingCartService {
private final PaymentProcessor processor;

@Autowired // Spring自动寻找并注入PaymentProcessor的实现
public ShoppingCartService(PaymentProcessor processor) {
this.processor = processor; // 构造完成!
}

public void checkout(Cart c) { // 纯粹的业务逻辑(使用)
...
processor.process(payment); // 使用注入的依赖
}
}

// 3. 主程序(启动容器)
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args); // 启动容器,完成所有构造
}
}

总结与启示

概念 启示
分离构造与使用 这是设计松散耦合、可维护系统的基石。你的业务代码不应该被 new 关键字污染。
使用DI和接口 这是实现上述分离的首要技术手段。它直接带来了可测试性,这是衡量设计好坏的重要标准。
框架是工具 Spring等DI框架不是目的,而是实现”构造与使用分离”这一目标的强大工具。它们扮演了”系统组装者”的角色。
架构目标 本章的最终目的是:管理复杂性。通过将构造的复杂性隔离到容器中,使得核心业务逻辑变得简单、清晰、易于变化。

一句话总结:

不要让你的业务代码(建筑师)去操心依赖如何被创建和组装(搬砖和水泥),而应该由一个专门的DI容器(施工队)根据配置(蓝图)来完成所有组装工作,然后将完整的、可用的依赖交付给业务代码去使用。


使用 Generic Host (推荐)

对于 C# 的 Console Application,使用依赖注入需要手动设置 DI 容器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using System.Threading.Tasks;

public interface IMessageService
{
void SendMessage(string message);
}

public class EmailService : IMessageService
{
public void SendMessage(string message)
{
Console.WriteLine($"发送邮件: {message}");
}
}

public class NotificationManager
{
private readonly IMessageService _messageService;

public NotificationManager(IMessageService messageService)
{
_messageService = messageService;
}

public void Notify(string message)
{
_messageService.SendMessage(message);
}
}

// 后台服务
public class MyBackgroundService : BackgroundService
{
private readonly NotificationManager _notificationManager;

public MyBackgroundService(NotificationManager notificationManager)
{
_notificationManager = notificationManager;
}

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
_notificationManager.Notify($"消息时间: {DateTime.Now}");
await Task.Delay(1000, stoppingToken);
}
}
}

class Program
{
static async Task Main(string[] args)
{
// 创建并配置主机
var host = Host.CreateDefaultBuilder(args)
.ConfigureServices((context, services) =>
{
// 注册服务
services.AddTransient<IMessageService, EmailService>();
services.AddTransient<NotificationManager>();

// 注册后台服务
services.AddHostedService<MyBackgroundService>();

// 也可以注册其他服务
services.AddSingleton<IConfigService, ConfigService>();
})
.Build();

// 运行主机
await host.RunAsync();
}
}

public interface IConfigService
{
string GetConfigValue(string key);
}

public class ConfigService : IConfigService
{
public string GetConfigValue(string key)
{
return $"Value for {key}";
}
}

虽然 Generic Host 功能强大,但并非所有控制台应用都需要它。对于非常简单的、一次性的脚本任务,直接使用传统的写法可能更直接和轻量。