单一职责原则(Single Responsibility Principle, SRP)
什么是单一职责原则
一个类只负责一件事情
一个模块的所有功能都应该高度相关、围绕同一职责
当需求变更时,应只影响到一个类,而不是同时影响多个不相关功能的类
为什么要遵守SRP
- 降低耦合
当一个类承担多种职责时,一个职责的变化可能会影响到其他职责。
- 提高可维护性
变更影响面更小,维护更轻松。
- 提高可测试性
单一职责的类更容易编写单元测试。
- 增强可读性
结构更清晰,让别人一眼就知道这个类的目的。
示例
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
- 减少风险
改旧代码容易引入 Bug。扩展新代码更安全。
- 提高可维护性
避免在核心流程中不停加 if-else、switch-case。
- 提高可扩展性
新增需求只需要新增类或策略,而不是动旧逻辑。
示例:
对象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(); } }
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(); } }
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("转账"); } }
|
依赖倒置原则
什么是依赖倒置原则
高层模(调用者)块不应该依赖于底层模块(被调用者)。两个都应该依赖于抽象。
抽像不应该依赖于细节,细节因该依赖于抽象
依赖倒置原则的本质就是通过抽象(接口或抽象类)使个格类或模块的实现彼此独立,互不影响,实现模块间的松耦合。
关于依赖
- 一个优秀的面向对象程序设计,核心的原则之一就是将变化封装,使得变化部分发生变化时,其他部分,不受影响。
- 为了实现这个目的,需要使用面向接口编程,使用后,客户类,不再直接依赖服务类,而是依赖一个抽象的接口,这样,客户端就不能在内部直接实例化服务类。
- 但是客户类在运行的过程中,又需要具体的服务类来提供服务,因为接口是不能实例化的,就产生了一个矛盾:客户类不允许实例化服务类,但是客户类又需要服务类的服务。
- 为了解决这个矛盾,我们设计了一个解决方案,既:客户类定义一个注入点,用于服务类的注入,而客户类的客户类(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),也叫最少知识原则,一句话版是:
一个对象应该尽量少地了解其他对象。
- 什么是“直接朋友”?
对一个对象 A 来说,下面这些算它的“朋友”:
自身(this)
作为参数传进来的对象
自己创建的对象
自己的成员变量
其他通过“链式调用”拿到的对象,都不是朋友。
- 经典反例(违反迪米特原则)
order.GetCustomer().GetAddress().GetCity();
问题在哪?
order 本来只该关心 Customer
却一路摸到 Address、City
耦合链太长,一改就崩
Order 知道得太多了。
- 符合迪米特原则的写法
把“细节”藏起来
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 怎么改,对外部影响最小
可维护性明显提升
- 和其他原则的关系(很重要)
单一职责原则(SRP)
职责清晰 → 不容易“知道太多”
SRP 是基础,LoD 是结果
接口隔离原则(ISP)
接口小而精 → 自然只暴露必要信息
ISP 能帮助实现 LoD
依赖倒置原则(DIP)
依赖抽象而非细节
降低“认识的人”的复杂度
- 在实际开发中的典型应用
1️Service 调用 Service
不要:
推荐:
DTO / Entity 设计
不要到处暴露对象内部结构
用行为方法代替“裸 Getter”
.NET / 依赖注入 场景(结合你常用的点)
你在做 Reflection / 模块加载 / 插件系统 时尤其重要:
插件直接操作宿主内部对象
插件只依赖 宿主暴露的最小接口
1 2 3 4 5
| public interface IPluginContext { ILogger Logger { get; } IConfiguration Config { get; } }
|
插件 不需要知道宿主到底有多少服务。
合成复用原则
- 合成复用原则(Composite Reuse Principle,CRP)
一句话先记住
优先使用“组合 / 聚合”,而不是“继承”来实现复用。
再狠一点的版本是:
能不用继承,就别用继承。
继承一多,就会出现:
类层次爆炸
父类一改,子类全员受伤
子类被迫继承不需要的行为
强耦合,不灵活
- 什么是“合成 / 聚合”?
组合(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 说的就是:用这两种方式复用行为,而不是继承。
- 经典对比:继承 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); } }
|
优点:
低耦合
易扩展
完美适配 依赖注入
- 和其他设计原则的关系(体系感)
里氏替换原则(LSP)
继承必须满足 LSP
CRP:那我干脆少用继承
依赖倒置原则(DIP)
组合 + 接口 = DIP 的天然搭档
开闭原则(OCP)
新功能 → 新组合对象
不改原有类
- 在 .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; } }
|
插件只“组合”宿主能力:
日志
配置
事件总线
插件复用的是“能力”,不是“类层次”。
- 什么时候“可以”用继承?
CRP ≠ 禁止继承,只是 慎用:
可以继承的场景:
is-a 关系非常明确
父类非常稳定
子类确实需要父类全部行为