单一职责原则(Single Responsibility Principle, SRP)

什么是单一职责原则

一个类只负责一件事情
一个模块的所有功能都应该高度相关、围绕同一职责
当需求变更时,应只影响到一个类,而不是同时影响多个不相关功能的类

为什么要遵守SRP

  1. 降低耦合

当一个类承担多种职责时,一个职责的变化可能会影响到其他职责。

  1. 提高可维护性

变更影响面更小,维护更轻松。

  1. 提高可测试性

单一职责的类更容易编写单元测试。

  1. 增强可读性

结构更清晰,让别人一眼就知道这个类的目的。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class TelPhone
{
public void Dial(string phoneNumber)
{
Console.WriteLine("给 " + phoneNumber+" 打电话");
}

public void HangUp(string phoneNumber)
{
Console.WriteLine("挂断 " + phoneNumber+" 的电话");
}

public void SendMessage(string phoneNumber, string message)
{
Console.WriteLine("给 " + phoneNumber + " 发送消息: " + message);
}

public void ReceiveMessage(string phoneNumber, string message)
{
Console.WriteLine(phoneNumber + " 收到消息: " + message);
}
}

变化一:内部的变化,如果TelPhone内部的方法,任意之一,发生了改变,那会需要修改TelPhone,不符合单一职责原则
变化二:外部的变化,如果TelPhone要添加新的方法,也需要修改TelPhone

只有添加的时候才会触发这个类的改变,其他的修改都不要触发这个类的改变

优化:

给每个方法,都提炼成一个接口,抽象成一种能力,然后分别鞋类,去实现接口,最终在TelPhone中,只进行调用。

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
public interface  IDidl
{
void Dial(string phoneNumber);
}

public interface IHangUp
{
void HangUp(string phoneNumber);
}

public interface ISendMessage
{
void SendMessage(string phoneNumber, string message);
}

public interface IReceiveMessage
{
void ReceiveMessage(string phoneNumber, string message);
}

public class Dialclass : IDidl
{
public void Dial(string phoneNumber)
{
Console.WriteLine("给 " + phoneNumber + " 打电话");
}
}
public class HangUpclass : IHangUp
{
public void HangUp(string phoneNumber)
{
Console.WriteLine("挂断 " + phoneNumber + " 的电话");
}
}
public class SendMessageclass : ISendMessage
{
public void SendMessage(string phoneNumber, string message)
{
Console.WriteLine("给 " + phoneNumber + " 发送消息: " + message);
}
}
public class ReceiveMessageclass : IReceiveMessage
{
public void ReceiveMessage(string phoneNumber, string message)
{
Console.WriteLine(phoneNumber + " 收到消息: " + message);
}
}

public class TelPhone
{
private IDidl _dial;
private IHangUp _hangUp;
private ISendMessage _sendMessage;
private IReceiveMessage _receiveMessage;

public TelPhone(IDidl dial, IHangUp hangUp, ISendMessage sendMessage, IReceiveMessage receiveMessage)
{
_dial = dial;
_hangUp = hangUp;
_sendMessage = sendMessage;
_receiveMessage = receiveMessage;
}
public void Dial(string phoneNumber)
{
_dial.Dial(phoneNumber);
}

public void HangUp(string phoneNumber)
{
_hangUp.HangUp(phoneNumber);
}

public void SendMessage(string phoneNumber, string message)
{
_sendMessage.SendMessage(phoneNumber, message);
}

public void ReceiveMessage(string phoneNumber, string message)
{
_receiveMessage.ReceiveMessage(phoneNumber, message);
}
}

开放封闭原则

什么是开放封闭原则

当需求变更时,应该 通过扩展新代码 来应对
而不是修改已稳定上线的旧代码

为什么需要 OCP

  1. 减少风险

改旧代码容易引入 Bug。扩展新代码更安全。

  1. 提高可维护性

避免在核心流程中不停加 if-else、switch-case。

  1. 提高可扩展性

新增需求只需要新增类或策略,而不是动旧逻辑。

示例:

对象1:用户:属性:记录不同类型的用户(存钱、取钱、转账)
对象2:银行柜员:帮助我们用户处理不同的需求
对象3:银行业务系统:处理存钱、取钱、转账等需求的操作系统

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
public class Bank
{
public static void Bankclas(string[] args)
{
BankClient client = new BankClient();
client.BankType = "存款";
BankStuff stuff = new BankStuff(new BankProcess());
stuff.HandleProcess(client);
}
}

public class BankClient
{
public string? BankType{get;set;}
}

public class BankStuff
{
private BankProcess _bankProcess;
public BankStuff(BankProcess bankProcess)
{
_bankProcess = bankProcess;
}
public void HandleProcess(BankClient client)
{
switch (client.BankType)
{
case "存款":
_bankProcess.Deposit();
break;
case "取款":
_bankProcess.Withdraw();
break;
case "转账":
_bankProcess.Transfer();
break;
default:
Console.WriteLine("无效的操作");
break;
}
}
}

public class BankProcess
{
//存款
public void Deposit()
{
Console.WriteLine("存款");
}
//取款
public void Withdraw()
{
Console.WriteLine("取款");
}
//转账
public void Transfer()
{
Console.WriteLine("转账");
}
}

此代码并没有实现单一原则,且开放封闭原则,只开放并没有封闭(一个软件实体应该对扩展开放,对修改封闭。即软件实体应尽量在不修改原有代码的情况下进行扩展)

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
public class Bank
{
public static void Bankclas(string[] args)
{
IBankClient client = new TransferClient();
BankStuff ctx = new BankStuff();
ctx.HandleProcess(client);
}
}

public class BankClient
{
public string? BankType{get;set;}
}

public class BankStuff
{
private IBankProcess? _bankprocess;

public void HandleProcess(IBankClient client)
{
_bankprocess = client.GetBankProcess();
_bankprocess.BankProcess();
}
}

// 以上是Bank.cs的接口定义,下面是BankClient.cs的实现
public interface IBankClient
{
IBankProcess GetBankProcess();
}

public class DepositClient : IBankClient
{
public IBankProcess GetBankProcess()
{
return new Deposit();
}
}

public class WithdrawClient : IBankClient
{
public IBankProcess GetBankProcess()
{
return new Withdraw();
}
}

public class TransferClient : IBankClient
{
public IBankProcess GetBankProcess()
{
return new Transfer();
}
}

// 以上是Bank.cs的实现,下面是BankClient.cs的实现

public interface IBankProcess
{
void BankProcess();
}

public class Deposit : IBankProcess
{
public void BankProcess()
{
Console.WriteLine("存款");
}
}

public class Withdraw : IBankProcess
{
public void BankProcess()
{
Console.WriteLine("取款");
}
}

public class Transfer : IBankProcess
{
public void BankProcess()
{
Console.WriteLine("转账");
}
}

依赖倒置原则

什么是依赖倒置原则

高层模(调用者)块不应该依赖于底层模块(被调用者)。两个都应该依赖于抽象。
抽像不应该依赖于细节,细节因该依赖于抽象
依赖倒置原则的本质就是通过抽象(接口或抽象类)使个格类或模块的实现彼此独立,互不影响,实现模块间的松耦合。

关于依赖

  1. 一个优秀的面向对象程序设计,核心的原则之一就是将变化封装,使得变化部分发生变化时,其他部分,不受影响。
  2. 为了实现这个目的,需要使用面向接口编程,使用后,客户类,不再直接依赖服务类,而是依赖一个抽象的接口,这样,客户端就不能在内部直接实例化服务类。
  3. 但是客户类在运行的过程中,又需要具体的服务类来提供服务,因为接口是不能实例化的,就产生了一个矛盾:客户类不允许实例化服务类,但是客户类又需要服务类的服务。
  4. 为了解决这个矛盾,我们设计了一个解决方案,既:客户类定义一个注入点,用于服务类的注入,而客户类的客户类(Program类)负责根据情况,实例化服务类,注入到客户类中,从而解决了这个矛盾。

依赖关系如何传递?(依赖注入)

通过接口传递(接口注入)
通过构造方法传递
通过属性的Set方法传递

通过接口传递(接口注入 Interface Injection)
1️⃣ 思想

依赖不是通过构造器或属性传

而是通过 一个专门的注入接口 来完成

2️⃣ 结构示意

1
2
3
4
5
Client

实现 Inject 接口

容器调用接口方法

3️⃣ 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public interface ILogger
{
void Log(string message);
}

// 注入接口
public interface ILoggerInjectable
{
void InjectLogger(ILogger logger);
}

public class OrderService : ILoggerInjectable
{
private ILogger _logger;

public void InjectLogger(ILogger logger)
{
_logger = logger;
}
}

容器或调用方:

1
2
var service = new OrderService();
service.InjectLogger(new ConsoleLogger());

4️⃣ 优缺点

✅ 优点

依赖注入过程显式、清晰

适合需要强制注入的场景

❌ 缺点

侵入性强(业务类必须实现注入接口)

实际项目中 很少使用

👉 现代 DI 框架基本不用这种方式

三、通过构造方法传递(构造函数注入)✅【最推荐】
1️⃣ 思想

依赖在对象创建时就必须存在

没有依赖,对象就不合法

2️⃣ 示例

1
2
3
4
5
6
7
8
9
public class OrderService
{
private readonly ILogger _logger;

public OrderService(ILogger logger)
{
_logger = logger;
}
}

容器:

1
services.AddTransient<OrderService>();

DI 容器会自动:

1
2
3
ILogger → ConsoleLogger

OrderService(logger)

3️⃣ 优点(非常重要)

✅ 依赖不可为空(强约束)
✅ 对象一创建就处于“完整状态”
✅ 天然支持 不可变设计(readonly)
✅ 最符合 单一职责原则 + 依赖倒置原则

❌ 缺点

构造函数参数可能变多(但这是“设计问题暴露”,不是坏事)

👉 95% 的场景:首选构造函数注入

四、通过属性的 Set 方法传递(Setter 注入)
1️⃣ 思想

对象可以先创建

依赖在之后通过属性或方法设置

2️⃣ 示例

1
2
3
4
5
6
7
8
9
public class OrderService
{
public ILogger Logger { get; set; }

public void CreateOrder()
{
Logger?.Log("Create order");
}
}

3️⃣ 优缺点

✅ 优点

适合 可选依赖

构造函数保持简洁

❌ 缺点

依赖可能为 null

对象可能处于“半初始化状态”

不利于并发和不变性设计

里氏替换原则

里氏替换原则的本质不是“语法继承”,而是“行为契约继承”。
一旦子类改变了父类对外的行为承诺,继承就是错误设计。

接口隔离原则

客户端不应该被迫依赖它不需要的方法。

接口要小而专一,而不是大而全

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public interface IWorkable
{
void Work();
}

public interface IEatable
{
void Eat();
}

public interface ISleepable
{
void Sleep();
}

实现类按需实现

1
2
3
4
5
6
7
8
9
10
11
public class HumanWorker : IWorkable, IEatable, ISleepable
{
public void Work() { }
public void Eat() { }
public void Sleep() { }
}

public class RobotWorker : IWorkable
{
public void Work() { }
}

接口分离原则要求我们用“多个小接口”
代替“一个大接口”,
让每个客户端只依赖它真正需要的能力。

迪米特原则

迪米特原则(Law of Demeter,LoD),也叫最少知识原则,一句话版是:

一个对象应该尽量少地了解其他对象。

  1. 什么是“直接朋友”?

对一个对象 A 来说,下面这些算它的“朋友”:

自身(this)

作为参数传进来的对象

自己创建的对象

自己的成员变量

其他通过“链式调用”拿到的对象,都不是朋友。

  1. 经典反例(违反迪米特原则)
    order.GetCustomer().GetAddress().GetCity();

问题在哪?

order 本来只该关心 Customer

却一路摸到 Address、City

耦合链太长,一改就崩

Order 知道得太多了。

  1. 符合迪米特原则的写法
    把“细节”藏起来
1
order.GetCustomerCity();
1
2
3
4
5
6
7
8
9
class Order
{
private Customer _customer;

public string GetCustomerCity()
{
return _customer.GetCity();
}
}

优点:

调用方不关心内部结构

Customer / Address 怎么改,对外部影响最小

可维护性明显提升

  1. 和其他原则的关系(很重要)
    单一职责原则(SRP)

职责清晰 → 不容易“知道太多”

SRP 是基础,LoD 是结果

接口隔离原则(ISP)

接口小而精 → 自然只暴露必要信息

ISP 能帮助实现 LoD

依赖倒置原则(DIP)

依赖抽象而非细节

降低“认识的人”的复杂度

  1. 在实际开发中的典型应用
    1️Service 调用 Service

不要:

1
a.B.C.D.DoSomething();

推荐:

1
a.DoSomething();

DTO / Entity 设计

不要到处暴露对象内部结构

用行为方法代替“裸 Getter”

.NET / 依赖注入 场景(结合你常用的点)

你在做 Reflection / 模块加载 / 插件系统 时尤其重要:

插件直接操作宿主内部对象

插件只依赖 宿主暴露的最小接口

1
2
3
4
5
public interface IPluginContext
{
ILogger Logger { get; }
IConfiguration Config { get; }
}

插件 不需要知道宿主到底有多少服务。

合成复用原则

  1. 合成复用原则(Composite Reuse Principle,CRP)
    一句话先记住

优先使用“组合 / 聚合”,而不是“继承”来实现复用。

再狠一点的版本是:

能不用继承,就别用继承。

继承一多,就会出现:
类层次爆炸
父类一改,子类全员受伤
子类被迫继承不需要的行为
强耦合,不灵活

  1. 什么是“合成 / 聚合”?

组合(Composition)

强拥有关系

生命周期一致

典型:成员变量

1
2
3
4
class OrderService
{
private readonly ILogger _logger;
}

聚合(Aggregation)

弱拥有关系

生命周期可独立

常见:构造器 / 参数传入

1
2
3
4
5
6
7
class OrderService
{
public OrderService(ILogger logger)
{
_logger = logger;
}
}

CRP 说的就是:用这两种方式复用行为,而不是继承。

  1. 经典对比:继承 vs 组合

继承式复用(不推荐)

1
2
3
4
5
6
7
class CacheService : RedisClient
{
public void Save(string key, string value)
{
Set(key, value);
}
}

问题:

CacheService 被绑死在 RedisClient

换成 Memory / DistributedCache 成本巨大

测试也很痛苦

合成复用(推荐)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class CacheService
{
private readonly ICacheClient _client;

public CacheService(ICacheClient client)
{
_client = client;
}

public void Save(string key, string value)
{
_client.Set(key, value);
}
}

优点:

低耦合

易扩展

完美适配 依赖注入

  1. 和其他设计原则的关系(体系感)
    里氏替换原则(LSP)

继承必须满足 LSP

CRP:那我干脆少用继承

依赖倒置原则(DIP)

组合 + 接口 = DIP 的天然搭档

开闭原则(OCP)

新功能 → 新组合对象

不改原有类

  1. 在 .NET / 插件 / 模块化场景下的价值(结合你的背景)

你之前提到 Reflection + 模块加载 / 插件引导器,CRP 在这里非常关键

错误方式

1
2
3
class MyPlugin : HostApplication
{
}

插件:

强依赖宿主

宿主一升级,插件全挂

正确方式(合成复用)

1
2
3
4
5
6
7
8
9
class MyPlugin : IPlugin
{
private readonly IPluginContext _context;

public MyPlugin(IPluginContext context)
{
_context = context;
}
}

插件只“组合”宿主能力:

日志

配置

事件总线

插件复用的是“能力”,不是“类层次”。

  1. 什么时候“可以”用继承?

CRP ≠ 禁止继承,只是 慎用:

可以继承的场景:

is-a 关系非常明确

父类非常稳定

子类确实需要父类全部行为