home commit with new plugins
This commit is contained in:
@@ -0,0 +1,257 @@
|
||||
# 里氏替换原则
|
||||
概念:
|
||||
- 里氏替换原则(Liskov Substitution Principle,LSP)说的是:
|
||||
==所有使用父类的地方,都应该能够无障碍地替换成子类,而程序行为仍然正确==
|
||||
- 简单理解:
|
||||
==子类必须真的能当父类来用,而不是“语法上能继承,逻辑上却不成立”==
|
||||
|
||||
通常用法:
|
||||
1. 判断一个继承关系到底合不合理
|
||||
2. 约束子类重写父类成员时,不要破坏原有规则
|
||||
3. 提醒自己:如果继承后总要“特殊处理”,那通常应该改成组合或接口
|
||||
|
||||
核心理解:
|
||||
1. 子类可以扩展父类功能
|
||||
2. 但==不能破坏父类原本承诺的行为==
|
||||
3. 如果父类能做的事,子类却突然“不支持”了,那往往就违反了里氏替换原则
|
||||
|
||||
----
|
||||
# 经典反例
|
||||
错误设计:
|
||||
```csharp
|
||||
class Bird
|
||||
{
|
||||
public virtual void Fly()
|
||||
{
|
||||
Console.WriteLine("鸟会飞");
|
||||
}
|
||||
}
|
||||
|
||||
class Sparrow : Bird
|
||||
{
|
||||
public override void Fly()
|
||||
{
|
||||
Console.WriteLine("麻雀飞走了");
|
||||
}
|
||||
}
|
||||
|
||||
class Penguin : Bird
|
||||
{
|
||||
public override void Fly()
|
||||
{
|
||||
throw new NotSupportedException("企鹅不会飞");
|
||||
}
|
||||
}
|
||||
|
||||
void LetBirdFly(Bird bird)
|
||||
{
|
||||
bird.Fly();
|
||||
}
|
||||
|
||||
LetBirdFly(new Sparrow()); // 正常
|
||||
LetBirdFly(new Penguin()); // 出错
|
||||
```
|
||||
|
||||
问题:
|
||||
- `LetBirdFly(Bird bird)` 这个函数相信:只要传进来的是 `Bird`,就能 `Fly()`
|
||||
- 但 `Penguin` 继承了 `Bird` 后,却把“会飞”这个规则破坏了
|
||||
- 这就说明:==`Penguin` 其实不应该直接继承这种“默认会飞”的 Bird==
|
||||
|
||||
----
|
||||
# 更合理的改法
|
||||
改法一:把“鸟”和“会飞”拆开
|
||||
```csharp
|
||||
abstract class Bird
|
||||
{
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
interface IFlyable
|
||||
{
|
||||
void Fly();
|
||||
}
|
||||
|
||||
class Sparrow : Bird, IFlyable
|
||||
{
|
||||
public void Fly()
|
||||
{
|
||||
Console.WriteLine("麻雀飞走了");
|
||||
}
|
||||
}
|
||||
|
||||
class Penguin : Bird
|
||||
{
|
||||
public void Swim()
|
||||
{
|
||||
Console.WriteLine("企鹅在游泳");
|
||||
}
|
||||
}
|
||||
|
||||
void LetBirdFly(IFlyable bird)
|
||||
{
|
||||
bird.Fly();
|
||||
}
|
||||
|
||||
LetBirdFly(new Sparrow()); // 正常
|
||||
// LetBirdFly(new Penguin()); // 编译期就不允许,更合理
|
||||
```
|
||||
|
||||
这样做的好处:
|
||||
1. `Bird` 只表达“它是一只鸟”
|
||||
2. `IFlyable` 只表达“它会飞”
|
||||
3. 不会飞的鸟就不用为了继承而硬实现一个错误行为
|
||||
|
||||
----
|
||||
# 怎么判断有没有违反
|
||||
可以拿下面几个问题自检:
|
||||
|
||||
1. 子类是不是父类的一种?
|
||||
- 如果读起来都别扭,那继承关系大概率有问题
|
||||
2. 把子类对象传到父类参数的位置,原有代码还能正常工作吗?
|
||||
- 如果经常要加 `if (对象是某某子类)`,设计就开始危险了
|
||||
3. 子类有没有把父类原本支持的行为变成“不支持”?
|
||||
- 例如:父类能 `Fly()`,子类却直接抛异常
|
||||
4. 子类有没有偷偷加强限制?
|
||||
- 父类允许任意正数,子类却只允许 1 到 10
|
||||
5. 子类有没有把父类承诺的结果变弱?
|
||||
- 父类保证成功,子类却经常返回空结果或失败
|
||||
|
||||
----
|
||||
# 违反里氏替换原则的常见信号
|
||||
1. 子类重写后直接抛 `NotSupportedException`
|
||||
2. 子类必须靠很多 `if/else` 特判才能安全使用
|
||||
3. 继承只是为了复用代码,但语义上并不是“is-a”关系
|
||||
4. 子类把父类的规则改掉了,而不是扩展
|
||||
|
||||
补充:
|
||||
- “能继承”不等于“该继承”
|
||||
- 很多违反 LSP 的代码,表面上编译没问题,真正出问题的是业务逻辑
|
||||
|
||||
----
|
||||
# 和继承的关系
|
||||
里氏替换原则不是反对继承,而是告诉你:
|
||||
|
||||
- ==继承必须建立在真正的替换关系上==
|
||||
- 如果子类不能稳定替换父类,就不要硬继承
|
||||
- 这时通常优先考虑:
|
||||
- 组合
|
||||
- 接口
|
||||
- 更细的抽象拆分
|
||||
|
||||
所以可以把它和[[继承]]一起记:
|
||||
- 继承解决“复用和扩展”
|
||||
- 里氏替换原则约束“这个继承是不是合理”
|
||||
|
||||
----
|
||||
# `is` 和 `as` 关键词怎么结合理解
|
||||
先说结论:
|
||||
|
||||
- `is` 和 `as` 是==类型判断 / 类型转换工具==
|
||||
- 它们能帮你在运行时判断“这个对象现在到底是什么类型”
|
||||
- 但它们==不能证明一个继承设计就合理==
|
||||
|
||||
## `is`
|
||||
作用:
|
||||
- 判断对象是否兼容某个类型
|
||||
- 也常用于模式匹配
|
||||
|
||||
例子:
|
||||
```csharp
|
||||
Bird bird = new Sparrow();
|
||||
|
||||
if (bird is Sparrow)
|
||||
{
|
||||
Console.WriteLine("这是麻雀");
|
||||
}
|
||||
|
||||
if (bird is IFlyable flyable)
|
||||
{
|
||||
flyable.Fly();
|
||||
}
|
||||
```
|
||||
|
||||
理解:
|
||||
- `bird is Sparrow` 的意思是:当前这个对象是不是 `Sparrow`
|
||||
- `bird is IFlyable flyable` 不仅判断类型,还顺手把结果保存到变量 `flyable`
|
||||
|
||||
## `as`
|
||||
作用:
|
||||
- 尝试把对象安全转换成某个引用类型或可空值类型
|
||||
- 转换失败不会抛异常,而是返回 `null`
|
||||
|
||||
例子:
|
||||
```csharp
|
||||
Bird bird = new Sparrow();
|
||||
|
||||
IFlyable flyable = bird as IFlyable;
|
||||
if (flyable != null)
|
||||
{
|
||||
flyable.Fly();
|
||||
}
|
||||
```
|
||||
|
||||
补充:
|
||||
- `as` 常用于引用类型
|
||||
- 如果转换失败,结果是 `null`
|
||||
- 对普通非可空值类型不能直接用 `as`
|
||||
|
||||
## 它和里氏替换原则的关系
|
||||
`is` / `as` 本身没有错,但如果你发现代码里总是这样:
|
||||
|
||||
```csharp
|
||||
void Handle(Bird bird)
|
||||
{
|
||||
if (bird is Sparrow)
|
||||
{
|
||||
// 一套逻辑
|
||||
}
|
||||
else if (bird is Penguin)
|
||||
{
|
||||
// 另一套逻辑
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
那通常说明一件事:
|
||||
- 你嘴上写的是“我接收 `Bird`”
|
||||
- 但你心里真正依赖的却是“具体子类是谁”
|
||||
|
||||
这往往意味着:
|
||||
1. 抽象不够稳定
|
||||
2. 多态没有真正用起来
|
||||
3. 继承关系可能不够合理
|
||||
4. 有概率正在逼着代码违反里氏替换原则
|
||||
|
||||
更好的方向一般是:
|
||||
- 让父类 / 接口直接提供调用方真正需要的能力
|
||||
- 或者把能力拆到更合适的接口里
|
||||
|
||||
一句话记:
|
||||
- `is` / `as` 是“识别类型”的工具
|
||||
- 里氏替换原则是“约束继承是否合理”的原则
|
||||
- ==如果父类代码里到处都要靠 `is` / `as` 分辨子类,通常该反过来检查抽象设计==
|
||||
|
||||
----
|
||||
# 一句话记忆
|
||||
==子类可以扩展父类,但不能打破父类原来的承诺==
|
||||
|
||||
再压缩一点:
|
||||
==父类能出现的地方,子类也必须能正常出现==
|
||||
|
||||
----
|
||||
# 面试/复习时怎么说
|
||||
可以这样答:
|
||||
|
||||
“里氏替换原则的意思是,子类对象必须能够替换父类对象,并且不影响程序原本的正确性。
|
||||
如果一个子类继承了父类,却把父类原本支持的行为改成异常、限制更多条件,或者导致调用方需要额外特殊判断,那这个继承关系通常就不合理。
|
||||
这时候应该考虑重新抽象,或者改成接口和组合。”
|
||||
|
||||
----
|
||||
# 注意
|
||||
1. 里氏替换原则是==设计原则==,不是语法规则
|
||||
2. 它常和[[继承]]、[[多态vob|多态]]、[[接口]]一起理解
|
||||
3. `is` / `as` 可以辅助做类型判断,但不能代替抽象设计
|
||||
4. 只要你发现“某个子类总是不太像父类”,就该警惕是不是违反了 LSP
|
||||
|
||||
#核心
|
||||
#设计原则
|
||||
Reference in New Issue
Block a user