里氏替换原则的理解
LSP
里氏代换原则(Liskov Substitution Principle, LSP):所有引用基类(父类)的地方必须能透明地使用其子类的对象。
里氏代换原则的严格表述如下:如果对每一个类型为 S 的对象 o1,都有类型为 T 的对象 o2,使得以 T 定义的所有程序 P 在所有的对象 o1 代换 o2 时,程序 P 的行为没有变化,那么类型 S 是类型 T 的子类型。
LSP 定义拓展
一个软件实体如果适用一个父类的话,那么就一定适用其子类,所有引用父类的地方必须能够透明地使用其子类的对象,子类对象能够替换父类对象,而程序逻辑不变。里氏代换原则告诉我们,在软件中将一个基类对象替换成它的子类对象,程序将不会产生任何错误和异常,反过来则不成立,如果一个软件实体使用的是一个子类对象的话,那么它不一定能够使用基类对象。例如:我喜欢动物,那我一定喜欢狗,因为狗是动物的子类;但是我喜欢狗,不能据此断定我喜欢动物,因为我并不喜欢老鼠,虽然它也是动物。
LSP 引申意义
子类可以扩展父类的功能,但是不能修改父类的功能。
LSP 理解
里氏替换原则通俗来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。也就是说:子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。
根据上述理解,对里氏替换原则的定义可以总结如下:
- 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法
- 子类中可以增加自己特有的方法
- 当子类的方法重载父类的方法时,方法的前置条件(即方法的输入参数)要比父类的方法更宽松
- 当子类的方法实现父类的方法时(重写/重载或实现抽象方法),方法的后置条件(即方法的的输出/返回值)要比父类的方法更严格或相等
如果程序违背了里氏替换原则,则继承类的对象在父类出现的地方会出现运行错误。这时其修正方法是:取消原来的继承关系,重新设计它们之间的关系。
Golang Demo
// 四边形类定义
package liskovsubstitution
type QuardRangle interface {
Width() int
Length() int
}
// 长方形实现
package liskovsubstitution
type Rectangle struct {
length int
width int
}
func (r *Rectangle) SetWidth(width int) {
r.width = width
}
func (r Rectangle) SetLength(length int) {
r.length = length
}
func (r Rectangle) Width() int {
return r.width
}
func (r Rectangle) Length() int {
return r.length
}
// 正方形实现
package liskovsubstitution
type Square struct {
sideLength int
}
func NewSquare(sideLength int) *Square {
return &Square{sideLength: sideLength}
}
func (s *Square) SetSideLength(sideLength int) {
s.sideLength = sideLength
}
func (s Square) Width() int {
return s.sideLength
}
func (s Square) Length() int {
return s.sideLength
}
// 测试里氏替换
package liskovsubstitution
import (
"fmt"
"testing"
)
func Test(t *testing.T) {
square := NewSquare(10)
//resize(square)
}
func resize(rectangle Rectangle) {
for rectangle.Width() <= rectangle.Length() {
rectangle.SetWidth(rectangle.Width() + 1)
fmt.Printf("width:%d,length:%d", rectangle.Width(), rectangle.Length())
}
}