一、SOLID原则简要概述
SOLID 是 5 条写代码的规矩 ,目的只有一个:让代码好改、好维护、不崩溃、不乱套。
1. S ------ 单一职责原则
英文:Single Responsibility
大白话:一个类/一个函数,只干一件事!
就像:
- 厨师只做饭
- 司机只开车
- 清洁工只打扫
如果一个人又做饭、又开车、又打扫,一旦他请假/生病,整个流程全崩。
代码里的意思:
一个类别又管登录、又管发邮件、又管存数据库。拆分开!
2. O ------ 开闭原则
英文:Open Closed
大白话:对扩展开放,对修改关闭。
简单说:
能加新功能,但别改老代码!
改老代码最容易出 bug。
正确做法:写新的代码去扩展,不要动已经跑稳的旧代码。
例子:
你有一个"支付"类,支持微信支付。
现在要加支付宝 → 不要改原来的代码,新加一个支付宝支付类。
3. L ------ 里氏替换原则
英文:Liskov Substitution
大白话:子类必须能完全代替父类,不能出乱子。
人话翻译:
儿子继承爸爸,儿子必须能当爸爸用,不能掉链子。
例子:
爸爸会"走路"
儿子继承爸爸,结果儿子一走路就摔倒 → 违反 L 原则。
代码里:
子类重写方法后,不能破坏原来的功能。
4. I ------ 接口隔离原则
英文:Interface Segregation
大白话:接口要小而专,别搞大杂烩!
不要让一个类被迫实现它根本用不上的方法。
例子:
你定义一个"动物"接口,里面有:吃、睡、飞、游泳、爬树。
但狗不会飞,也不会爬树 → 狗被迫实现一堆没用的方法 → 烂代码。
正确:拆成小接口
- 会飞的接口
- 会游泳的接口
- 会爬树的接口
谁需要谁实现,不强迫。
5. D ------ 依赖倒置原则
英文:Dependency Inversion
大白话:依赖抽象,不要依赖具体。
最简单理解:
高层不要直接依赖底层细节,要靠"接口/抽象"连接。
例子:
你要喝水:
- 不要直接依赖"某品牌矿泉水"
- 要依赖"水"这个抽象概念
这样换任何水都能用,代码不用改。
代码好处:解耦!好替换、好测试。
总结
- S:一个东西只干一件事
- O:加新功能,不改老代码
- L:子类能完美替代父类
- I:接口小而专,不强迫实现没用的
- D:依赖抽象,不依赖具体细节
那我直接给你一套最接地气、最不装逼 的版本:
先大白话 + 反例(烂代码)+ 正例(好代码),全程用最简单的逻辑讲。
二、SOLID原则正反例
1. S -- 单一职责原则
Single Responsibility
大白话:一个类/函数,只干一件事
反例(烂代码)
一个类干所有事:
- 验证用户
- 存数据库
- 发邮件
- 打日志
改其中一个功能,整个类都要动,一不小心全崩。
正例(好代码)
拆成:
- UserValidator 只负责校验
- UserRepository 只负责存数据库
- EmailService 只负责发邮件
- Logger 只负责打日志
改邮件不影响数据库,改数据库不影响校验。
2. O -- 开闭原则
Open Closed
大白话:能扩展新功能,但别改老代码
反例(烂代码)
支付类里写死:
if(type == WECHAT) ...
else if(type == ALIPAY) ...
加个云闪付,必须改原来的代码 → 风险巨大。
正例(好代码)
定义一个支付接口 Payment,每个支付方式自己实现:
- WechatPay
- Alipay
- UnionPay
新增支付方式,只加新类,不改老代码。
3. L -- 里氏替换原则
Liskov Substitution
大白话:子类必须能完全替代父类,不能出 bug
反例(烂代码)
父类:鸟 → 能飞
子类:鸵鸟 → 继承鸟,但飞不了
你写代码循环调用所有鸟的 fly(),鸵鸟一跑直接报错。
正例(好代码)
重新抽象:
- 鸟:吃、走
- 会飞的鸟:继承鸟 + 飞方法
鸵鸟只继承"鸟",不继承"会飞的鸟",完美替换不出错。
4. I -- 接口隔离原则
Interface Segregation
大白话:接口要小,别强迫别人实现没用的方法
反例(烂代码)
一个超大接口:
interface Animal {
eat(); sleep(); fly(); swim(); climb();
}
狗实现它,必须写 fly()、swim()、climb(),但狗根本不会。
正例(好代码)
拆小接口:
- Eatable
- Sleepable
- Flyable
- Swimmable
狗只实现 Eatable + Sleepable,不用实现别的。
5. D -- 依赖倒置原则
Dependency Inversion
大白话:依赖抽象,不依赖具体实现
反例(烂代码)
业务类直接 new MySQLRepository()
换数据库必须改业务代码。
正例(好代码)
依赖 IRepository 接口
MySQLRepository、OracleRepository 都实现它
业务类只跟接口打交道,换数据库不用动业务。
三、接口隔离原则------案例实战分析(一)
接口隔离原则:服务调用者角度看待接口
单一职责原则:服务提供者角度看待接口
你的女朋友开车追尾了,你二话不说,给你对象买了辆坦克,这样以后就不会被追尾了
定义一个IVehicle接口,有Run方法,由Car和Truck类实现
定义一个ITank接口,有Fire和Run方法,由LightTank、MediumTank和HeavyTank实现
csharp
using System;
namespace ConsoleApp1
{
internal class Program
{
static void Main(string[] args)
{
Driver driver = new Driver(new Car()); // 开车
driver.Drive();
// Driver driver = new Driver(new HeavyTank()); // 开重型坦克
// driver.Drive(); //坦克的走
}
}
interface IVehicle
{
void Run();
}
public class Car : IVehicle
{
public void Run()
{
Console.WriteLine("Car is running");
}
}
public class Truck : IVehicle
{
public void Run()
{
Console.WriteLine("Truck is running");
}
}
interface ITank
{
void Run();
void Fire();
}
public class LightTank : ITank
{
public void Fire()
{
Console.WriteLine("LightTank fire: Boom!");
}
public void Run()
{
Console.WriteLine("LightTank is running");
}
}
public class MediumTank : ITank
{
public void Fire()
{
Console.WriteLine("MediumTank fire: Bang!");
}
public void Run()
{
Console.WriteLine("MediumTank is running");
}
}
public class HeavyTank : ITank
{
public void Fire()
{
Console.WriteLine("HeavyTank fire: Pow!");
}
public void Run()
{
Console.WriteLine("HeavyTank is running");
}
}
class Driver
{
private IVehicle vehicle_; // 开车
public Driver(IVehicle vehicle)
{
this.vehicle_ = vehicle;
}
public void Drive()
{
this.vehicle_.Run();
}
/*
// 开坦克
private ITank tank_; // 开坦克
public Driver(ITank tank)
{
this.tank_ = tank;
}
public void Drive() // 女朋友只用到了开车,调用的只是坦克的Run方法,但Fire这个功能始终没有,所以就成了胖接口
{
this.tank_.Run();
}
*/
}
}
问题来了:ITank本身有Fire和Run方法,但女朋友也就用Run就行,Fire根本用不到,也不可能用到,故这个ITank是个胖接口,违反了接口隔离原则,这个胖接口由两个本质不同的东西合并而成(Run和Fire),我们应该把这个胖接口分裂成两个接口
解决方法:
Fire和Run隶属于两个不同的事物,Fire隶属于武器(Weapon),Run隶属于机动车辆(Vehicle)
csharp
using System;
namespace ConsoleApp1
{
internal class Program
{
static void Main(string[] args)
{
//Driver driver = new Driver(new Car()); // 开车
//Driver driver = new Driver(new Truck()); // 开卡车
//Driver driver = new Driver(new HeavyTank()); // 开重型坦克
Driver driver = new Driver(new LightTank()); // 开轻型坦克
driver.Drive();
}
}
interface IVehicle
{
void Run();
}
interface IWeapon
{
void Fire();
}
interface ITank : IVehicle, IWeapon { }
public class Car : IVehicle
{
public void Run()
{
Console.WriteLine("Car is running");
}
}
public class Truck : IVehicle
{
public void Run()
{
Console.WriteLine("Truck is running");
}
}
public class LightTank : ITank
{
public void Fire()
{
Console.WriteLine("LightTank fire: Boom!");
}
public void Run()
{
Console.WriteLine("LightTank is running");
}
}
public class MediumTank : ITank
{
public void Fire()
{
Console.WriteLine("MediumTank fire: Bang!");
}
public void Run()
{
Console.WriteLine("MediumTank is running");
}
}
public class HeavyTank : ITank
{
public void Fire()
{
Console.WriteLine("HeavyTank fire: Pow!");
}
public void Run()
{
Console.WriteLine("HeavyTank is running");
}
}
class Driver
{
private IVehicle vehicle_; // 开车,车只有Run,坦克也只有Run
public Driver(IVehicle vehicle)
{
this.vehicle_ = vehicle;
}
public void Drive()
{
this.vehicle_.Run();
}
}
}
四、接口隔离原则------案例实战分析(二)
接口的显示实现(C#独有)
C#不仅可以做到接口隔离,甚至可以把一些隔离出来的接口给隐藏掉,直到用户显示的使用这种接口类型的变量去引用一个实现了这个接口的具体类的实例的时候,这个接口里面的方法才可以被看见被使用
爱憎分明的杀手:一面修养的gentleman、一面是冷酷无情的杀手killer
- IGentleman接口,有个返回值为void的Love接口
- IKiller接口,有个返回值为void的Kill接口
- 普通和显示接口实现
- 普通的接口隔离:WarmKiller类去实现IGentleman和IKiller接口
- 主函数
WarmKiller wk = new WarmKiller();wk对象可以看到Kill方法也可以看到Love方法 - 此时设计的逻辑就不合理了:一个杀手走在路上,不应该让别人轻易看出来自己是个杀手
编程世界:一个接口,我们不是想那么轻易的想让人拿到的话,那么就不应该轻易的被别人看到,这时候就要用到接口的显式实现了
- 主函数
- 显式的实现:void IKiller.Kill(){xxxx;}
- 只有我们拿IKiller类型的变量来引用WarmKiller类类型的实例的时候,这个Kill方法才可以被调用
- 主函数
IKiller killer = new WarmKiller(); killer.Kill();//这才可以看到Kill方法而之前的WarmKiller wk = new WarmKiller();wk对象只能看到Love方法,看不到Kill方法 - 若需要Love方法的时候,把killer强转成WarmKiller一下就行,
WarnKiller wk = killer as WarmKiller; wk.Love();
- 普通的接口隔离:WarmKiller类去实现IGentleman和IKiller接口
普通的接口隔离
csharp
using System;
using System.Collections;
namespace ConsoleApp1
{
internal class Program
{
static void Main(string[] args)
{
WarmKiller wk = new WarmKiller();
wk.Kill();
wk.Love();
}
}
interface IGentleman
{
void Love();
}
interface IKiller
{
void Kill();
}
class WarmKiller : IGentleman, IKiller
{
public void Kill()
{
Console.WriteLine("WarmKiller will Kill you!");
}
public void Love()
{
Console.WriteLine("WarmKiller will Love you!");
}
}
}
显示接口实现
csharp
using System;
using System.Collections;
namespace ConsoleApp1
{
internal class Program
{
static void Main(string[] args)
{
WarmKiller wk = new WarmKiller();
wk.Love();
// wk.Kill(); // 看不到Kill方法
IKiller killer = new WarmKiller();
killer.Kill(); // 显示接口调用Kill方法
WarmKiller killer2wk = killer as WarmKiller;
killer2wk.Love(); // 显示接口调用Love方法
}
}
interface IGentleman
{
void Love();
}
interface IKiller
{
void Kill();
}
class WarmKiller : IGentleman, IKiller
{
void IKiller.Kill()
{
Console.WriteLine("WarmKiller will Kill you!");
}
public void Love()
{
Console.WriteLine("WarmKiller will Love you!");
}
}
}
五、反射(reflection)
反射不是C#的功能,而是.Net框架的功能
只要用到.Net框架的地方,用什么编程语言都可以用反射
反射大白话:你给我一个对象,反射可以在不用new操作符的情况下,也不知道用户给的对象具体是什么静态类型的情况下,反射可以创建出来一个同类型的对象
反射相当于进一步的解耦,有new操作符的地方后面一定会跟类型,一跟类型就有依赖了
使用反射,创建对象可以不用new操作符,可以不出现静态类型,很多时候这个耦合可以弱到忽略不计
反射有什么用?
很多时候,程序的逻辑不是能够在写程序的时候就可以确定的,有时候这个逻辑是要到用户跟程序进行交互的时候才可以确定,此时程序处于运行时,而程序在写的时候和编译的时候都属于静态状态,所以等用户和正在运行的程序进行交互时,程序已经离开开发和编译环境了。
还是用静态的方法去预测用户可以的动态操作就得写成千上万个if等语句,程序就很臃肿,此时需要程序具备一种以不变应万变的能力,这个功能就是反射
现在的.Net平台有两个版本,一个是Windows上的.Net FrameWork,另一个是跨平台的.Net Core(未来力推)
俩平台版本都有反射机制,但是两个版本类库在调用的时候有些许差异
反射是动态的在内存里面拿到对象的描述、拿到对象与它绑定的类型的描述,再用这些描述去创建新的对象,这些过程总归会对程序的性能有一定的影响,故在程序当中,不要盲目的或过多的去使用反射机制,以免影响程序的运行性能
反射的基本原理
csharp
using System;
using System.Reflection;
namespace ConsoleApp1
{
internal class Program
{
static void Main(string[] args)
{
ITank tank = new HeavyTank(); // 由ITank对象引用一个HeavyTank类型的实例,静态类型
// ------------------------------------
var t = tank.GetType(); // 拿到这个对象在运行时与它关联的动态类型的一些描述信息
object o = Activator.CreateInstance(t);
MethodInfo fireMi = t.GetMethod("Fire");
MethodInfo runMi = t.GetMethod("Run");
fireMi.Invoke(o, null); // 参数一:在o这个对象上调用;参数二:要不要把参数传到方法里面,没有参数就传个null
runMi.Invoke(o, null);
}
}
interface IVehicle
{
void Run();
}
public class Car : IVehicle
{
public void Run()
{
Console.WriteLine("Car is running");
}
}
public class Truck : IVehicle
{
public void Run()
{
Console.WriteLine("Truck is running");
}
}
interface ITank
{
void Run();
void Fire();
}
public class LightTank : ITank
{
public void Fire()
{
Console.WriteLine("LightTank fire: Boom!");
}
public void Run()
{
Console.WriteLine("LightTank is running");
}
}
public class MediumTank : ITank
{
public void Fire()
{
Console.WriteLine("MediumTank fire: Bang!");
}
public void Run()
{
Console.WriteLine("MediumTank is running");
}
}
public class HeavyTank : ITank
{
public void Fire()
{
Console.WriteLine("HeavyTank fire: Pow!");
}
public void Run()
{
Console.WriteLine("HeavyTank is running");
}
}
class Driver
{
private IVehicle vehicle_; // 开车
public Driver(IVehicle vehicle)
{
this.vehicle_ = vehicle;
}
public void Drive()
{
this.vehicle_.Run();
}
/*
// 开坦克
private ITank tank_; // 开坦克
public Driver(ITank tank)
{
this.tank_ = tank;
}
public void Drive() // 女朋友只用到了开车,调用的只是坦克的Run方法,但Fire这个功能始终没有,所以就成了胖接口
{
this.tank_.Run();
}
*/
}
}
封装的反射
封装好了的反射最重要的一个功能是依赖注入(dependency injection,简称DI)
前面有介绍到依赖反转原则(dependency inversion principle,简称DI,实际全称DIP)
没有依赖反转原则就不可能有依赖注入,依赖反转是个概念,依赖注入是在这个概念的基础之上结合接口、结合反射机制的一个应用
依赖注入------实战案例
依赖注入需要借助依赖注入框架
.Net的依赖注入框架是Microsoft.Extensions.DependencyInjection





csharp
using System;
using System.Reflection;
using Microsoft.Extensions.DependencyInjection; // 引入依赖注入的命名空间
namespace ConsoleApp1
{
internal class Program
{
static void Main(string[] args)
{
// 依赖注入最重要的是container容器
// 把各种各样的类型和对应的接口都放到容器里面,容器叫做service provider
// 后续需要实例的话,只需要从容器里面取就可以了
// 一次性的注册,可以在程序启动的时候注册
ServiceCollection sc = new ServiceCollection(); // 接口的实现者就是服务的提供者,ServiceCollection就是容器
sc.AddScoped(typeof(ITank), typeof(HeavyTank)); // 参数一:接口的类型是什么? 参数二:是哪个类实现了这个接口
// 若ITank接口对应的实现类不再是HeavyTank,而是MediumTank 只需要修改一行即可:sc.AddScoped(typeof(ITank), typeof(MediumTank));
sc.AddScoped(typeof(IVehicle), typeof(Car)); // 先跟IVehicle接口绑定的是Car
// sc.AddScoped(typeof(IVehicle), typeof(LightTank)); // 只需要修改这一行即可
sc.AddScoped<Driver>(); // 再把Driver塞进去
// 注入:用我们注册的类型创建的实例给注入到Driver构造函数中
ServiceProvider sp = sc.BuildServiceProvider();
// -----------------------------------------
// 在程序的各个地方,只要能看到ServiceProvider的地方,都可以用
// 从container中调用对象
ITank tank = sp.GetService<ITank>();
tank.Fire();
tank.Run();
// 好处:在整个程序的开发过程中,在程序的千千万万个地方都用到了ITank所引用的实例,突然有一天程序升级了,ITank接口对应的实现类不再是HeavyTank,而是MediumTank
// 如果在程序当中所有地方都是用的ITank itank = new ITank();那么程序中有多少个new操作符,就得改多少次,把HeavyTank改成MediumTank
// 但并不保证所有的new HeavyTank都得变成new MediumTank,因为有可能某个代码是给其他逻辑准备的
// 有了依赖注入之后,只需要修改 sc.AddScoped(typeof(ITank), typeof(HeavyTank)); 改成sc.AddScoped(typeof(ITank), typeof(MediumTank));即可
Driver driver = sp.GetService<Driver>(); // 向容器要一个Driver实例
// 之前创建Driver实例时,需要先创建一个驾驶的什么对象,然后把这个对象传给Driver的构造函数,
// 现在的话,因为Driver和IVehicle都已经注册在container容器中,当container创建Driver实例的时候,就会自己去找
// 要一个IVehicle接口类型的实例,自动把IVehicle注册的实例给创建出来,然后塞给构造函数
// 就可以直接Driver driver = sp.GetService<Driver>();搞定
driver.Drive();
}
}
interface IVehicle
{
void Run();
}
public class Car : IVehicle
{
public void Run()
{
Console.WriteLine("Car is running");
}
}
public class Truck : IVehicle
{
public void Run()
{
Console.WriteLine("Truck is running");
}
}
interface ITank
{
void Run();
void Fire();
}
public class LightTank : ITank
{
public void Fire()
{
Console.WriteLine("LightTank fire: Boom!");
}
public void Run()
{
Console.WriteLine("LightTank is running");
}
}
public class MediumTank : ITank
{
public void Fire()
{
Console.WriteLine("MediumTank fire: Bang!");
}
public void Run()
{
Console.WriteLine("MediumTank is running");
}
}
public class HeavyTank : ITank
{
public void Fire()
{
Console.WriteLine("HeavyTank fire: Pow!");
}
public void Run()
{
Console.WriteLine("HeavyTank is running");
}
}
class Driver
{
private IVehicle vehicle_; // 开车
public Driver(IVehicle vehicle)
{
this.vehicle_ = vehicle;
}
public void Drive()
{
this.vehicle_.Run();
}
/*
// 开坦克
private ITank tank_; // 开坦克
public Driver(ITank tank)
{
this.tank_ = tank;
}
public void Drive() // 女朋友只用到了开车,调用的只是坦克的Run方法,但Fire这个功能始终没有,所以就成了胖接口
{
this.tank_.Run();
}
*/
}
}