《设计模式》杂记之里氏替换原则

前端之家收集整理的这篇文章主要介绍了《设计模式》杂记之里氏替换原则前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。
在这篇博文中,我想把自己学习过的里氏替换原则一些好知识点分享给大家。首先我想把继承的一下优缺点给大家分享一下,然后再引出里氏替换原则吧!
我们都知道在面向对象的语言中,继承是必不可少的,那么它的优点是哪些呢?引用书上一段话吧!
(1) 代码共享,减少创建类的工作量,每个子类都拥有父类方法属性
(2) 提高了代码的重用性;
(3) 子类可以形似父类,但有异于父类
(4) 提高了代码的可扩展性;
(5) 提高产品或项目的开放性。
既然有了优点就应该有缺点:
(1) 继承是侵入性的。只有继承,就必须拥有父类的所有属性方法
(2) 降低代码的灵活性。子类必须拥有父类属性方法,让子类受到了许多的约束;
(3) 增强了耦合性。当父类的常量,变量和方法修改时,必须要考虑子类的修改,更糟糕的结果就是大片的代码需要重构。
C#C++不同,不支持多重继承(即一个类从多个直接基类派生)。那么这里就引出了里氏替换原则。
里氏替换原则通俗的讲就是:只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或者异常,我们根本不需要知道是父类还是子类。
但是这里我们需要注意的是:有子类出现的地方,父类未必就能适应。
前面我们引出了里氏替换原则的概念,通过这些概念我们可以了解到里氏替换原则为良好的继承定义了一个规范。这里就包含了4层含义:
1 ,子类必须完全实现父类方法
《设计模式之禅》这本书上的例子,我个人感觉比较有意思,所以自己就直接把书上的例子分享给大家学习吧!呵呵~~
书上引用的是CS打枪游戏例子,这里把枪的类图列出:

通过这个类图我们可以知道,枪得主要职责是射击,如何设计在各个具体的子类中去定义。在Soldier中定义了一个方法killEnemy,使用枪来杀敌,具体使用什么枪来杀敌人,只有我们在调用时才知道啊!
下面是AbstractGun类的源程序代码
abstract class AbstractGun
{
// 射击
public abstract void shoot();
}
手枪,步枪,机枪的实现类源代码
class Handgun:AbstractGun
{
public override void shoot()
{
Console.WriteLine(" 手枪射击 " );
}
}
class Rifle:AbstractGun
{
public override void shoot()
{
Console.WriteLine(" 步枪射击 " );
}
}
class MachineGun:AbstractGun
{
public override void shoot()
{
Console.WriteLine(" 机枪扫射 " );
}
}
下面我们来定义一个士兵来使用这些枪吧!代码如下:
class Program
{
static void Main(string[] args)
{
// 产生个士兵
Soldier soldier = new Soldier();
// 给士兵一支枪
soldier.setGun(new Handgun());
soldier.killEnemy();
System.Threading.Thread.Sleep(5000);
}
}
在这个程序中,在编写程序时Solider士兵类根本不用知道是哪个型号的枪(子类)被传入。
注意:在类中调用其他类时务必要使用父类或接口,如果不能使用父类或接口,则说明类的设计已经违背了LSP原则。
可能有人会说,我们如果使用仿真枪呢?大家可以想象仿真枪是不能用来射击杀死人的,所以说我们就不能写在shoot方法中。下面是ToyGun的源代码
class ToyGun:AbstractGun
{
// 仿真枪是不能射击的只能虚构一个了。
public override void shoot()
{
// 仿真枪不能杀人,所以这个方法不能实现。
}
}
因为我们引入了新的子类,在main方法中源代码如下:
class Program
{
static void Main(string[] args)
{
// 产生一个士兵
Solder soldier = new Solder();
soldier.setGun(new ToyGun());
soldier.killEnemy();
System.Threading.Thread.Sleep(5000);
}
}
在这种情况下,我们发现业务调用类已经出现问题了,正常的业务逻辑不能运行。作者给我们提供了两种解决办法:
1) Soldier类中增加判断,如果是仿真枪,就不用来杀人。这个方法可以解决问题,但是在程序中,我们每增加一个类,所有与这个父类有关系的类都必须修改,这样就不可行了。所以这个方案被否定了。
2) ToyGun脱离继承,建立一个独立的父类,可以与 AbstractGun 建立关联委托关系。类图如下:

大家都知道C#的三大特性:继承,封装,多态。继承就是告诉我们拥有父类方法属性,然后我们就可以重写父类方法。那么按照继承原则,我们定义的仿真枪继承AbstractGun是没有问题的。但是我们在具体应用场景中就要考虑这个问题:子类是否能够完整地实现父类的业务。
注意:如果子类不能完整地实现父类方法,或者父类的某些方法在子类中已经发生“畸变”,则建议断开父子继承关系,采用依赖,聚合等关系代替继承。
2 ,子类可以有自己的个性
由于里氏替换原则可以正着用,但是不能反过来用。在子类出现的地方,父类未必就可以胜任。这次我们以步枪举例子吧!步枪也分为冲锋枪和狙击枪,如AK47AUG狙击枪等等,Rifle子类图如下:

AUG狙击枪源代码如下:
class AUG:Rifle
{
public void zoomout()
{
Console.WriteLine(" 通过望远镜观看敌人 ?" );
}
public override void shoot()
{
Console.WriteLine("AUG 射击 " ); ;
}
}
狙击手的源代码如下:
class Sniper
{
private AUG aug;
public void setGun(AUG _aug)
{
this.aug = _aug;
}
public void killEnemy()
{
// 首先用望远镜观察
aug.zoomout();
// 开始射击
aug.shoot();
}
}
那么如何让狙击手使用AUG杀死人呢?
static void Main(string[] args)
{
Sniper sniper = new Sniper();
sniper.setGun(new AUG());
sniper.killEnemy();
System.Threading.Thread.Sleep(5000);
}
在这里,系统直接调用了子类。如果我们这个时候使用父类传递进来呢?代码如下:
static void Main(string[] args)
{
Sniper sniper = new Sniper();
sniper.setGun((AUG)(new Rifle()));
sniper.killEnemy();
System.Threading.Thread.Sleep(5000);
}
在运行时抛出InvalidCastException异常,从里氏替换原则来看,有子类出现的地方父类未必就可以出现。

猜你在找的设计模式相关文章