文档章节

浅谈“李氏代换”——从纪念金庸和斯坦李说起

SamYjy
 SamYjy
发布于 11/17 08:31
字数 2462
阅读 72
收藏 3

李氏代换(LSP)简介

李氏代换是软件设计的一个原则,又名依赖倒转原则或依赖倒置原则,其衍生原则有接口分离原则等。该原则由Barbara Liskov于1988年提出。

该原则指出,程序中高级别的元素应当独立于继承于它的低级别的元素独立存在,而程序中低级别的元素的设计和运行应当依赖于(继承)高级别元素的存在。换句话说,即高级的类、模块或接口应当不知道低级的类、模块或接口的存在,而低级的类、模块或接口的设计、实现以及运行应当继承高级的类、模块或接口,从而实现了“强内聚、低耦合”的软件设计要求。由此原则,可以衍生出以下一些规则(推论):

  1. 程序中低级别的元素应当能够完全用以替代高级别的元素。
  2. 设计接口时应当不考虑实体的具体行为,使得实体的具体行为实现可以建立在实现接口的基础上进行。
  3. 实体类的具体行为不应当依赖于与这些行为无关的接口。

规则2和3即为接口分离原则。

举个栗子

前不久金庸大侠和斯坦李大英雄都不幸逝世了,这两位大师的笔下叙述了很多的英雄形象(Hero)的故事,这个例子就与如何对小说影视等作品中出现的英雄人物编程有关。这些英雄人物各有不同的故事(IStory)、不同的超能力(IAbility),有的还有会飞(IFly)这种行为。可以设定为有超能力的英雄一定有故事,即用IAbility继承IStory。会飞的英雄一定有故事、并且有超能力,所以可以继承上述两个接口。

这些英雄形象一般在中国叫作武侠(Wuxia),而在美国叫做超人(SuperHero),因此可以将Wuxia和SuperHero根据李氏代换原则设置为Hero的子类,这些类应当实现IAbility接口。有的武侠会飞(FlyingWuxia),因此会飞的武侠可以继承武侠并扩展“会飞”的特性,而FlyingWuxia的实体在运行中也可以用于完全替代武侠类。类似地可以创建FlyingSuperHero类。这些会飞的武侠或者超人应当去实现IFly这一接口。

接口的实现

在程序设计中,接口名一般以大写字母“I”开头,其Java实现如下:

public interface IStory{
    public void showStory(String whatHappened);
}

public interface IAbility extends IStory {
    public void showAbility(String whatAbility);
}

public interface IFly extends IAbility, IStory{
    public void take_off(String tool);

    public void fly();

    public void landing();
}

Java中接口可以多继承,但是类不可以。Python是个更强调具体实现的语言,由于(原则上)所有的类都是可实例化、可多继承的对象,因此没有了接口这一说。但是我们仍然可以使用abc模块的ABCMeta、abstractmethod这两个子模块进行接口与抽象类(设计上)的实现。标注abstractmethod的目的仅仅是为了提醒后续的类进行实现(当然与Java不同,python中对于这些接口或抽象类的“抽象方法”不作实现也不会影响运行)

from abc import ABCMeta, abstractmethod

class IStory:
    __metaclass__ = ABCMeta

    @abstractmethod
    def showStory(person, whatHappened):
        print(person, " experienced ", whatHappened)

class IAbility(IStory):
    __metaclass__ = ABCMeta

    @abstractmethod
    def showAbility(person, ability):
        print(person, "has ability", ability)

class IFly(IAbility, IStory):
    __metaclass__ = ABCMeta

    @abstractmethod
    def take_off(thing):
        print(thing, "takes off!")

    @abstractmethod
    def fly(thing):
        print(thing, "flying!")

    @abstractmethod
    def landing(thing):
        print(thing, "landing!")

抽象类的实现

关于Hero这个类的实现,由于每个英雄人物都有特定的姓名、性别等特性,但每个英雄都有不同的故事,因此我们可以考虑设置英雄为一个抽象类并包含抽象方法“它的故事”。

“英雄”的Java实现:

public abstract class Hero implements IAbility {
    private String name;
    private char gender;
    private String nationality;
    private int age;

    public Hero(String name, char gender, String nationality) {
        this(name, gender, nationality, 20);
    }

    public Hero(String name, char gender, String nationality, int age) {
        this.name = name;
        this.gender = gender;
        this.nationality = nationality;
        this.age = age;
    }

    @Override
    public void showAbility(String whatAbility) {
        System.out.println(this + " has ability " + whatAbility);
    }

    @Override
    public String toString(){
        return name + " " + gender + " " + age + " " + nationality;
    }

    @Override
    public abstract void showStory(String whatHappened);

}

“英雄”的Python实现:

class Hero(IAbility):
    def __init__(self, name, gender, nationality, age=20):
        self.__name = name
        self.__gender = gender
        self.__nationality = nationality
        self.__age = age

    def __str__(self):
        return (self.__name + " " +  self.__gender + " " +  str(self.__age) + 
                " comes from " + self.__nationality)

    @abstractmethod
    def showStory(self, whatHappened):
        print(self, " experienced ", whatHappened)

其他实现

其他的“武侠”、“超人”等实现,继承“英雄”类并实现“超能力”接口即可,会飞的英雄人物需要实现“飞行”接口。这都比较简单,此处就直接上代码出招,不做赘述。

“武侠”类的Java实现:

public class Wuxia extends Hero{

    public Wuxia(String name, char gender) {
        super(name, gender, "中国", 20);
    }

    public Wuxia(String name, char gender, int age) {
        super(name, gender, "中国", age);
    }

    @Override
    public String toString(){
        return super.toString();
    }

    @Override
    public void showStory(String whatHappened) {
        System.out.println(this + " experienced " + whatHappened);
    }
}

“武侠”类的Python实现:

class Wuxia(Hero):
    def __init__(self, name, gender, age=20):
        Hero.__init__(self, name, gender, "中国", age)

“飞行武侠”类的Java和Python实现:

public class FlyingWuxia extends Wuxia implements IFly{

    public FlyingWuxia(String name, char gender) {
        super(name, gender);
    }

    public FlyingWuxia(String name, char gender, int age) {
        super(name, gender, age);
    }

    @Override
    public String toString(){
        return super.toString();
    }

    @Override
    public void take_off(String tool) {
        System.out.println(this + " uses " + tool + " to take off!");
    }

    @Override
    public void fly() {
        System.out.println(this + " is flying!");
    }

    @Override
    public void landing() {
        System.out.println(this + " is landing!");
    }

    @Override
    public void showStory(String whatHappened) {
        System.out.println(this + " experienced " + whatHappened);
    }

}
class FlyingWuxia(Wuxia, IFly):
    def __init__(self, name, gender, age=20):
        Wuxia.__init__(self, name, gender, age)

    def take_off(self, tool):
        print(self, "uses ability", tool, "to take off!")

“飞行超人”的Java和Python实现:

public class FlyingSuperHero extends Hero implements IFly{

    public FlyingSuperHero(String name, char gender) {
        super(name, gender, "U.S.A");
    }

    public FlyingSuperHero(String name, char gender,int age) {
        super(name, gender, "U.S.A", age);
    }

    @Override
    public String toString(){
        return super.toString();
    }

    @Override
    public void take_off(String tool) {
        System.out.println(this + " uses " + tool + " to take off!");
    }

    @Override
    public void fly() {
        System.out.println(this + " is flying!");
    }

    @Override
    public void landing() {
        System.out.println(this + " is landing!");
    }

    @Override
    public void showStory(String whatHappened) {
        System.out.println(this + " experienced " + whatHappened);
    }

}
class FlyingSuperHero(Hero, IFly):
    def __init__(self, name, gender, age=20):
        Hero.__init__(self, name, gender, "U.S.A", age)

    def take_off(self, tool):
        print(self, "uses tool", tool, "to take off!")

几点思考

Java和Python都是面向对象的编程语言,关于二者在面向对象方面的优劣,江湖上各派也各有各的观点。比如,Python虽然实现简单,但是它的封装特性有很大的问题,经常成为各路黑客们行走江湖、“行侠仗义”或“替天行道”的工具;而Java虽然能够做到类型安全,也体现了很多的设计原则,但毕竟实现过程颇费周折。在“李氏代换”这个原则下,对于这两个编程语言可以分别有如下思考:

  1. 这两种OOP语言都能体现“低级别的元素应当能够完全用以替代高级别的元素”这一李氏代换衍生规则一。需要注意的区别是,Java的“向下转化(down-casting)”更是获得了低级别类的行为在需要时能够代替高级别类的行为的效果,但也因为开放了对高级别类行为的修改而违反了“开放-封闭”原则中“对修改封闭”的原则。Python则因为根本没有down-casting这一出而没有在此处违反“开放-封闭”原则。
//Java main函数中调用英雄示例:
public static void main(String[] args) {
        Wuxia duanyu = new Wuxia("段誉", 'M', 19);
        duanyu.showAbility("六脉神剑");
        demoStory(duanyu, "赶走慕容复");
        System.out.println("");

        //Python中这下一行是不可以的
        Wuxia changqing = new FlyingWuxia("徐长卿", 'M', 26);
        changqing.showAbility("蜀山剑法");
        demoTakeOff((FlyingWuxia)changqing, "御剑");
        demoStory(changqing, "当了蜀山长老");
        System.out.println("");

        Hero captain = new FlyingSuperHero("Steven", 'M', 98);
        captain.showAbility("Flying");

		//在Python中这也是不可以的
        demoTakeOff((FlyingSuperHero)captain, "aegis");
        System.out.println("");
    }

    public static void demoTakeOff(IFly fly, String tool){
        fly.take_off(tool);
    }

    public static void demoStory(IStory story, String whatHappened){
        story.showStory(whatHappened);
    }

# Python调用英雄人物示例:

# If you did this:
# changqing = Wuxia("徐长卿", 'M', 26)
# you cannot do:
# FlyingWuxia(changqing).take_off("御剑") in Python
changqing = FlyingWuxia("徐长卿", 'M', 26)
IAbility.showAbility(changqing, "蜀山剑法")
IStory.showStory(changqing, "当了蜀山长老")
changqing.take_off("御剑")
print()
captain = FlyingSuperHero("Steven", 'M', 98)
captain.take_off("aegis")
print()
boeing757 = Airplane("Boeing 757")
boeing757.take_off()
boeing757.landing()

以下分别是java netbeans和python spyder下的运行效果:

  1. 两种编程语言都能够体现李氏代换推论2,但是Java由于强制要求类对于所实现的接口中所有方法必须进行实现,这难免违反了推论3的规则。因此很容易就导致代码冗长。从上述实现中,读者可以发现Python的代码远远要短于Java的代码。再具体一点,如果只需要关注作品中的部分情节,比如详细描写“起飞”而不需要过度关注怎么平飞和降落的情形时(比如,“徐长卿利于剑上,运用内功缓缓升起,顷刻便到了长安城。”的描写中就忽略了平飞和降落的细节),这时明显Python给出的实现方式更佳。
  2. 通过思考的结论2,很明显,如果在日常的编码实现中能够充分应用、活用“李氏代换”的原则,我们就能够大幅度减少代码重复性,从而实现码农早下班、不脱发的美好理想。

参考资料

  1. 程杰, 大话设计模式. 北京: 清华大学出版社, 2015.
  2. Y. D. Liang, Introduction to Java programming, Tenth edition. Boston: Pearson, 2015.
  3. M. Lutz, Learning Python, Fifth edition. Beijing: O’Reilly, 2013.
  4. C. Giridhar, “Learning Python Design Patterns - Second Edition,” p. 311.

特别鸣谢

金庸大侠和斯坦李大英雄带给我们的青春记忆,以及程杰老师开创的故事体叙述软件设计模式的先河。

© 著作权归作者所有

共有 人打赏支持
SamYjy

SamYjy

粉丝 60
博文 55
码字总数 101820
作品 0
其它
程序员
私信 提问
“漫威之父”斯坦·李去世,西方江湖再无英雄

美国漫画界元老、漫威系列作者斯坦·李(Stan Lee)在美国病逝,享年95岁。 想不到,灭霸一个响指,也带走了他!从今天开始,漫威的英雄电影里再也没有漫威之父“斯坦·李”的背影了。 这个在...

程序员之家_
11/13
0
0
day147-2018-11-14-英语流利阅读-待学习

《毒液》刚刚上映,创造漫威宇宙的人却走了 雪梨 2018-11-14 1.今日导读 中国的金庸创造了侠义英雄和江湖传奇,而大洋彼岸的斯坦·李也同样创造了一个绚烂璀璨的漫威宇宙,他构思的每个超级英...

飞鱼说编程
11/14
0
1
上一个时代落幕,我们还在......

币梗日常 BTC和ETH都被隔壁房间的动静吵醒了,两人站在门口大眼对小眼,此时BCH的房间只听见床铺摇晃的声音,声音还越来越大。其中还夹杂着BCH的喘息声。 BTC和ETH实在忍不住了,BTC第一个踹...

Cindy_e9ed
11/14
0
0
漫威之父斯坦·李辞世,幸好我们还拥有他的科技梦想

  大数据文摘编辑组出品   当地时间11月12日,斯坦·李去世在洛杉矶一家医院去世,享年95岁。   这位打造了无数超级英雄的漫威之父,成就了无数小朋友和成年人的梦想。而这些广为流传的...

大数据文摘
11/13
0
0
论艺术的古典精神 一一 纪念艺术大师梅兰芳

我总是觉得 , 一个民族拥有自己的伟大的艺术家是这个民族的福份。一切的 民族都要生存 , 必须解决衣食住行的物质问题 , 但有什么样的精神生活 , 拥有什么样的艺术形式 ,有什么样的艺术家 , ...

vucndnrzk8iwx
2017/12/15
0
0

没有更多内容

加载失败,请刷新页面

加载更多

开源 serverless 产品原理剖析(二) - Fission

背景 本文是开源 serverless 产品原理剖析系列文章的第二篇,关于 serverless 背景知识的介绍可参考文章开源 serverless 产品原理剖析(一) - Kubeless,这里不再赘述。 Fission 简介 Fiss...

阿里云官方博客
8分钟前
0
0
Android面试整理(附答案)

面试,无非都是问上面这些问题(挺多的 - -!),聘请中高级的安卓开发会往深的去问,并且会问一延伸二。以下我先提出几点重点,是面试官基本必问的问题,请一定要去了解! 基础知识 – 四大组...

终端研发部
12分钟前
1
0
Vue 改变数组触发视图更新

Vue 改变数组触发视图更新 以下方法调用会改变原始数组 push(), pop(), shift(), unshift(), splice(), sort(), reverse()push()push() 方法可向数组的末尾添加一个或多个元素,并返回新的...

不负好时光
18分钟前
1
0
计算机系统要素 C5

本章值得一提的是组织计算机的结构。Hack 的指令和数据是分开存储的,因此它的 CPU 有两个 input: IN inM[16], // M value input (M = contents of RAM[A]) instruction[16],...

lionets
34分钟前
3
0
SpringSecurity404需要注意的地方

在使用@RequestMapping的时候路径的值如果写为("auth"),虽然用的时候前面加不加"/"没有区别,但是在配置了SpringSecurity的http.authorizeRequests().antMatchers()时就必须要注意了! 🌰1...

百萬馬力
37分钟前
2
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部