三、里氏替换原则
里氏代换原则由2008年图灵奖得主、美国第一位计算机科学女博士Barbara Liskov教授和卡内基·梅隆大学Jeannette Wing教授于1994年提出。其严格表述如下:如果对每个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1都代换o2时,程序P的行为没有变化,那么类型S是类型T的子类型。这个定义比较拗口且难以理解,因此一般使用它的另一个通俗版定义:
里氏代换原则(Liskov Substitution Principle,LSP):所有引用基类(父类)的地方必须能透明地使用其子类的对象。
里氏替换原则强调。程序设计中,一个地方可以使用父类对象,那么一定可以使用其子类替换。因为子类是父类的泛化,父类定义的行为和属性,在子类中一定有对应的实现。里氏替换原则是实现开闭原则的保证,只要满足该原则,我们可以在定义对象时使用父类,在程序运行时使用子类来替换,这样可以很方便的扩展系统的功能。
四、依赖倒转原则
依赖倒转原则是Robert C.Martin在1996年为“C++Reporter”所写的专栏Engineering Notebook的第3篇,后来加入他在2002年出版的经典著作Agile Software Development,Principles,Patterns,and Practices一书中。依赖倒转原则定义如下:
依赖倒转原则(Dependency Inversion Principle,DIP):抽象不应该依赖于细节,细节应该依赖于抽象。换言之,要针对接口编程,而不是针对实现编程。
依赖倒转原则强调,在关联关系或者依赖关系中,程序应该引用层次更高的抽象类,而不是具体的类。抽象类被用来设计处理一些共性的问题。这样当系统的行为发生变动时,可以通过增加具体的类的实现来解决具体问题,降低影响范围。
里氏替换原则和依赖倒转原则常常被放在一起使用,用来增加程序的扩展性。下面来看一个例子:
有一个服务器应用,需要得到配置文件的数据,配置文件的类型可以是INI,XML,JSON,未来还有可能增加新的文件类型。在这种情况下,我们可以设计一个"文件解析者"的抽象类,专门用来解析文件的内容,并让他的子类作为具体类,对某一种具体文件的进行具体的解析。
1 #include"iostream" 2 3 class fileParser 4 { 5 public: 6 virtual void parseContent() = 0; 7 }; 8 9 class service 10 { 11 public: 12 // 在定义时使用父类定义 13 void getFileContent(fileParser*file) 14 { 15 // 针对抽象类编程,而不是具体类 16 file->parseContent(); 17 } 18 }; 19 20 class XMLFileParser:public fileParser 21 { 22 void parseContent() 23 { 24 std::cout<<"Parse XML"<<std::endl; 25 } 26 }; 27 28 class jsonFileParse:public fileParser 29 { 30 void parseContent() 31 { 32 std::cout<<"Parse Json"<<std::endl; 33 } 34 }; 35 36 37 int main() 38 { 39 service s; 40 XMLFileParser *file1 = new XMLFileParser(); 41 jsonFileParse *file2 = new jsonFileParse(); 42 // 使用子类实现 43 s.getFileContent(file1); 44 s.getFileContent(file2); 45 return 0; 46 }
在上面这个例子中,我们在定义service的getFileContent接口时,使用抽象类fileParser作为形参,符合依赖倒转原则,在获取具体的文件内容时,使用具体类,符合里氏替换原则。如果需要新增其他的配置文件,只需要增加一个新的具体文件解析类,不用改动其他源代码,这也符合开闭原则。
在大多数情况下,这3个设计原则会同时出现,开闭原则是目标,里氏代换原则是基础,依赖倒转原则是手段,它们相互补充,相辅相成,目标一致,只是分析问题时所站角度不同而已。