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

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

温 昱

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

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

 

 一、从一则禅师语录说起

 

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

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

 

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

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

 

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

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

 

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

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

  

二、从OOD层面认识继承

 

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

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

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

  

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

 

1、相关理论

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

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

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

 

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

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

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

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

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

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

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

笔者曾有一个项目,该系统需要实时地将本系统的数据变化,通知远端的另一个系统。相关设计如下图所示。在问题领域层,仅包含了一个抽象接口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编码类》 温昱 《CSDN开发高手》第1期

以及来自www.objectmentor.com的多篇文章 

作者简介:

温昱,架构设计师,资深咨询顾问,松耦合空间(http://lcspace.nease.net)创办人。擅长面向对象、架构和框架设计,对设计模式、UML和软件工程有深入研究。可以通过wenyu@china.com和作者联系。


  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
保护继承是指子类继承父类的成员,但是只有类内和友元可以访问这些成员,对于类外部的其他代码是不可见的。这样可以保护父类的实现细节,同时也可以在子类中使用这些成员。 下面以学生信息类为例,演示如何使用保护继承。 ```c++ #include <iostream> #include <string> using namespace std; // 父类:人类 class Person { public: Person(string name, int age) : m_name(name), m_age(age) {} void showInfo() { cout << "姓名:" << m_name << endl; cout << "年龄:" << m_age << endl; } protected: // 保护成员 string m_name; // 姓名 int m_age; // 年龄 }; // 子类:学生类 class Student : protected Person { public: Student(string name, int age, int score) : Person(name, age), m_score(score) {} void showInfo() { Person::showInfo(); // 调用父类的 showInfo 函数 cout << "成绩:" << m_score << endl; } private: int m_score; // 成绩 }; int main() { Student s("小明", 18, 90); s.showInfo(); // 调用子类的 showInfo 函数 return 0; } ``` 在上面的例子中,父类 `Person` 中的成员 `m_name` 和 `m_age` 被声明为保护成员,子类 `Student` 继承了这两个成员。在子类中,我们使用 `protected` 访问修饰符将父类的成员变量和成员函数设置为保护成员,这样子类就可以访问这些成员了。 在子类中,我们重写了 `showInfo` 函数,并在其中调用了父类的 `showInfo` 函数,然后输出了子类新增的成员 `m_score`。最后在 `main` 函数中,我们创建了一个 `Student` 对象,并调用了 `showInfo` 函数,输出了学生的信息。 在这个例子中,我们使用保护继承来访问父类的成员,这样可以保护父类的实现细节,同时也可以在子类中使用这些成员。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值