面向对象的设计原则(二)

3.里氏代换原则(Liskov Substitution Principle, LSP)

      
      里氏代换原则是实现开闭原则的重要方式之一,由2008年图灵奖得主、美国第一位计算机科学女博士Barbara Liskov教授和卡内基·梅隆大学Jeannette Wing教授于1994年提出,其定义如下:
      所有引用基类(父类)的地方必须能透明地使用其子类的对象。
      上述定义可以理解为子类型必须能够替换其父类型。只有当子类可以替换其父类,软件单位的功能不受影响时,父类才能被真正复用,而子类也能够在父类的基础上增加新的行为。同时,由于使用基类对象的地方都可以使用子类对象,因此在程序中尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象。在使用里氏代换原则时需要注意如下问题:
      (1)子类的所有方法必须在父类中声明,或子类必须实现父类中声明的所有方法。根据里氏代换原则,为了保证系统的扩展性,在程序中通常使用父类来进行定义,如果一个方法只存在子类中,在父类中不提供相应的声明,则无法在以父类定义的对象中使用该方法。
      (2)在运用里氏代换原则时,尽量把父类设计为抽象类或者接口,让子类继承父类或实现父接口,并实现在父类中声明的方法,运行时,子类实例替换父类实例,从而可以很方便地扩展系统的功能,同时无须修改原有子类的代码,增加新的功能可以通过增加一个新的子类来实现。里氏代换原则是开闭原则的具体实现手段之一。
下来我们来看一个具体的实例。在现在的很多应用中,用户都有普通用户和VIP用户之分,比如鹅厂的游戏。在游戏中,VIP用户通常拥有着很多特权,比如华丽的登场特效。针对此功能,可能会有如下的初始方案设计:

面向对象的设计原则(二)_第1张图片
      在对系统进行进一步分析后发现,无论是普通客户还是VIP客户,其登场的过程是相同的,只不过显示效果不同,也就是说两个display()方法中的代码重复。并且当增加新类型的用户时,其重复度仍会增加,并且需要在GameRender类中增加新的方法,违反了“开闭”原则。为了让系统具有更好的扩展性,同时减少代码重复,使用里氏代换原则对其进行重构。
      本实例中,可以考虑增加一个新的抽象客户类Customer,而将CommonCustomer和VIPCustomer类作为其子类,游戏渲染类GameRender类针对抽象客户类Customer编程,根据里氏代换原则,能够接受基类对象的地方必然能够接受子类对象,因此将GameRender中的send()方法的参数类型改为Customer,如果需要增加新类型的客户,只需将其作为Customer类的子类即可。重构后的结构如图所示:

面向对象的设计原则(二)_第2张图片

4.依赖倒转原则(Dependency Inversion  Principle, DIP)


       如果说开闭原则是面向对象设计的目标的话,那么依赖倒转原则就是面向对象设计的主要实现机制之一,它是系统抽象化的具体实现。依赖倒转原则是Robert C. Martin在1996年为“C++Reporter”所写的专栏Engineering Notebook的第三篇,后来加入到他在2002年出版的经典著作“Agile Software Development, Principles, Patterns, and Practices”一书中。依赖倒转原则定义如下:
      抽象不应该依赖于细节,细节应当依赖于抽象。换言之,要针对接口编程,而不是针对实现编程。
      依赖倒转原则要求我们在程序代码中传递参数时或在关联关系中,尽量引用层次高的抽象层类,即使用接口和抽象类进行变量类型声明、参数类型声明、方法返回类型声明,以及数据类型的转换等,而不要用具体类来做这些事情。为了确保该原则的应用,一个具体类应当只实现接口或抽象类中声明过的方法,而不要给出多余的方法,否则将无法调用到在子类中增加的新方法。
      在引入抽象层后,系统将具有很好的灵活性,在程序中尽量使用抽象层进行编程,而将具体类写在配置文件中,这样一来,如果系统行为发生变化,只需要对抽象层进行扩展,并修改配置文件,而无须修改原有系统的源代码,在不修改的情况下来扩展系统的功能,满足开闭原则的要求。
      在实现依赖倒转原则时,我们需要针对抽象层编程,而将具体类的对象通过依赖注入(DependencyInjection, DI)的方式注入到其他对象中,依赖注入是指当一个对象要与其他对象发生依赖关系时,通过抽象来注入所依赖的对象。常用的注入方式有三种,分别是:构造注入,设值注入(Setter注入)和接口注入。构造注入是指通过构造函数来传入具体类的对象,设值注入是指通过Setter方法来传入具体类的对象,而接口注入是指通过在接口中声明的业务方法来传入具体类的对象。这些方法在定义时使用的是抽象类型,在运行时再传入具体类型的对象,由子类对象来覆盖父类对象。
      下面来看一个实例。开发人员在开发某CRM系统时发现:该系统经常需要将存储在TXT或Excel文件中的客户信息转存到数据库中,因此需要进行数据格式转换。在客户数据操作类中将调用数据格式转换类的方法实现格式转换和数据库插入操作,初始设计方案结构如图所示:

面向对象的设计原则(二)_第3张图片


      在编码实现图所示结构时,开发人员发现该设计方案存在一个非常严重的问题,由于每次转换数据时数据来源不一定相同,因此需要更换数据转换类,如有时候需要将TXTDataConvertor改为ExcelDataConvertor,此时,需要修改CustomerDAO的源代码,而且在引入并使用新的数据转换类时也不得不修改CustomerDAO的源代码,系统扩展性较差,违反了开闭原则,现需要对该方案进行重构。
      在本实例中,由于CustomerDAO针对具体数据转换类编程,因此在增加新的数据转换类或者更换数据转换类时都不得不修改CustomerDAO的源代码。我们可以通过引入抽象数据转换类解决该问题,在引入抽象数据转换类DataConvertor之后,CustomerDAO针对抽象类DataConvertor编程,而将具体数据转换类名存储在配置文件中,符合依赖倒转原则。根据里氏代换原则,程序运行时,具体数据转换类对象将替换DataConvertor类型的对象,程序不会出现任何问题。更换具体数据转换类时无须修改源代码,只需要修改配置文件;如果需要增加新的具体数据转换类,只要将新增数据转换类作为DataConvertor的子类并修改配置文件即可,原有代码无须做任何修改,满足开闭原则。重构后的结构如图所示:

面向对象的设计原则(二)_第4张图片

参考文献:
1、大话设计模式

2、http://blog.csdn.net/lovelion/article/details/17517213

【作者:墨叶扶风 http://blog.csdn.net/yefufeng


你可能感兴趣的:(设计模式)