13-C#.Net-设计模式六大原则-学习笔记
六大原则是写出高质量、可维护代码的基础,记住一句话:对修改关闭,对扩展开放是核心思想,其余五条都是在不同角度强化这个目标。
一、SRP — 单一职责原则
Single Responsibility Principle
核心思想
一个类(或方法)只负责一件事,只有一个引起它变化的原因。
通俗理解
厨师只管做菜,服务员只管上菜,收银员只管收钱。如果一个人全干,哪个环节出问题都要改这个人。
代码中的问题演示
// 反例:Animal 类的 Breath() 方法承担了所有动物的呼吸逻辑
public void Breath()
{
if (_Name.Equals("鸡")) Console.WriteLine("呼吸空气");
else if (_Name.Equals("鱼")) Console.WriteLine("呼吸水");
// 每新增一种动物就要改这个方法 —— 违反SRP
}
正确做法:拆分成多个类
// 抽象基类定义职责
public abstract class AbstractAnimal
{
protected string _Name;
public abstract void Breath(); // 呼吸是谁的职责?各自子类的
public abstract void Action(); // 行动是谁的职责?各自子类的
}
public class Chicken : AbstractAnimal
{
public override void Breath() => Console.WriteLine($"{_Name} 呼吸空气");
public override void Action() => Console.WriteLine($"{_Name} flying");
}
public class Fish : AbstractAnimal
{
public override void Breath() => Console.WriteLine($"{_Name} 呼吸水");
public override void Action() => Console.WriteLine($"{_Name} swimming");
}
实际案例:BankClient 拆分
以银行客户端为例,一个典型的违反 SRP 的写法是把所有逻辑堆在一个类里:
// 反例:所有职责混在一起
public class BankClient
{
public void Run()
{
// 1. 验证用户
// 2. 查询余额
// 3. 计算利息
// 4. 展示结果
// 任何一个环节变化,都要改这个类
}
}
正确做法是按变化原因拆分:
public class UserCheckService { public bool CheckUser(string id) { ... } }
public class AccountService { public decimal GetBalance(string id) { ... } }
public class InterestCalculator{ public decimal Calculate(decimal balance) { ... } }
public class DisplayService { public void Show(decimal amount) { ... } }
验证逻辑可能从数据库换成远程接口,余额查询可能换数据源——变化原因不同,就该分开。
记忆口诀
一个类,一件事,改一处,不牵连。
二、OCP — 开闭原则
Open Closed Principle
核心思想
对扩展开放,对修改关闭。新增功能时,尽量不改已有代码,而是新增代码。
通俗理解
手机出了新功能,不是把旧手机拆了重装,而是出一款新型号。
四种扩展方式
以 Student.Study() 方法为例,当需要新增"免费学习"功能时,有四种做法,优劣各不同:
方式1:直接改原方法(最差)
public virtual void Study(bool isFree = false)
{
if (isFree) Console.WriteLine("Study Free");
else Console.WriteLine("Study Paid");
// 改了原有逻辑,可能引入新 Bug,影响已有功能
}
方式2:在原类新增方法(次之)
public class Student
{
public virtual void Study() => Console.WriteLine("Study Paid");
public virtual void StudyFree() => Console.WriteLine("Study Free"); // 新增,不改原方法
}
// 缺点:原类仍然被修改,随着功能增多类会越来越臃肿
方式3:继承扩展(推荐)
public class StudentChild : Student
{
public override void Study()
{
base.Study();
Console.WriteLine("Study Review"); // 扩展行为
// 完全不修改父类,通过继承扩展
}
}
方式4:新增独立类(最彻底)
public class StudentFree // 全新的类,完全不影响原有 Student
{
public virtual void Study() => Console.WriteLine("Study Free");
}
记忆口诀
加功能,加代码;别动旧代码。
三、LSP — 里氏替换原则
Liskov Substitution Principle
核心思想
子类必须能够替换父类使用,且程序行为不变。换句话说:能用父类的地方,换成子类也没问题。
通俗理解
你叫了一辆出租车,来了一辆网约车,你还是能到达目的地——这就是替换。但如果来了一辆自行车,你就到不了——这就违反了替换原则。
C# 中的多态机制
理解 LSP 的关键是搞清楚 C# 里三种方法的行为差异:
// 父类引用指向子类对象
ParentClass instance = new ChildClass();
instance.CommonMethod(); // 调用的是父类的 CommonMethod(new 隐藏,不是多态)
instance.VirtualMethod(); // 调用的是子类的 VirtualMethod(override 覆写,多态生效)
instance.AbstractMethod(); // 调用的是子类的 AbstractMethod(抽象方法必须覆写)
三种方法的区别:
| 方法类型 | 关键字 | 子类能否覆写 | 父类引用调用结果 |
|---|---|---|---|
| 普通方法 | 无 | 用 new 隐藏 |
调用父类版本 |
| 虚方法 | virtual |
用 override |
调用子类版本(多态) |
| 抽象方法 | abstract |
必须 override |
调用子类版本(多态) |
什么时候不该继承?
继承要满足"is-a"关系。如果子类无法完整履行父类的所有契约,就不该继承。
以 People 和 Japanese 为例:假设 People 有一个 Traditional() 方法(传统礼仪),而 Japanese 的礼仪体系完全不同,强行继承后要么抛异常,要么调用方需要做类型判断——这就违反了 LSP。
// 违反LSP的写法
public class Japanese : People
{
public override void Traditional()
{
throw new NotImplementedException(); // 被迫实现,但实际不支持
}
}
// 正确:断掉不合适的继承关系,各自独立
public class Japanese
{
public void Ninja() { ... } // 只实现自己真正有的行为
}
判断是否该继承的简单方法: 把子类替换到所有使用父类的地方,程序行为是否仍然正确?如果不是,就不该继承。
记忆口诀
子类替父类,行为不能变;不是"是一种",就别乱继承。
四、ISP — 接口隔离原则
Interface Segregation Principle
核心思想
不要让一个类被迫实现它用不到的接口方法。接口要小而专,不要大而全。
通俗理解
你去餐厅只想点菜,不需要同时学会做菜、洗碗、收银。如果菜单上把这些都列出来要求你会,那就太荒谬了。
反例:大而全的接口
// 问题接口:包含了所有功能
public interface IExtendAll
{
void Photo(); // 拍照
void Online(); // 上网
void Game(); // 游戏
void Record(); // 录像
void Movie(); // 看电影
void Map(); // 地图
void Pay(); // 支付
}
TV 类实现 IExtendAll 的问题:
public class TV : IExtendAll
{
public void Online() { ... } // 电视能上网 ✓
public void Movie() { ... } // 电视能看电影 ✓
public void Photo() { throw new NotImplementedException(); } // 电视不能拍照!被迫实现 ✗
public void Pay() { throw new NotImplementedException(); } // 电视不能支付!被迫实现 ✗
}
正确做法:拆分成小接口
public interface IExtendPhoto { void Photo(); }
public interface IExtendRecord { void Record(); }
public interface IExtendGame { void Online(); void Game(); }
// 相机只实现它需要的接口
public class Camera : IExtendPhoto, IExtendRecord
{
public void Photo() { ... }
public void Record() { ... }
// 不需要实现 Online/Game/Pay 等
}
// 需要组合时,通过接口继承
public interface IExtendAll2 : IExtendPhoto, IExtendRecord, IExtendGame
{
void Movie(); void Map(); void Pay();
}
ISP vs SRP 的区别
- SRP 说的是类只做一件事
- ISP 说的是接口只暴露需要的方法
记忆口诀
接口要小,按需实现;大接口拆小,各取所需。
五、DIP — 依赖倒置原则
Dependence Inversion Principle
核心思想
高层模块不应该依赖低层模块,两者都应该依赖抽象。抽象不应该依赖细节,细节应该依赖抽象。
通俗理解
你用充电器充电,插头是标准接口(抽象),不管是苹果还是安卓的充电器(具体实现),只要符合接口标准就能用。你不需要为每种手机买专用充电器。
反例:依赖具体类
public class Student
{
// 每新增一款手机,就要在 Student 里加一个方法 —— 高层依赖低层
public void PlayiPhone(iPhone phone) { phone.Call(); phone.Text(); }
public void PlayLumia(Lumia phone) { phone.Call(); phone.Text(); }
public void PlayMi(Mi phone) { phone.Call(); phone.Text(); }
// 来了 Honor 怎么办?再加一个 PlayHonor?
}
正确做法:依赖抽象
// 抽象层
public abstract class AbstractPhone
{
public abstract void Call();
public abstract void Text();
}
// 高层模块依赖抽象,不依赖具体
public class Student
{
public void PlayPhone(AbstractPhone phone) // 参数是抽象类
{
phone.Call();
phone.Text();
}
}
// 新增手机只需新增类,Student 完全不用改
public class Honor : AbstractPhone
{
public override void Call() { ... }
public override void Text() { ... }
}
抽象类 vs 接口
DIP 中的"抽象"可以用抽象类或接口来实现,两者各有适用场景:
// 用接口实现 DIP
public interface IPhotoInterface
{
void Photo();
void Video();
}
// 高层只依赖接口,不关心具体实现
IPhotoInterface photo = new iPad();
photo.Photo();
photo.Video();
// 想换成其他设备?只需注入不同的实现类,上层代码不用改
抽象类和接口的选择:
- 抽象类:有公共实现逻辑可以共享时用(比如多个手机品牌都有相同的
Show()展示逻辑,放在抽象基类里共享) - 接口:纯约束,没有实现,多继承场景用(C# 不支持多继承,但支持实现多个接口)
记忆口诀
依赖抽象不依赖具体;新增功能加类,不改旧代码。
六、LOD — 迪米特法则
Law Of Demeter(也叫最少知识原则)
核心思想
一个对象应该对其他对象保持最少的了解。只和"直接朋友"说话,不和"陌生人"说话。
直接朋友指:当前对象本身、方法参数、方法返回值、成员变量。
通俗理解
你找工作,只需要联系 HR,不需要直接联系每个部门负责人。HR 帮你协调,你不需要了解公司内部结构。
反例:School 直接操作 Student
// 违反迪米特:School 直接操作 Student(陌生人)
public void ManageOther()
{
foreach (Class c in ClassList)
{
// School 直接拿到 StudentList,直接操作 Student
List<Student> studentList = c.StudentList;
foreach (Student s in studentList)
{
Console.WriteLine(s.StudentName); // School 不应该直接认识 Student
}
}
}
正确做法:通过直接朋友委托
// 遵循迪米特:School 只和 Class 说话
public void Manage()
{
foreach (Class c in ClassList)
{
c.ManageClass(); // School 只调用 Class 的方法,不直接碰 Student
}
}
// Class 负责管理自己的 Student
public void ManageClass()
{
foreach (Student s in StudentList)
{
s.ManageStudent(); // Class 才是 Student 的直接朋友
}
}
访问修饰符与迪米特
访问修饰符本质上也是迪米特法则的体现——控制"谁能知道什么":
private:只有自己知道(隐私)protected:只有子类知道internal:只有同一程序集知道public:所有人都知道
暴露越少,耦合越低。 设计类时,默认从 private 开始,只在真正需要时才提升可见性。
记忆口诀
只和朋友说话,不认识的别搭理;中间人帮你传话,自己别乱串门。
六大原则总结对比
| 原则 | 英文缩写 | 一句话总结 |
|---|---|---|
| 单一职责 | SRP | 一个类只做一件事 |
| 开闭原则 | OCP | 扩展加代码,修改不动旧代码 |
| 里氏替换 | LSP | 子类能无缝替换父类 |
| 接口隔离 | ISP | 接口要小而专,不强迫实现无用方法 |
| 依赖倒置 | DIP | 依赖抽象,不依赖具体实现 |
| 迪米特法则 | LOD | 只和直接朋友交互,降低耦合 |
记忆口诀:SOLID + LOD,其中 SOLID = SRP + OCP + LSP + ISP + DIP
本文来自博客园,作者:龙猫•ᴥ•,转载请注明原文链接:https://www.cnblogs.com/nullcodeworld/p/19757788

浙公网安备 33010602011771号