设计模式的开门砖 - 六大设计原则 - 单一职责原则 - C#
在学习设计模式之前,我强烈建议你先搞懂设计原则。
前言:为什么要先学设计原则?
用最通俗的话来解释:
-
设计原则是"指导思想"——告诉你什么方向是对的
-
设计模式是"通用模板"——告诉你针对某个具体问题怎么写代码是对的
设计模式就是设计原则的"现成答案"。
如果你不了解原则,直接学模式,很容易出现几种情况:
-
为了用模式而用模式,写出又重又绕的代码
-
换个场景就不会用了,因为只记得"怎么写",不明白"为什么这么写"
当你先明白了单一职责原则,再去看桥接模式、装饰器模式、策略模式,你就会恍然大悟:
"原来这些模式就是为了实现单一职责而总结出来的一套经典写法——把不同维度的变化拆到不同的类里去。"
原则是道,模式是术。先悟道,再学术,事半功倍。
一、设计原则是什么?
词语定义:
设计原则是前辈们在长期的软件开发实践中总结出来的通用指导原则,用来帮助我们写出更容易维护、扩展、复用和理解的代码。
它不是具体的代码(不是库、不是框架),也不是外面的语法规则(不遵守代码也能跑),而是一种"最佳实践"或"编程好习惯"。
设计原则解决了什么问题?
无设计原则时常见现象:
-
改一个功能,拆东墙补西墙
-
加一个小需求,改十几种
-
一种几千行,谁也不敢动
设计原则的目标就是避免上面这些情况。
设计原则的本质
识别变化 + 隔离变化
好代码不是一成不变的,而是把容易变化的部分和稳定的部分分开,这样需求增加,你只需要改变"变化区",不碰"稳定区"。
六大设计原则草案速览
| 原则 | 核心一句 |
|---|---|
| 开闭原则 | 增加功能多加新类,少改旧类 |
| 里氏替换原则 | 子类要能替换父类不出错 |
| 依赖倒置原则 | 依赖接口,不依赖具体类 |
| 接口隔离原则 | 接口别太胖,深入定制 |
| 单一职责原则 | 一类只干一件事 |
| 迪米特原则 | 别和陌生人说话 |
二、单一职责原则(SRP)的概念
标准定义
A class should have only one reason to change.
—— 罗伯特·C·马丁(Robert C. Martin),2003年
中文翻译:
一个类应该有且只有一个引起它变化的原因。
两个词拆解
| 要点 | 意义 | 通俗解释 |
|---|---|---|
| 单一 | 一个类只承担一种责任 | 只做一件事,并且把它做好 |
| 职责 | 一个类变化的原因应当只有一个 | 如果老板改需求会导致改这个类,财务改制度也会导致改这个类——那就是两个职责 |
通俗理解
单一职责原则通俗讲就是:一个类不要既管这个又管那个。每个类只对自己的一亩三分地负责,各司其职,互不越界。
为什么这么说?因为一个类承担的职责越多,它被复用的可能性就越小。一个类承担的职责越多,就等于这些职责耦合在了一起。一个职责的变化可能会削弱或抑制这个类完成其他职责的能力。
判断标准:怎么知道"职责过多"?
两个简单的方法:
-
描述法:用一句话描述这个类是干什么的。如果描述里出现了"和"、"或者"、"同时",大概率职责过多了。
比如:"Employee类负责员工薪资计算和报表生成和数据库存储" → 三个职责,严重违反 SRP。
-
变化源法:问自己"什么情况下要改这个类?"如果答案超过一个,就违反 SRP。
一句话记住
一个类,只干一件事。
三、违反 vs 符合单一职责原则(C#代码对比)
违反单一职责原则:一个 Employee 类干了三件事
// 违反 SRP:Employee 类同时承担了业务逻辑、持久化、报表三种职责 public class Employee { public string Name { get; set; } public double BaseSalary { get; set; } public double Bonus { get; set; } // 职责一:薪资计算(业务逻辑) public double CalculateSalary() { double totalSalary = BaseSalary + Bonus; // 五险一金计算…… // 个税计算…… return totalSalary; } // 职责二:数据库操作(持久化) public void SaveToDatabase() { // 拼接 SQL、打开连接、执行插入…… string sql = $"INSERT INTO Employee VALUES ('{Name}', {BaseSalary}, {Bonus})"; Console.WriteLine($"执行 SQL:{sql}"); } // 职责三:报表生成(展示逻辑) public void GeneratePayslip() { // 排版、格式化、导出 PDF…… Console.WriteLine($"————工资条————"); Console.WriteLine($"姓名:{Name}"); Console.WriteLine($"实发:{CalculateSalary()}"); Console.WriteLine($"——————————————"); } }问题分析:
薪资算法变了 → 要改
CalculateSalary,碰了 Employee 类数据库从 MySQL 换到 SQL Server → 要改
SaveToDatabase,又碰了 Employee 类工资条模板换了 → 要改
GeneratePayslip,又双叒叕碰了 Employee 类无论哪个需求变了,都要动同一个类。改一次,就要重新测试整个 Employee。
符合单一职责原则:一个类只干一件事
// 职责一:纯数据模型 — 只负责承载数据 public class Employee { public string Name { get; set; } public double BaseSalary { get; set; } public double Bonus { get; set; } } // 职责二:薪资计算 — 只负责计算逻辑 public class SalaryCalculator { public double Calculate(Employee emp) { double total = emp.BaseSalary + emp.Bonus; // 五险一金计算…… // 个税计算…… return total; } } // 职责三:数据持久化 — 只负责数据库操作 public class EmployeeRepository { public void Save(Employee emp) { string sql = $"INSERT INTO Employee VALUES ('{emp.Name}', {emp.BaseSalary}, {emp.Bonus})"; Console.WriteLine($"执行 SQL:{sql}"); } } // 职责四:报表生成 — 只负责展示 public class PayslipGenerator { private readonly SalaryCalculator _calculator; public PayslipGenerator(SalaryCalculator calculator) { _calculator = calculator; } public void Generate(Employee emp) { Console.WriteLine($"————工资条————"); Console.WriteLine($"姓名:{emp.Name}"); Console.WriteLine($"实发:{_calculator.Calculate(emp)}"); Console.WriteLine($"——————————————"); } }优势分析:
薪资算法变了 → 只改
SalaryCalculator,Employee、Repository、PayslipGenerator 纹丝不动换数据库 → 只改
EmployeeRepository,其他地方不碰工资条换模板 → 只改
PayslipGenerator每个变化只影响一个类,测试范围精准,风险可控。
对比总结表
场景 违反 SRP 符合 SRP 薪资算法变更 改 Employee 类(影响 DB、报表) 只改 SalaryCalculator 数据库切换 改 Employee 类(影响计算、报表) 只改 EmployeeRepository 报表模板修改 改 Employee 类(影响计算、DB) 只改 PayslipGenerator 单次测试范围 整个 Employee 的所有方法 只测改动的那个类 代码复用性 低(计算逻辑绑在 Employee 上,别处没法用) 高(SalaryCalculator 到处可注入)
四、如何识别和拆分职责?
| 方法 | 说明 | 示例 |
|---|---|---|
| 凭感觉 | 描述法:用一句话描述类,出现"和/或者/同时"就拆分 | "它负责数据处理和UI 渲染" → 拆 |
| 看变化源 | 找出导致类变化的不同角色/部门 | HR 提需求会改它、IT 部门提需求也会改它 → 至少两个职责 |
| 按层拆分 | 业务逻辑 != 数据持久化 != 界面展示 | Employee / EmployeeRepository / EmployeeViewModel |
| 看方法调用关系 | 一个类里的某些方法只用到了部分字段,另一些方法只用到了另一些字段 | CalculateSalary 只用 Name/BaseSalary/Bonus;SendEmail 只用 Name/Email → 它们是两个职责 |
拆分粒度把握
不是拆得越细越好。过犹不及:
-
拆太粗 → 一个类几千行,牵一发而动全身
-
拆太细 → 一个功能七八个类来回跳,过度设计,理解成本暴增
最佳粒度:一个类的变化原因只有一个,但这个"原因"是一个完整的、有意义的概念。
五、常见误区
| 误区 | 正解 |
|---|---|
| "一个类只能有一个方法" | SRP 说的是职责单数,不是方法单数。一个类可以有多个方法,只要它们都在完成同一个职责。 |
| "拆得越细越好" | 拆分是有成本的——类变多了,关系变复杂了。当变化源确实只有一个时,不需要为了拆分而拆分。 |
| "SRP 只适用于大类" | SRP 适用于所有层次:方法、类、模块、微服务。一个微服务也可以违反 SRP。 |
| "这是在教我把代码写啰嗦" | 短期看代码量增加了,但长期看每个类更短、更专注、更容易理解和测试。你是在用今天多写几分钟换明天少调几小时。 |
六、结语:多几个类 vs 一个巨大的类
你可能听过这样一句话:"把代码写简单,别搞那么复杂。"
这确实对。但学了单一职责原则之后,你可能会产生一个疑问:
单一职责让我把一个类拆成三四个类,文件数量翻倍,不是变复杂了吗?
答案是:不是变复杂了,是复杂被分摊了。
短视角(只看今天)
| 违反 SRP | 符合 SRP | |
|---|---|---|
| 文件数 | 1 个 Employee.cs | 4 个(Employee / Calculator / Repository / PayslipGenerator) |
| 今天的感受 | ✅ 简单,一个文件全搞定 | ❌ 文件多,跳来跳去 |
纵向视角(看三个月、一年后)
| 违反 SRP | 符合 SRP | |
|---|---|---|
| 第 10 次需求变更 | Employee.cs 从 200 行涨到 800 行 | 每个类依然在 100 行以内 |
| 新人接手 | 看一个 800 行的类,心态崩了 | 每个类看一眼就知道是干什么的 |
| 改 Bug | 难定位,改了这里怕坏那里 | 职责明确,Bug 直接定位到对应类 |
| 单元测试 | 很难写(一个类依赖太多) | 轻松写(每个类独立可测) |
| 代码复用 | 薪资算法锁死在 Employee 里,别处只能复制粘贴 | SalaryCalculator 一行不改,到处注入 |
一句话破
"把代码写简单"不是说把东西都塞进一个文件叫简单。
真正的简单,是你一年后打开一个类,三分钟就搞懂它——因为它只干了一件事,而且干得很好。
两者不冲突:
-
违返 SRP 的"简单" → 今天省事,三个月后没人敢动
-
符合 SRP 的"复杂" → 今天多写几个类,半年后谁都可以维护
真正的厉害
厉害的程序员不是能把所有逻辑塞进一个类里还不出 Bug 的人。
而是知道什么时候该拆、拆成几个类、每个类叫什么名字的人。
单一职责原则给了你一套拆分的依据:
-
"这个类会被同一种原因改变吗?" → 不会 → 拆
-
"这两个方法会同时被同一个需求牵连吗?" → 不会 → 拆
-
"这个字段组和那个字段组服务于不同的功能吗?" → 对 → 拆
金句收尾
一个巨大的类,不叫"封装得好"——叫"还没有炸"。
拆,不是过度设计;拆,是给未来的自己留一条生路。
单一职责原则,就是让你在每个类的大门上写清楚:此处只干一件事,闲杂需求请绕行。
下一篇预告
依赖倒置原则 — "我找谁"变成"谁来找我"。主动权在你手里,别交给具体类。
设计模式的开门砖 - 六大设计原则 - 依赖倒置原则 - C#
如果觉得文章对你有帮助,欢迎点赞、收藏、关注,你的支持是我更新的动力!
更多推荐
所有评论(0)