设计模式学习--面向对象的5条设计原则之Liskov替换原则--LSP

一、LSP简介(LSP--Liskov Substitution Principle):

定义:如果对于类型S的每一个对象o1,都有一个类型T的对象o2,使对于任意用类型T定义的程序P,将o2替换为o1,P的行为保持不变,则称S为T的一个子类型。

子类型必须能够替换它的基类型。LSP又称里氏替换原则。

对于这个原则,通俗一些的理解就是,父类的方法都要在子类中实现或者重写。

二、举例说明:

对于依赖倒置原则,说的是父类不能依赖子类,它们都要依赖抽象类。这种依赖是我们实现代码扩展和运行期内绑定(多态)的基础。因为一旦类的使用者依赖某个具体的类,那么对该依赖的扩展就无从谈起;而依赖某个抽象类,则只要实现了该抽象类的子类,都可以被类的使用者使用,从而实现了系统的扩展。 但是,光有依赖倒置原则,并不一定就使我们的代码真正具有良好的扩展性和运行期内绑定。请看下面的代码:

public class Animal
{
    private string name;
    public Animal(string name)
    {
        this.name = name;
    }
    public void Description()
    {
        Console.WriteLine("This is a(an) " + name);
    }
}
//下面是它的子类猫类:
public class Cat : Animal
{
    public Cat(string name)
    {
 
    }
    public void Mew()
    {
        Console.WriteLine("The cat is saying like 'mew'");
    }
}
//下面是它的子类狗类:
public class Dog : Animal
{
    public Dog(string name)
    {
    }
    public void Bark()
    {
        Console.WriteLine("The dog is saying like 'bark'");
    }
}
//最后,我们来看客户端的调用:
public void DecriptionTheAnimal(Animal animal)
{
    if (typeof(animal) is Cat)
    {
        Cat cat = (Cat)animal;
        Cat.Decription();
        Cat.Mew();
    }
    else if (typeof(animal) is Dog)
    {
        Dog dog = (Dog)animal;
        Dog.Decription();
        Dog.Bark();
    }
}

通过上面的代码,我们可以看到虽然客户端的依赖是对抽象的依赖,但依然这个设计的扩展性不好,运行期绑定没有实现。 是什么原因呢?其实就是因为不满足里氏替换原则,子类如Cat有Mew()方法父类根本没有,Dog类有Bark()方法父类也没有,两个子类都不能替换父类。这样导致了系统的扩展性不好和没有实现运行期内绑定。

现在看来,一个系统或子系统要拥有良好的扩展性和实现运行期内绑定,有两个必要条件:第一是依赖倒置原则;第二是里氏替换原则。这两个原则缺一不可。

我们知道,在我们的大多数的模式中,我们都有一个共同的接口,然后子类和扩展类都去实现该接口。

下面是一段原始代码:

if(action.Equals(“add”))
{
  //do add action
}
else if(action.Equals(“view”))
{
  //do view action
}
else if(action.Equals(“delete”))
{
  //do delete action
}
else if(action.Equals(“modify”))
{
  //do modify action
}

我们首先想到的是把这些动作分离出来,就可能写出如下的代码:

public class AddAction
{
    public void add()
    {
        //do add action
    }
}
public class ViewAction
{
    public void view()
    {
        //do view action
    }
}
public class deleteAction
{
    public void delete()
    {
        //do delete action
    }
}
public class ModifyAction
{
    public void modify()
    {
        //do modify action
    }
}

我们可以看到,这样代码将各个行为独立出来,满足了单一职责原则,但这远远不够,因为它不满足依赖颠倒原则和里氏替换原则。 下面我们来看看命令模式对该问题的解决方法:

public interface Action
{
    public void doAction();
}
//然后是各个实现:
public class AddAction : Action
{
    public void doAction()
    {
        //do add action
    }
}
public class ViewAction : Action
{
    public void doAction()
    {
        //do view action
    }
}
public class deleteAction : Action
{
    public void doAction()
    {
        //do delete action
    }
}
public class ModifyAction : Action
{
    public void doAction()
    {
        //do modify action
    }
}
//这样,客户端的调用大概如下:
public void execute(Action action)
{
    action.doAction();
}

看,上面的客户端代码再也没有出现过typeof这样的语句,扩展性良好,也有了运行期内绑定的优点。

三、LSP优点:

1、保证系统或子系统有良好的扩展性。只有子类能够完全替换父类,才能保证系统或子系统在运行期内识别子类就可以了,因而使得系统或子系统有了良好的扩展性。 2、实现运行期内绑定,即保证了面向对象多态性的顺利进行。这节省了大量的代码重复或冗余。避免了类似instanceof这样的语句,或者getClass()这样的语句,这些语句是面向对象所忌讳的。 3、有利于实现契约式编程。契约式编程有利于系统的分析和设计,指我们在分析和设计的时候,定义好系统的接口,然后再编码的时候实现这些接口即可。在父类里定义好子类需要实现的功能,而子类只要实现这些功能即可。

四、使用LSP注意点:

1、此原则和OCP的作用有点类似,其实这些面向对象的基本原则就2条:1:面向接口编程,而不是面向实现;2:用组合而不主张用继承

2、LSP是保证OCP的重要原则 3、这些基本的原则在实现方法上也有个共同层次,就是使用中间接口层,以此来达到类对象的低偶合,也就是抽象偶合!

4、派生类的退化函数:派生类的某些函数退化(变得没有用处),Base的使用者不知道不能调用f,会导致替换违规。在派生类中存在退化函数并不总是表示违反了LSP,但是当存在这种情况时,应该引起注意。  5、从派生类抛出异常:如果在派生类的方法中添加了其基类不会抛出的异常。如果基类的使用者不期望这些异常,那么把他们添加到派生类的方法中就可以能会导致不可替换性。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏个人随笔

Java 面向对象三大特征之一:封装

面向对象三大特征之一:封装 概念: 将类的某些信息隐藏在类内部,不允许外部程序直接访问,而是通过该类提供的方法类实现对隐藏信息的操作和访问 封装的好处: 隐藏...

3497
来自专栏游戏杂谈

JavaScript立即调用的函数表达式

主要参考知乎上这个问题:javascript 匿名函数有哪几种执行方式 长天之云的回答。

1072
来自专栏猿人谷

Lombok介绍、使用方法和总结

1 Lombok背景介绍 官方介绍如下: Project Lombok makes java a spicier language by adding 'han...

5256
来自专栏风中追风

写一个自己的springMVC

今天我们来实现一个简单的springMVC框架,可以理解为 springMVC1.0这个版本,只是功能比较简单而已;

35314
来自专栏博客园

WebAPI返回JSON

web api写api接口时默认返回的是把你的对象序列化后以XML形式返回,那么怎样才能让其返回为json呢,下面就介绍两种方法:  方法一:(改配置法)  ...

4902
来自专栏.NET后端开发

认识XmlReader

摘要 XmlReader类是组成.NET的关键技术之一,极大地方便了开发人员对Xml的操作。通过本文您将对XmlReader有一个很好的认识,并将其应用到实际开...

34410
来自专栏java架构师

C# 温故而知新:Stream篇(—)

C# 温故而知新:Stream篇(—) 什么是Stream? MSDN 中的解释太简洁了: 提供字节序列的一般视图 (我可不想这么理解,这必定让我抓狂,我理解的...

3508
来自专栏编舟记

Java内部类的异常处理

最近遇到一个问题,使用Java写某个DSL标记语言X的parser(解析器)Maven插件的时候,对外暴露一个名为Callback的接口和一个待实现的方法get...

692
来自专栏battcn

一起学设计模式 - 访问者模式

访问者模式: 预留通路,回调实现。它的实现主要就是通过预先定义好调用的通路,在被访问的对象上定义 accept方法,在访问者的对象上定义 visit方法;然后在...

651
来自专栏上善若水

006-golang中XML文件的处理

data),那么,字符数据会被累加到struct中第一个有tag为”,chardata”的字段。struct字段的类型可以是string或[]byte。如果没有...

1752

扫码关注云+社区