见山只是山 见水只是水——提升对继承的认识

    技术2022-06-12  38

    见山只是山 见水只是水——提升对继承的认识 作者:温 昱 本文发布于《开发高手》

     

    封装、继承、多态是OO 的三大特性,由此可见继承思想的重要性。但是,不少人对继承的理解过多地局限在OOP 层面,从而限制了继承思想在OOD 层面的巨大作用。笔者认为,软件工程师应该不断提升对OO 思想的认识层面,加强实际开发能力。

    本文站在OOD 的角度,将继承看成实现OOD 的强大手段,通过具体例子,说明针对接口编程(P rogram To An Interface )、混入类(Mix In Class )、基于角色的设计(Rolebased Design )这三个与继承紧密相关的著名OOD 技巧。

     

      一、从一则禅师语录说起

     

    《五灯会元》卷十七中,有一则青原惟信禅师的语录:“老僧三十年前未参禅时,见山是山,见水是水。及至后来亲见知识,有个入处,见山不是山,见水不是水。而今得个休歇处,依前见山只是山,见水只是水。”

    禅师高论,颇具哲理,讲的是悟道的过程。其实,领悟OOD 之道的过程又何尝不是如此呢?

     

    1 、见继承是继承——程序员境界

    初学OOP 的人,大多处在“见继承是继承”的层面,最关心的是类的语法、类的成员变量、类的成员函数等这些实现层的东西。这是程序员境界。

     

    2 、见继承不是继承——成长境界

    开始研习OOD 之时,又往往跳到另一个极端,只关心设计,而无心(也可能是无力)关心实现,处在所谓“见继承不是继承”的层面。在这个阶段的人,脑中的兴奋点是“设计”,是职责分配、接口设计、可重用性、可扩展性、耦合度、聚合度等这些设计层的概念。这是成长境界。

     

    3 、见继承只是继承——设计师境界

    学通OOD 之后,会达到“见继承只是继承”的层面。一个“只”字,体现了继承背后的“设计理念”才是该境界的要害。但是,这个阶段和第二阶段不同,第二阶段是一味的否定,而本阶段是否定之否定,把OOP 层面的继承机制看成用来实现特定OOD 的手段加以利用。这是设计师境界。

     

    二、从OOD 层面认识继承

     

    OOP 层面,除了类、成员变量、成员函数这些最基本的概念,最重要的就是代码重用和名字空间的可见性了。而OOD 层面,最基本的概念是类、职责、状态、角色这些更抽象一级的概念,及其相关的耦合度、聚合度、可重用性、可扩展性、可维护性等。可见,虽然OOD 最终要依赖OOP 作为实现手段,但显然OODOOP 并非在同一抽象级上,有不同的概念体系和思维方式。

    再说继承。单纯从OOP 层面看,继承是一个通过复用父类功能而扩展应用功能的基本机制,它允许你根据旧的类快速定义新的类;还有些人用继承仅为了获取名字空间的可访问性。但是,从OOD 层面看,继承可以演变出 “Is-A ”、“Plays Role Of ”等抽象的设计概念。因此,担任设计师角色的人如果自己还限制在OOP 的层面,“设计乏术”的局面是不可避免的。总之,提升对继承的认识,对活用接口继承和实现继承这两种继承机制来实现OOD 意图非常重要。

    与继承相关的OOD 技巧有很多,本文仅讨论比针对接口编程、混入类、基于角色的设计这三种技巧,下图展示了它们和继承的关系。

     

    三、针对接口编程——隔离变化

     

    1 、相关理论

    耦合是依赖的同义词,被定义为“两个元素之间的一种关系,其中一个元素变化,导致另一个元素变化”。抽象耦合被定义为“若类A 维护一个指向抽象类B 的引用,则称类A 抽象耦合于B ”。

    依赖性倒置原则(Dependency Inversion Principle )形式化了抽象耦合的概念,明确表述了应该“依赖于抽象类,不要依赖于具体类”。

    针对接口编程遵守上述原则,从而在很大程度上阻止了变化波及范围的扩大,有效地隔离了变化,有助于增强系统的可重用性和可扩展性。

     

    2 、针对接口编程举例——用于体系结构设计

    根据经典的CoadOOD 理论,一个项目通常包含四个层:用户界面层、问题领域层、数据管理层、系统交互层,如下图所示。

    将体系结构划分为层的一个很大好处是,这些层形成了开发小组的自然分界——每层的开 发人员所需要的技巧是不同的。用户界面层的开发小组需要了解将使用的用户界面工具包;数据管理层的开发小组需要熟悉相关的数据库、持久工具或者使用的文件 系统;系统交互层的开发小组需要了解通讯协议和用到的中间件产品;问题领域层的开发小组不需要了解这些知识,他们需要最深的领域知识,以及用到的相关分布 对象或组件技术。

    但是,要真正使得各个开发小组最大限度地独立开发,还需要一个稳定的体系结构设计做保证才行,其设计的核心思想是:问题领域层“不依赖于”其他任何层,而其他任何层“只依赖于”问题领域层。如下图所示。

    该体系结构设计的实现,极为重要的一点,就是要使用针对接口编程的技巧。以系统交互层对问题领域层的单向依赖为例:

    Ø          如果系统交互层要调用问题领域层的操作,直接调用即可。

    Ø          如果问题领域层要调用系统交互层的操作,需要由问题领域小组定义一个通用的抽象接口,通过针对接口编程调用这个抽象接口;而系统交互小组通过接口继承机制,定义抽象接口的子类,该子类完成抽象接口的具体实现。

    笔者曾有一个项目,该系统需要实时地将本系统的数据变化,通知远端的另一个系统。相关设计如下图所示。在问题领域层,仅包含了一个抽象接口CChangeReporter ,而并不关心CChangeReporter 的具体实现。系统交互层拥有选择具体实现方法的自由,比如CSoapChangeReporter 是用SOAP 通讯协议实现的CChangeReporter CTcpChangeReporter 是用TCP 协议实现的CChangeReporter 。而且假设由于技术的或商业的原因,将来需要同时支持多种通讯协议,也比较容易。

     

    3 、针对接口编程举例——用于类设计

    笔者曾在《运用设计模式设计MIME 编码类》一文中,详述了如何使用策略模式来设计一个可重用、易扩充的MIME 类层次,其中抽象接口类CMimeAlgo 起到了至关重要的作用,现简述如下。

    用户通过CMimeString 使用MIME 编码的功能,CMimeString 允许用户在运行过程中动态配置MIME 编码的具体算法;具体MIME 编码算法由CMimeAlgo 类层次提供,具体的CMimeAlgo 子类的实例化是由CMimeString 根据用户的配置动态完成的;要增加新的MIME 编码算法,只需实现新的CMimeAlgo 子类,并简单扩充CMimeString 的动态实例化代码即可。如下图所示。

     

    四、混入类——更好的重用性

     

    1 、相关理论

    混入类被定义为“一种被设计为通过继承与其他类结合的类”,它给其他类提供可选择的接口或功能。

    从实现上讲,混入类要求多继承;混入类通常是抽象类,不能实例化。

    混入类的作用 在于:它不仅可以提高功能的重用性,减小代码冗余;而且还可以使相关的“行为”集中在一个类中,而不是分布到多个类中,避免了所谓的“代码分散”和“代码交织”问题,提高了可维护性。

     

    2 、混入类举例

    来看一个具体项目。在一个信用卡客户服务系统项目中,要求能够以多种方式发送多种信息给用户,并能够适应未来业务的发展变化。

    当前系统需要支持的发送方式:

    Ø          打印(并邮寄)

    Ø          Email

    Ø          传真

    可预见的未来要支持的发送方式:

    Ø          手机短信

    Ø          PDA 消息

    当前系统需要支持的待发送信息:

    Ø          信用卡对账单

    Ø          信用卡透支催收单

    可预见的未来要支持的待发送信息:

    Ø          信用卡新业务宣传单

    Ø          信用卡促销活动宣传单

    下面是一些设计考虑。一种发送方式要支持多种待发送信息,我们希望发送功能有很好的可重用性;为了方便未来加入对新的发送方式和发送信息的支持,设计必须具有良好的可扩展性。相关设计如下图所示。其中采用了混入类的 OOD 技巧,用一个 CSendableDoc 作为混入类,支持发送功能的重用; CSendalbeDoc 还采用了策略模式支持发送方式的扩充。

     

     

    五、基于角色的设计——使用角色组装协作

     

    1 、相关理论

    协作被定义为“多个对象为了完成某种目标而进行的交互”。角色被定义为“特定协作中的对象的抽象”,它“仅定义了对象特征的一个对某协作有意义的子集”。协作和角色的概念和现实世界很接近,比如下图中, Jane 教授扮演三个角色——母亲、妻子、教授。

    接口分离原则( Interface Separation Principle )信奉“多个专用接口优于一个单一的通用接口”的思想,因为“任何接口都应当具有高内聚性”,以便“保证实现该接口的类的实例对象可以只呈现为单一的角色”。

    基于角色的 设计的意义在于:我们很容易通过已有角色的组合来构造新的协作,以完成新的功能。而且,从 UML 类图可以很自然地导出基于角色的设计方案,例如:

     

    从上面的类图很自然地导出下面的设计:

    2 、基于角色的设计举例

    比如,待开发的一个系统,其后台数据源可能是关系数据库、一般的文件、还可能是另一个私有数据库。既然接口可以隔离变化, 我们可以定义一个单一的接口,为所有的数据客户类提供服务。如下图所示。

    但是,上面的设计违背了基于角色的设计思想,根本不能保证“实现该接口的类的实例对象可以只呈现为单一的角色 ”,这会带来一些问题。比如,有一个数据客户类,不需要插入、更新等功能,而仅仅需要对数据进行读操作,这时显然一个提供“读”服务的“角色”是最合理的设计,但CRowSetManager 却是如此之“宽”的一个接口。最终,我们可以这样来改进设计,如下图所示。

     

    参考文献:

    《设计模式》 Erich Gamma 等著 李英军等译

    《重构——改善既有代码的设计(影印版)》 Martin Fowler

    UML 面向对象设计基础》 Meilir Page-Jones 著 包晓露等译

    Java 设计:对象、UML 和过程》 Kirk Knoernschild 著罗英伟 汪小林译

    《特征驱动开发方法原理与实践》Stephen R. Palmer, John M. Felsing 著熊焕宇等译

    Object-oriented programming: Role-based design W.McUmber 来自网上的幻灯片

    Role = Interface: A Merger of Concepts Friedrich Steimann 来自JOOP

    《运用设计模式设计 MIME 编码类》 温昱 开发高手》第 1


    最新回复(0)