细说 里氏替换原则[通俗易懂]

细说 里氏替换原则[通俗易懂]转载:细说LSP(里氏替换原则)|chengco的博客前言曾经在一次给新入职员工做Java开发的课程培训,讲到面向对象的SOLID设计原则时,大家普遍认为”里氏替换原则”是其中最难理解的一个。也许从头说起才能弄清来龙去脉。为什么叫里氏替换原则?里氏替换原则在SOLID这五个设计原则中是比较特殊的存在:如果违反了里氏替换原则,不只是降低软件设计的优雅性,很可能会导致Bug 只有里氏替换原则是以人名命令的里氏替换原则译自Liskovsubstitutionprinciple

大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。

Jetbrains全家桶1年46,售后保障稳定

转载:

细说 LSP(里氏替换原则) | chengco的博客

前言

曾经在一次给新入职员工做Java开发的课程培训,讲到面向对象的SOLID设计原则时,大家普遍认为”里氏替换原则”是其中最难理解的一个。也许从头说起才能弄清来龙去脉。

为什么叫里氏替换原则?

里氏替换原则在SOLID这五个设计原则中是比较特殊的存在:

  • 如果违反了里氏替换原则,不只是降低软件设计的优雅性,很可能会导致Bug
  • 只有里氏替换原则是以人名命令的

里氏替换原则译自Liskov substitution principle。Liskov是一位计算机科学家,也就是Barbara Liskov,麻省理工学院教授,也是美国第一个计算机科学女博士,师从图灵奖得主John McCarthy教授,人工智能概念的提出者。

里氏替换原则最初由Barbara Liskov在1987年的一次学术会议中提出,而真正正式发表是在1994年,Barbara Liskov 和 Jeannette Wing发表的一篇学术论文《A behavioral notion of subtyping》.

什么是里氏替换原则?

里氏替换原则在1994年Barbara Liskov 和 Jeannette Wing发表论文中的描述是:

If S is a declared subtype of T, objects of type S should behave as objects of type T are expected to behave, if they are treated as objects of type T

从字面上翻译:如果S是T的子类型,对于S类型的任意对象,如果将他们看作是T类型的对象,则对象的行为也理应与期望的行为一致。

而另一种关于里氏替换原则的描述为Robert Martin在《敏捷软件开发:原则、模式与实践》一书中对原论文的解读:子类型(subtype)必须能够替换掉他们的基类型(base type)。这个是更简明的一种表述。

如何理解里氏替换原则?

不管是Barbara Liskov论文中的表述,还是Robert Martin的解读,都是比较抽象的表达。要理解里氏替换原则,其实就是要理解两个问题:

  • 什么是替换?
  • 什么是与期望行为一致的替换(Robert Martin所说的“必须能够替换”)?

什么是替换

替换的前提是面向对象语言所支持的多态特性,同一个行为具有多个不同表现形式或形态的能力。以JDK的集合框架为例,List接口的定义为有序集合,List接口有多个派生类,比如大家耳熟能详的ArrayListLinkedList。那当某个方法参数或变量是List接口类型时,既可以是ArrayList的实现, 也可以是LinkedList的实现,这就是替换。

举个简单的例子:

public String getFirst(List<String> values) {
        return values.get(0);
}

Jetbrains全家桶1年46,售后保障稳定

对于getFirst方法,接受一个List接口类型的参数,那既可以传递一个ArrayList类型的参数:

List<String> values = new ArrayList<>();
values.add("a");
values.add("b");
String firstValue = getFirst(values);

又可以接收一个LinkedList参数:

List<String> values = new LinkedList<>();
values.add("a");
values.add("b");
String firstValue = getFirst(values);

什么是与期望行为一致的替换?

在不了解派生类的情况下,仅通过接口或基类的方法,即可清楚的知道方法的行为,而不管哪种派生类的实现,都与接口或基类方法的期望行为一致。或者说接口或基类的方法是一种契约,使用方按照这个契约来使用,派生类也按照这个契约来实现。这就是与期望行为一致的替换。继续以上节中的例子说明:

public String getFirst(List<String> values) {
        return values.get(0);
}

对于getFirst方法,接收List类型的参数,而List类型的get方法返回特定位置的元素,对于本例即为第一个元素。这些是不依赖派生类的知识的。所以对于上节中的示例,不管是ArrayList类型的实现,还是LinkedList的实现,getFirst方法最终的返回值是一样的。这就是与期望行为一致的替换。

违反里氏替换原则的场景

从直观上可能觉得派生类对象可以在替换其基类对象是理所当然的,但会有出现一些场景有意无意地违反了里氏替换原则。

子类中抛出了基类未定义的异常

还以JDK的集合框架为例,如果自定义一个List的派生类,如下:

class CustomList<T> extends ArrayList<T> {
    @Override
    public T get(int index) {
        throw new UnsupportedOperationException();
    }
}

仅重写get方法,throw一个UnsupportedOperationException,因为List接口关于get方法的描述,仅会抛出IndexOutOfBoundsException, throw UnsupportedOperationException的行为并不是基类所期望的,即违反了里氏替换原则,

/**
     * Returns the element at the specified position in this list.
     *
     * @param index index of the element to return
     * @return the element at the specified position in this list
     * @throws IndexOutOfBoundsException if the index is out of range
     *         ({@code index < 0 || index >= size()})
     */
    E get(int index);

子类改变了基类方法的语义或引入了副作用

同样,如果自定义另一个List的派生类,如下:

class CustomList<T> extends ArrayList<T> {
    @Override
    public T get(int index) {
        if (index >= size()){
            return null;
        }
        return get(index);
    }
}

仅重写get方法,当输入index大于当前list的size时,返回null,而不抛出IndexOutOfBoundsException, 因为List接口关于get方法的描述,当index超出范围时抛出IndexOutOfBoundsException,所以改变了基类方法的语义,即违反了里氏替换原则。

违反里氏替换原则的危害

当我们违反了这一原则会带来有一些危害:

  • 反直觉。期望所有子类行为是一致的,但如果不一致可能需要文档记录,或者在代码跑失败后涨此知识;
  • 不可读。如果子类行为不一致,可能需要不同的逻辑分支来适配不同的行为,徒增代码复杂度;
  • 不可用。可能出错的地方终将会出错。

如何避免违反里氏替换原则

谈到如何避免,当然要基于里氏替换原则的定义,与期望行为一致的替换

  • 从行为出发来设计。在做抽象或设计时,不只是要从模型概念出发,还要从行为出发,比如一个经典的例子,正方形和长方形,从现实的概念中正方形是一个长方形,但是在计算其面积的行为上是不一致的。
  • 基于契约设计。这个契约即是基类方法签名、功能描述、参数类型、返回值等。在派生类的实现时,时刻保持派生类与基类的契约不被破坏。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/209982.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

  • Java学习之Mybatis框架基础篇

    0x00前言续上篇文章的入门篇,继续markMybatis内容,上一章节只是写了Mybatis的一个简单查询功能,这篇来写他的删改查等其他操作。0x01Mybatis增加大法添加的操作和查

    2021年12月12日
  • Linux之常用命令

    Linux之常用命令2.常用命令2.1命令格式的说明命令格式:命令\[-选项][参数]参数eg:ls-la/usr说明:大部分命令遵从该格式多个选项时,可以一起写eg:ls–l–als–la简化选项与完整选项(注:并非所有选项都可使用完整选项)eg:ls–allls–a帮助命令:(相当于命令说明书)2.2帮助命令2.2.1man英文:…

  • mysql字符串拼接的方法_sql中拼接字符串的方法

    mysql字符串拼接的方法_sql中拼接字符串的方法总是记不住字符串拼接,每次都要百度去搜索,所以在这里记录一下,好方便后续的查找,如有错误和问题可以提出,谢谢。字符串拼接分为几种方式,在这里会一一举例写出:第一种:mysql自带语法CONCAT(string1,string2,…),此处是直接把string1和string2等等的字符串拼接起来(无缝拼接哦)说明:此方法在拼接的时候如果有一个值为NULL,则返回NULL…

  • C++和Java有哪些区别

    C++和Java有哪些区别1.C++创建对象后需要在使用结束后调用delete方法将其销毁,Java有垃圾回收机制,用来监视new出来的所有对象,辨别不会再被引用的对象,然后释放内存空间2.C++可以重载操作符,Java不能重载3.当变量作为类的成员使用时,Java才确保给定默认值,以确保那些基本类型的成员变量得到初始化,但是C++没有此功能4.C++有多继承,Java只有单继承5.Java中没有sizeof(),在C++中sizeof()操作符能够告诉我们为数据项分配的字节数,因为C++中不同的数据类型在不同的机器上可能有

  • 记录一次C#使用JWT单点登录

    记录一次C#使用JWT单点登录好久没更新了,最近确实比较忙,现在弄完后,第一时间来记录一下最近学到的一些东西JWT单点登录一、简单介绍 JWT全称是JSONWebToken,是一种是目前最流行的跨域身份验证解决方案。为了网络应用环境间传递声明而执行的一种基于JSON的开发标准(RFC7519),该token被设计为紧凑且安全的,特别适用于分布式站点的单点登陆(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,该token也可直接被用于认证,也可被加.

  • GBDT的原理_gbdt怎么计算特征重要性

    GBDT的原理_gbdt怎么计算特征重要性看了许多GBDT构建特征的资料整理而成,具体资料见Reference。背景1GradientBoosting2GradientBoostingDecisionTree3GBDT应用-回归和分类GBDT构建新的特征思想GBDT与LR融合方案源码内容generateGBDTfeaturesgeneratefeaturesforFFMPython

    2022年10月12日

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号