设计模式-六大原则
设计模式-六大原则
设计模式的六大原则
单一职责原则
Single Responsibility Principle
提示
单一职责原则是实现高内聚、低耦合的指导方针,它是最简单但又最难运用的原则,需要设计人员发现类的不同职责并将其分离,而发现类的多重职责需要设计人员具有较强的分析设计能力和相关实践经验—只有熟悉业务,才能做好设计
案例
动物的故事
多分支的Animal该如何设计
都放在一个类---需要各种判断+分支---肯定不稳定,有很多个原因让他变化---违背了单一职责---拆分成多个类(chicker cow fish)---每个类就都稳定了,都单一职责了同一父类多个类型,其实应该拆开写,而不是都塞在一个类---满足单一职责
银行客户端
Client--->
利率要变,得改类,说明职责不单一---封装利率获取的逻辑到别的类账号验证要变,得改类,说明职责不单一---封装到别的类里面—UserCheckService一个类的多个动作,交给别的类去封装,只负责调用---满足单一职责,增强稳定
心得
优缺点
优点:
缺点:
提示
有所得必有所失,设计师做的,就应该是扬长避短,灵活应用
场景
里氏替换原则
Liskov Substitution Principle
知识点同步
案例
People
总结
依赖倒置原则
Dependence Inversion Principle
案例
学生-手机
Student Play 手机
依赖细节(具体手机),新增手机都需要修改Student---不合理依赖抽象(AbstractPhone),新增的手机都可以用,只要实现了这个抽象类1 一个方法应对更多类型
2 支持扩展新增,保持稳定性
层级多了,依赖细节,一个改动会影响一连串---水波效应依赖抽象,类的改动影响就不会扩散
类A直接依赖类B,假如要将类A改为依赖类C,则必须通过修改类A的代码来达成。这种场景下,类A一般是高层模块,负责复杂的业务逻辑;类B和类C是低层模块,负责基本的原子操作;假如修改类A,会给程序带来不必要的风险。
分析
依赖倒置原则基于这样一个事实:相对于细节的多变性,抽象的东西要稳定的多。(插头(可以各种造型各种材质)-插排---车子轮胎-17寸18寸19寸—接口大小)
以抽象为基础搭建起来的架构比以细节为基础搭建起来的架构要稳定的多。
抽象指的是接口或者抽象类,细节就是具体的实现类,使用接口或者抽象类的目的是制定好规范和契约,而不去涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成。
建议
1 低层模块尽量都要有抽象类或接口,或者两者都有
2 变量的声明类型尽量是抽象类或接口
3 使用继承时遵循里氏替换原则(避免埋雷)
依赖倒置原则的核心就是要我们面向抽象/接口编程,理解了面向抽象/接口编程,也就理解了依赖倒置。
各种工厂的面向抽象---80%的设计模式都跟面向抽象有关现代化开发
IOC已内置—无处不在的IOC---就得有抽象
接口抽象类选择问题
抽象类: 可以包含具体实现---约束+实现
接口: 不能包含具体实现---约束
C#8.0变化
1 方法可以声明public 也可以protected,但不能是private
2 字段还是不能声明
3 以前委托和类不能声明,现在都可以声明了
4 以前不能有具体实现,现在也可以有了
这个有没有意义?
有,而且是颠覆性的—以前都说接口不能变,现在可以直接增加实现---这对有的设计模式就会产生冲击
还需要抽象类吗?
还是需要的,抽象类是 is a
而接口还是can do
抽象类:(父类只有一个) 继承+约束: 其实为了继承,约束是副业 is a---抽象类表述的对象
接口: (接口可以多个) 只是做约束 can do,就可以跨不同类型 ----接口表述的是行为,以及规则
不是语法上的问题,而是语义上的问题
经验
C#8.0后,接口允许有默认实现,会改变很多认知
选择还是以:
抽象类表述的对象
接口表述的是行为和规则
更多时候,选择接口就对了,除非有代码想复用(值得商榷)
接口隔离原则
Interface Segregation Principle
案例
手机-相机
手机:打电话 发短信 看电影 上网 玩游戏 拍照 拍视频导航 支付
平板: 看电影 上网 玩游戏 拍照 拍视频
电视: 看电影 上网 玩游戏
相机: 拍照 拍视频
1 接口太大:不需要的功能也得实现--不合理
2 接口太小:接口众多,还会出现一个对象实现多接口,但不能用3 接口合并:
一定在一起的,要合并(上网-玩游戏 拍照-拍视频); 不需要对外暴露,只暴露一个(一个动作 多个步骤导航---定位-计算路线-语音播报)
通过继承来组合接口,定制大接口
总结
和SRP区别
接口隔离和单一职责的区别:
迪米特法则
Law Of Demeter
案例
学校-班级-学员
1 去掉内部依赖:学校不要直接管student,也不关心细节(BCL基类库类型除外)
2 降低访问修饰符:避免暴露太多细节
3 属性变方法:也是为了保护,可以扩充逻辑
分析
目标都是为了低耦合,高内聚。
耦合的方式很多,依赖、关联、组合、聚合等。其中,我们称出现成员变量、方法参数、方法返回值中的类为直接的朋友,而出现在局部变量中的类则不是直接的朋友。也就是说,陌生的类最好不要作为局部变量的形式出现在类的内部
引入
程序对象的直接朋友太多,还可能会引入一个中介,来完成通信,等于是把直接的朋友给变成间接的朋友
比如:一个人和另一个人聊天,发消息 收消息 选择对象 通过中介来发消息 收消息
门面模式
中介者模式
应用范围
1 小到类与类之间:避免跟局部变量交互
2 到类库和类库之间,三层之间:UI-BLL-DAL,UI和DAL不要通信3 大到系统和系统之间:调用用户数据,只能找用户服务,不能找数据库
开闭原则
Open Closed Principle
由来
面向对象语言是一种静态语言,最害怕变化,会波及很多东西,需要全面测试最理想就是新增类,对原有代码没有改动,原有的代码才是可信的
开闭原则只是一个目标,并没有任何的手段,也被称之为总则
其他5个原则的建议,就是为了更好的做到OCP
开闭原则也是面向对象语言开发一个终极目标
案例
学生学习
1 学生学习,需要增加个公开课的分支,是修改方法增加判断,换成新增一个方法--甚至换成新增一个类----从修改变新增
2 学生学习,需要增加个额外的动作,是修改方法,还是新增方法,或者子类override ----从修改变新增
代码升级顺序
提示
满足OCP的程度排序:
1 修改配置—IOC/工厂使用配置文件
2 增加dll—依赖抽象+反射
3 增加class--
4 增加方法—
5 修改方法
遵循OCP,也会带来很多成本
SRP—为了少修改,多增加
LSP---用父类,减少修改
DIP---使用抽象
LOD---减少修改
ISP---小一点,稳定点
六大原则的代码示例
单一职责原则 — 代码重构
// 违反 SRP:一个类承担了多个职责
public class UserProcessor_Bad
{
public void Validate(string email, string password) { /* 验证逻辑 */ }
public void SaveToDatabase(string email, string password) { /* 存库逻辑 */ }
public void SendWelcomeEmail(string email) { /* 发邮件逻辑 */ }
public void LogActivity(string action) { /* 日志逻辑 */ }
}
// 遵守 SRP:拆分为多个单一职责的类
public class UserValidator
{
public bool ValidateEmail(string email) => email.Contains("@");
public bool ValidatePassword(string password) => password.Length >= 8;
}
public class UserRepository
{
public void Save(string email, string password) { /* 存库 */ }
}
public class WelcomeEmailSender
{
public void Send(string email) { /* 发送欢迎邮件 */ }
}
public class ActivityLogger
{
public void Log(string action) { /* 记录日志 */ }
}
// 客户端 — 通过组合使用
public class UserService
{
private readonly UserValidator _validator;
private readonly UserRepository _repository;
private readonly WelcomeEmailSender _emailSender;
private readonly ActivityLogger _logger;
public UserService(UserValidator validator, UserRepository repository,
WelcomeEmailSender emailSender, ActivityLogger logger)
{
_validator = validator; _repository = repository;
_emailSender = emailSender; _logger = logger;
}
public bool Register(string email, string password)
{
if (!_validator.ValidateEmail(email)) return false;
if (!_validator.ValidatePassword(password)) return false;
_repository.Save(email, password);
_emailSender.Send(email);
_logger.Log("用户注册");
return true;
}
}里氏替换原则 — 正确的继承设计
// 违反 LSP:子类修改了父类的行为契约
public class Rectangle_Bad
{
public virtual int Width { get; set; }
public virtual int Height { get; set; }
public int Area => Width * Height;
}
public class Square_Bad : Rectangle_Bad
{
// 正方形要求宽高相等,修改了父类的行为契约
public override int Width { set => base.Width = base.Height = value; }
public override int Height { set => base.Width = base.Height = value; }
}
// 调用方预期 Width 和 Height 可以独立设置
void Resize(Rectangle_Bad r)
{
r.Width = 10;
r.Height = 5;
// 如果传入 Square_Bad,Area = 25(不是预期的 50)— 违反 LSP
}
// 正确做法:提取共同抽象
public abstract class Shape
{
public abstract int Area { get; }
}
public class Rectangle : Shape
{
public int Width { get; }
public int Height { get; }
public Rectangle(int w, int h) { Width = w; Height = h; }
public override int Area => Width * Height;
}
public class Square : Shape
{
public int Side { get; }
public Square(int side) { Side = side; }
public override int Area => Side * Side;
}依赖倒置原则 — 面向抽象编程
// 违反 DIP:高层直接依赖低层实现
public class NotificationService_Bad
{
private readonly SmsSender _sender = new(); // 直接依赖具体类
public void Notify(string message) => _sender.Send(message);
}
public class SmsSender { public void Send(string msg) { /* 发短信 */ } }
// 遵守 DIP:依赖抽象
public interface IMessageSender
{
void Send(string message);
}
public class SmsSender : IMessageSender
{
public void Send(string message) { Console.WriteLine($"[SMS] {message}"); }
}
public class EmailSender : IMessageSender
{
public void Send(string message) { Console.WriteLine($"[Email] {message}"); }
}
public class PushNotificationSender : IMessageSender
{
public void Send(string message) { Console.WriteLine($"[Push] {message}"); }
}
// 高层模块依赖抽象
public class NotificationService
{
private readonly IMessageSender _sender;
public NotificationService(IMessageSender sender) => _sender = sender;
public void Notify(string message) => _sender.Send(message);
}
// DI 容器注册 — 运行时决定使用哪种发送方式
services.AddScoped<IMessageSender, EmailSender>();六大原则之间的关系
开闭原则(OCP)— 总目标
|
+---------+----+----+---------+
| | | |
SRP LSP DIP ISP
单一职责 里氏替换 依赖倒置 接口隔离
| | | |
+---------+----+----+---------+
|
迪米特法则(LOD)原则间的协作关系
- SRP 让类稳定,为 OCP 提供基础
- LSP 保证替换安全,为多态提供保障
- DIP 通过抽象解耦,为 OCP 提供手段
- ISP 让接口精简,减少不必要的依赖
- LOD 减少交互面,降低耦合度
- 五个原则共同服务于 OCP(开闭原则)
常见组合应用场景
场景一:支付模块重构
// 支付方式经常变化,需要同时满足 SRP、OCP、DIP
// DIP + ISP:定义细粒度接口
public interface IPaymentProcessor
{
Task<PaymentResult> ProcessAsync(PaymentRequest request);
}
public interface IRefundProcessor
{
Task<RefundResult> RefundAsync(RefundRequest request);
}
// SRP:每种支付方式独立实现
public class AlipayProcessor : IPaymentProcessor, IRefundProcessor
{
public Task<PaymentResult> ProcessAsync(PaymentRequest request)
{
Console.WriteLine($"[支付宝] 支付: {request.Amount:C}");
return Task.FromResult(new PaymentResult(true, "ALI" + Guid.NewGuid().ToString("N")[..8]));
}
public Task<RefundResult> RefundAsync(RefundRequest request)
{
Console.WriteLine($"[支付宝] 退款: {request.TransactionId}");
return Task.FromResult(new RefundResult(true));
}
}
// OCP:新增支付方式不需要修改已有代码
public class WeChatPayProcessor : IPaymentProcessor, IRefundProcessor
{
public Task<PaymentResult> ProcessAsync(PaymentRequest request)
{
Console.WriteLine($"[微信] 支付: {request.Amount:C}");
return Task.FromResult(new PaymentResult(true, "WX" + Guid.NewGuid().ToString("N")[..8]));
}
public Task<RefundResult> RefundAsync(RefundRequest request)
{
Console.WriteLine($"[微信] 退款: {request.TransactionId}");
return Task.FromResult(new RefundResult(true));
}
}场景二:日志框架设计
// ISP:将日志能力拆分为多个小接口
public interface ILogWriter
{
void Write(LogLevel level, string message, Exception? ex = null);
}
public interface ILogFormatter
{
string Format(LogLevel level, string message, Exception? ex);
}
public interface ILogFilter
{
bool ShouldLog(LogLevel level);
}
// DIP:日志器依赖抽象
public class Logger
{
private readonly ILogWriter _writer;
private readonly ILogFormatter _formatter;
private readonly ILogFilter _filter;
public Logger(ILogWriter writer, ILogFormatter formatter, ILogFilter filter)
{
_writer = writer; _formatter = formatter; _filter = filter;
}
public void Info(string message) => Log(LogLevel.Info, message);
public void Error(string message, Exception? ex = null) => Log(LogLevel.Error, message, ex);
private void Log(LogLevel level, string message, Exception? ex)
{
if (!_filter.ShouldLog(level)) return;
var formatted = _formatter.Format(level, message, ex);
_writer.Write(level, formatted, ex);
}
}性能考量
原则与性能的权衡
// 过度应用 SRP 的性能问题
public class DataValidator
{
public bool Validate(Data data) { return true; } // 每次验证创建新对象
}
public class DataTransformer
{
public Data Transform(Data data) { return data; }
}
public class DataPersister
{
public void Persist(Data data) { /* 数据库操作 */ }
}
// 每次操作都需要经过三层调用 — 调用链过长
// 解决方案:在性能敏感路径上适当合并职责
public class DataProcessor
{
public void Process(Data data)
{
// 合并验证、转换、持久化 — 减少方法调用开销
if (!IsValid(data)) return;
var transformed = Transform(data);
Persist(transformed);
}
private bool IsValid(Data data) => true;
private Data Transform(Data data) => data;
private void Persist(Data data) { /* 数据库操作 */ }
}性能建议
- 热路径(频繁执行的代码)上适当放宽原则要求
- 抽象层不宜过深,一般不超过 3 层
- 使用接口而非抽象类可以避免虚方法调用的开销
- DI 注册时选择合适的生命周期(Scoped vs Transient vs Singleton)
总结
提示
这代表一个系统设计时,对6大原则的一个完成度1234都是正常的
1 第5个有问题,兼而有之,都没照顾好
2 第6个也不好,过犹不及,太夸张了
设计模式六大原则,也只是指导性原则
努力遵循,程序可以更具备扩展性
可以违背,一切为了目标
具体的体会,还需要更多的设计模式来验证
原则在不同规模下的实践
方法级别
// 一个方法做太多事 — 违反方法级 SRP
public void ProcessOrder_Bad(Order order)
{
// 验证
if (order.Items == null || order.Items.Count == 0)
throw new ArgumentException("订单为空");
// 计算价格
decimal total = order.Items.Sum(i => i.Price * i.Quantity);
// 应用折扣
if (total > 500) total *= 0.9m;
// 扣库存
foreach (var item in order.Items) DeductStock(item.ProductId, item.Quantity);
// 创建订单记录
SaveOrder(order, total);
// 发通知
SendNotification(order);
}
// 遵守方法级 SRP — 拆分为小方法
public void ProcessOrder(Order order)
{
ValidateOrder(order);
var total = CalculateTotal(order);
DeductInventory(order);
SaveOrder(order, total);
SendNotification(order);
}
private void ValidateOrder(Order order) { /* 验证逻辑 */ }
private decimal CalculateTotal(Order order) { /* 计算逻辑 */ }
private void DeductInventory(Order order) { /* 库存逻辑 */ }类级别
// 类级别 — 每个类一个职责
public class OrderValidator
{
public ValidationResult Validate(Order order)
{
if (order.Items == null || order.Items.Count == 0)
return ValidationResult.Fail("订单为空");
return ValidationResult.Ok();
}
}
public class PricingService
{
public decimal Calculate(Order order) => order.Items.Sum(i => i.Price * i.Quantity);
public decimal ApplyDiscount(decimal total, decimal discountRate) => total * discountRate;
}
public class InventoryService
{
public void Reserve(Order order) { /* 锁定库存 */ }
public void Release(Order order) { /* 释放库存 */ }
}项目级别
项目职责划分:
├── MyApp.Core — 核心业务逻辑(不依赖任何框架)
├── MyApp.Infrastructure — 数据访问、外部服务调用
├── MyApp.API — 控制器、中间件、请求处理
└── MyApp.Tests — 单元测试、集成测试
每个项目都有明确的职责边界,依赖方向:API -> Core <- Infrastructure面试高频问题
常见面试题
- 开闭原则如何落地? — 通过抽象(接口/抽象类)+ 策略模式 + 工厂模式 + DI 容器实现
- 依赖倒置和依赖注入的关系? — DIP 是原则,DI 是实现 DIP 的技术手段
- 接口隔离和单一职责的区别? — SRP 约束类(一个类一个职责),ISP 约束接口(客户端不应依赖不需要的接口)
- 什么时候可以违反原则? — 简单场景、性能敏感路径、原型验证阶段可以适当放宽
- 六大原则和设计模式的关系? — 原则是指导思想,设计模式是具体实现手段
关键知识点
- 模式不是目标,降低耦合和控制变化才是目标。
- 先找变化点、稳定点和协作边界,再决定是否引入模式。
- 同一个模式在不同规模下的收益和代价差异很大。
项目落地视角
- 优先画出参与对象、依赖方向和调用链,再落到代码。
- 把模式放到一个真实场景里,比如支付、规则引擎、工作流或插件扩展。
- 配合单元测试或契约测试,保证重构后的行为没有漂移。
常见误区
- 为了看起来“高级”而套模式。
- 把简单问题拆成过多抽象层,导致阅读和排障都变难。
- 只会背 UML,不会解释为什么这里需要这个模式。
进阶路线
- 继续关注模式之间的组合用法,而不是孤立记忆。
- 从业务建模、演进策略和团队协作角度看模式的适用性。
- 把模式结论沉淀为项目模板、基类或约束文档。
适用场景
- 当你准备把《设计模式-六大原则》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合在业务规则频繁变化、分支增多或对象协作复杂时引入。
- 当你希望提高扩展性,但又不想把系统拆得过度抽象时,这类主题很有参考价值。
落地建议
- 先识别变化点,再决定是否引入模式,而不是反过来套模板。
- 优先为模式的边界、依赖和调用路径画出简单结构图。
- 把模式落到一个明确场景,例如支付、规则计算、插件扩展或工作流。
排错清单
- 检查抽象层是否过多,导致调用路径和责任不清晰。
- 确认引入模式后是否真的减少了条件分支和重复代码。
- 警惕“为了模式而模式”,尤其是在简单业务里。
复盘问题
- 如果把《设计模式-六大原则》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《设计模式-六大原则》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《设计模式-六大原则》最大的收益和代价分别是什么?
