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"关系。如果子类无法完整履行父类的所有契约,就不该继承。

PeopleJapanese 为例:假设 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

posted @ 2026-03-23 14:49  龙猫•ᴥ•  阅读(0)  评论(0)    收藏  举报