设计原则 - 里氏替换原则

在了解里氏替换原则之前,我们先回忆一下我们熟悉的继承,在面向对象的语言中,继承是必不可少的、非常优秀的语言机制,它有如下优点:

  • 代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性;
  • 提高代码的重用性;
  • 子类可以形似父类,但又异于父类,“龙生龙,凤生凤,老鼠生来会打洞”是说子拥有父的“种”,“世界上没有两片完全相同的叶子”是指明子与父的不同;
  • 提高代码的可扩展性,实现父类的方法就可以“为所欲为”了,君不见很多开源框架的扩展接口都是通过继承父类来完成的;
  • 提高产品或项目的开放性。

自然界的所有事物都是优点和缺点并存的,即使是鸡蛋,有时候也能挑出骨头来,继承的缺点如下:

  • 继承是侵入性的。只要继承,就必须拥有父类的所有属性和方法;
  • 降低代码的灵活性。子类必须拥有父类的属性和方法,让子类自由的世界中多了些约束;
  • 增强了耦合性。当父类的常量、变量和方法被修改时,需要考虑子类的修改,而且在缺乏规范的环境下,这种修改可能带来非常糟糕的结果——大段的代码需要重构。


为什么会有里氏替换原则

我们都知道面向对象的三大特性:封装、继承、多态。在实际的开发中,继承是我们经常使用的工具,因为子类可以使用父类的方法和属性,非常方便,但是有时候由于随意重写父类已经实现好的方法,这样是很容易增加代码出错的概率。比如:我们已经有了某一功能M1,由类A来完成,现在需要对功能M1进行扩展,扩展后的功能为M,其中M是由原功能M1和新功能M2组成,新功能M由类A的子类B来实现,那么当子类B在完成新功能M2的同时,有可能会导致原有功能M1出现问题。看个简单例子:

class A {
    func method1(a: Int, b: Int) -> Int {
        return a + b
    }
}

class B: A {
    override func method1(a: Int, b: Int) -> Int {
        return a - b
    }

    func method2(a: Int, b: Int) -> Int {
        return method1(a: a, b: b) + 50
    }
}

简单使用

let b = B()
print("200+100=", b.method1(a: 200, b: 100)) // 100
print("200-100=", b.method1(a: 200, b: 100)) // 100
print("200+100+50", b.method2(a: 200, b: 100)) // 150
可以看到实现的效果并不是我们所想要的,这是因为我们自己重写父类方法所致,父类A中有一个相加的方法,当子类为了实现新功能重写父类方法之后,导致了父类原有的功能发生了变化,因而在使用父类方法的时候出现了错误,这其实违反了里氏替换原则。


什么是里氏替换原则

里氏替换原则(Liskov Substitution Principle - LSP包含两种定义:

第一种定义:

If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T,the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.

如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有发生变化,那么类型S是类型T的子类型。

第二种定义:

Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.

所有引用基类的地方必须能透明地使用其子类的对象。

第二个定义是最清晰明确的,通俗点讲,只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或异常,使用者可能根本就不需要知道是父类还是子类。但是,反过来就不行了,有子类出现的地方,父类未必就能适应。

简单理解就是:派生类(子类)对象能够替换其基类(超类)对象被使用,任意使用基类的地方都可以使用子类,保证了子类能够完美的替换基类。简单看一下代码

class Person {
    func eat() {
        print("eat")
    }
}

class Young: Person {
    func work() {
        print("work")
    }
}

class Client {
    func eatFood(_ person: Person) {
        person.eat()
    }

    func doWork(_ person: Young) {
        person.work()
    }
}

对于使用基类的方法,子类也可以使用

let client = Client()
client.eatFood(Person())
client.eatFood(Young())

但是可以使用子类的地方,基类未必适用

client.doWork(Young())
client.doWork(Person()) // 报错

另外里氏替换原则还包含一层含义:父类中凡事已经实现好的方法(相对于抽象方法而言),实际上是在设定一系列的规范和契约,虽然它不强制要求所有的子类都必须遵从这些契约,但是如果子类对这些已经实现好的方法(非抽象方法)进行任意的修改,就会对整个继承体系造成破坏。


里氏替换原则的四层含义

1、子类必须完全实现父类的方法

2、子类可以有自己的个性,即可以增加特有的方法和属性

3、当子类重写父类的方法是,方法的前置条件要比父类方法的输入参数更宽松。前置条件(pre-condiiton):每个方法调用之前,该方法应该校验传入参数的正确性,只有正确才能执行该方法,否则认为调用方违法签约,不予执行,这称为前置条件。

4、覆写(重写)或者实现父类的方法时,方法的后置条件要比父类更严格。后置条件(post-condition):一旦通过前置条件的校验,方法必须执行,并且必须保证执行结果符合契约,这称之为后置条件。

总的来说:在日常开发过程中,如果需要使用重写,那么就将父类方法声明为抽象方法,即只有声明没有实现,对于已经实现好的方法最好不要进行重写修改,因为会改变父类原有的行为,违法LSP。


参考

《设计模式之禅》

里氏替换原则



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值