C#forUnity编程语言快速入门教程(连载10)_C#OOP编程之里氏替换原则
最近在Unity教学过程中,有学员对于C#的“里氏替换原则”(LSP)产生疑问表示不太理解。问题集中在不知道这个原则是做什么,有什么优势,具体在游戏开发(程序开发)过程中有什么借鉴与指导作用。
就以上问题我总结梳理一下,与大家共同探讨。
起源: 在我们的程序开发过程中,起初应该都是以完成项目功能为重点。但是随着软件规模的不断扩大,人们发现只完成功能是远远不够的。于是聪明的软件工程师,从大量的编程实践中开始总结一些共性的软件开发理论,以供后人规范编程,更有效率、更稳定的开发各种项目(包括游戏项目)。
目前公认的一些编程原则:
1: 单一职责原则 英文名称是Single Responsibility Principle,简称SRP
2: 开闭原则 英文全称是Open Close Principle,简称OCP
3: 里氏替换原则 英文全称是Liskov Substitution Principle,简称LSP
4: 依赖倒置原则 英文全称是Dependence Inversion Principle,简称DIP
在以上编程开发原则中,“开闭原则”与“里氏替换原则”比较类似,都是研究如何在相对稳定的编码环境中,尽可能的允许程序功能的变化。也就是最低的编码改动量,实现最大程度的软件项目变化性,来适应用户对软件系统不断变化的功能要求。
解释如上概念:
“开放-封闭原则”(即: 开闭原则)(OCP): 对于程序的扩展是开放的(Open for extension),对于程序的修改是封闭的(Closed for modification)
“里氏替换原则”:子类型必须能够替换掉它们的父类型!(反之则不成立)
“ 里氏替换”(也叫“里氏代换”原则)原则英文全称是Liskov Substitution Principle,简称LSP。 由2008年图灵奖得主、美国第一位计算机科学女博士Barbara Liskov教授和卡内基·梅隆大学Jeannette Wing教授于1994年提出。 里氏替换原则就是要求写程序前,先建立抽象,通过抽象建立规范,具体的实现在运行时替换掉抽象,保证系统的高扩展性、灵活性。
“开闭原则”要求我们写的程序修改尽量要少(即:封闭),但是对于功能的扩展确实开放的、允许的。 这其实是提了一个很高的编程目标,很好的一个指导原则,但是如何具体到实现上,是没有方法的,是不接“底气”的。 所以“里氏替换”就给了一个具体的实现方法的原则,就是让子类来无缝的对接父类,“子类型必须能够替掉父类”。
“里氏替换原则”具体的要求可以表述如下:
1: 子类的所有方法必须在父类中声明,或子类必须实现父类中声明的所有方法。根据里氏替换原则,为了保证系统的扩展性,在程序中通常使用父类来进行定义,如果一个方法只存在子类中,在父类中不提供相应的声明,则无法在以父类定义的对象中使用该方法。
2: 我们在运用里氏替换原则时,尽量把父类设计为抽象类或者接口,让子类继承父类或实现父接口,并实现在父类中声明的方法,运行时,子类实例替换父类实例,我们可以很方便地扩展系统的功能,同时无须修改原有子类的代码,增加新的功能可以通过增加一个新的子类来实现。里氏替换原则其实就是“开闭原则”的具体实现手段之一!
我们从序号为2的表述中可以看出,其实“里氏替换”原则就是一种更加具体的手段来实现“开闭原则”。 序号为2的理论其实就是我们常说的“动态多态性”的规则。
针对以上理论,为了更好的理解,笔者给出“里氏替换原则”的实现参考代码,供C#初学者进行参考研究,有具体问题,可以留言,谢谢!
//参考源码1: 以下是使用“虚方法”,来实现“里氏替换”。
class Person
{
//虚方法
public virtual void SpeakLanguage()
{
Console.WriteLine("Person, 说话方法");
}
}
class MrLiu:Person
{
//方法重写
public override void SpeakLanguage()
{
Console.WriteLine("MrLiu, 刘老师说中国话!");
}
}
class Tom:Person
{
//方法重写
public override void SpeakLanguage()
{
Console.WriteLine("Tom, Tom Speak Engligh!");
}
}
class Test
{
public static void Main()
{
/* “里氏替换原则”(即:动态多态性)测试 */
//功能的定义部分
//Person per = new MrLiu(); //打印“MrLiu, 刘老师说中国话!”
Person per = new Tom(); //打印“Tom, Tom Speak Engligh!”
//功能的实现部分
per.SpeakLanguage();
}
}
//参考源码2: 以下是使用“接口”,来实现“里氏替换”。
interface ISpeakable
{
//抽象方法
void SpeakLanguage();
}
class MrLiu:ISpeakable
{
//方法实现
void ISpeakable.SpeakLanguage()
{
Console.WriteLine("MrLiu, 刘老师说中国话!");
}
}
class Tom:ISpeakable
{
//方法实现
void ISpeakable.SpeakLanguage()
{
Console.WriteLine("Tom, Tom Speak Engligh!");
}
}
class Test
{
public static void Main()
{
/* “里氏替换原则”(即:动态多态性)测试 */
//功能的定义部分
//ISpeakable Ispeak = new Tom();//打印“Tom, Tom Speak Engligh!”
ISpeakable Ispeak = new MrLiu();//打印“MrLiu, 刘老师说中国话!”
//功能的实现部分
Ispeak.SpeakLanguage();
}
}
以上两段代码,分别用了父类“虚方法”与“接口”的方式分别实现“里氏替换”,当然还可以用“抽象类”来实现,究竟三者哪个方式,更应该优先使用呢? 现给出一个原则:
“里氏替换”原则可以用以下方式来实现:
1> 虚方法的动态多态性。
2> 抽象方法的动态多态性。
3> 接口方法的动态多态性。
规则: 实现“里氏替换”原则(动态多态性): 能用接口,不用抽象方法,能用抽象方法,不用虚方法。
好了,针对以上问题,笔者就总结以上内容,感兴趣的C#编程爱好者可以进行留言讨论。