六大设计原则:里氏替换原则

1,858 阅读5分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动

本文同时参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金

学了这个后,我才发现以前自己写的代码还真是不及格,说明了自己还没真正理清事件的关系,也许是理清了关系,只是运用的不好。总而言之,只要还有明天,就不断学习,不断努力,有进步,有收获,纵然起点低,也要有不放弃的信心。

定义

里氏替换原则:如果对一个类型为S的对象o1,都有类型为T的对象o2,使得以S所定义的程序P中在o1全都替换成o2时,程序P的行为不发生任何变化,那么S为T的子类。

这个是由麻省理工学院计算机科学实验室的里斯科夫(Liskov)女士在 1987 年的“面向对象技术的高峰会议”(OOPSLA)上发表的一篇文章《数据抽象和层次》(Data Abstraction and Hierarchy)里提出来的。

简而言之就是所有引用基类(父类)的地方必须能透明地使用其子类的对象。

通俗的说,子类对象可以替换父类对象,而不会引起程序的异常和错误

当然,反过来是不行的。

重点

我们虽然大概理解的意思,但是我们需要怎么去做呢?

我们要注意的是什么时候应该使用继承,什么时候不应该使用继承,也就是前面说的,要理清好关系,尽量不要重载或者重写父类的方法,这不包括抽象的方法,我们这么做就是不要改变父类原有的行为。

很明显这也是对开闭原则的补充,是实现抽象化的具体步骤的规范。

优点

  • 增强代码的复用性,每个子类都拥有父类的属性和方法
  • 减少代码出错的风险,克服了继承中重写父类造成的可复用性变差的缺点
  • 加强了程序的健壮性,变更需求也容易做到兼容。
  • 提高代码可扩展性,维护性

例子

又来到举例子环节了,这次例子还是很简单,假设公司全部员工的基本工资都是一样的,都有3k。

我们就可以定义一个 Employee 类,类里面有一个baseMoney的方法。直接返回3k。

代码如下:

class Employee {
    
    /// 基本工资
    /// - Returns: 钱
    func baseMoney() -> Int {
        return 3000
    }
}

接着定义一个程序猿的类,继承员工类,公司说程序猿必须多10块钱,好,那我们就复用baseMoney,加多10块。

//程序猿
class CXY: Employee {
  
    override func baseMoney() -> Int {
        return 3000 + 10
    }
}

我们请求一下员工的工资

var employee = Employee()
print("员工基本工资:", employee.baseMoney())

var cxy = CXY()
print("程序猿工资:", cxy.baseMoney())

输出的结果是:

员工基本工资: 3000

程序猿工资: 3010

这么一看,我们是不是也能实现需求。

而已这么写就违背了里氏替换原则,明显看到,子类cxy替换父类employee,输出的结果已经发生了变化,这不是我们想要的效果。

程序猿的工资是不是没有问题,但我想问一下的是,程序猿的基本工资是多少呢,获取不了,因为子类重写了父类的baseMoney,我们就拿不到3k了。正常的流程cxy.baseMoney输出结果也要是3k才对。

所以,我们应该这么写

//程序猿
class CXY: Employee {
    func cxymoney() -> Int {
       return baseMoney() + 10
    }
}

我们输出一下代码

var employee = Employee()
print("员工基本工资:", employee.baseMoney())

var cxy = CXY()
print("程序猿基本工资:", cxy.baseMoney())
print("程序猿实际工资:", cxy.cxymoney())

输出结果为:

员工基本工资: 3000

程序猿基本工资: 3000

程序猿实际工资: 3010

看,是不是这就解决了程序猿既能获取基本工资,也能获取实际工资,子类替换父类,也不影响程序的结果呢。

好,那我们回想一下,为什么不要在cxy里面改baseMoney,因为我们定义的就是全体员工的基本工资就是3k,所以这里是不能动的,那里氏替换原则是不是就不能用重写,不能多态呢。

这种说法是错误的,对于这个例子,员工是有工资,那我们就可以再加一个员工的方法,然后子类再重写这个方法,这才是正确的重写方法。

class Employee {
    
    /// 基本工资
    /// - Returns: 钱
    func baseMoney() -> Int {
        return 3000
    }
    
    func money() -> Int {
        baseMoney()
    }
}

//程序猿
class CXY: Employee {
    
    override func money() -> Int {
        super.money() + 10
    }
}

输出代码

var employee = Employee()
print("员工基本工资:", employee.baseMoney())

var cxy = CXY()
print("程序猿基本工资:", cxy.baseMoney())
print("程序猿实际工资:", cxy.money())

输出结果:

员工基本工资: 3000

程序员基本工资: 3000

程序员实际工资: 3010

这么做是不是也能解决需要。当然除了这种写法,我还有另外一种写法。

实际上不同的角色员工的工资是不同的结果的,那么money这个方法,输出是不确定的,我们就可以把它看做一个抽象方法

protocol EmployeeProtocol {
    func money() -> Int
}

class Employee {
    
    /// 基本工资
    /// - Returns: 钱
    func baseMoney() -> Int {
        return 3000
    }
}

//程序猿
class CXY: Employee, EmployeeProtocol {
    
    func money() -> Int {
        baseMoney() + 10
    }
}

输出结果:

员工基本工资: 3000

程序员基本工资: 3000

程序员实际工资: 3010

看,是不是也能实现效果呢。

总结

从上面这里例子处理结果来看,实现需求是多样的,我们要写会灵活的运用,尽可能先设计好逻辑,再编写代码。

如果以上说法有不足之处,欢迎留意。