《程序设计与数据结构》——框架与重用

电子说

1.2w人已加入

描述

周立功教授数年之心血之作《程序设计与数据结构》以及《面向AMetal框架与接口的编程(上)》,电子版已无偿性分享到电子工程师与高校群体,。书本内容公开后,在电子行业掀起一片学习热潮。经周立功教授授权,本公众号特对《程序设计与数据结构》一书内容进行连载,愿共勉之。

第四章为面向对象编程,本文为 4.5框架与重用

重用不仅限于软件,比如,贝多芬在他的66号作品中,就重用了另一个伟大作曲家莫扎特的音乐。他从莫扎特的歌曲《魔笛》第22场中,借用了咏叹调“一个女朋友”,然后在该咏叹调中为钢琴师配乐的大提琴家写了一连串7个变奏。

代码重用的问题与所有的设计方法一样,代码的可用性和可重用性取决于它是如何设计和实现的。虽然代码重用并不是OO设计所专有的,但OO方法确实提供了一些机制,有利于可重用代码的开发。

>>>   4.5.1 框架

框架被定义为“一组相互协作的类,形成某类软件的一个可复用设计。框架将设计划分为一组抽象类,并定义它们各自的职责和相互之间的协作,以此指导体系结构级的设计,开发者通过继承框架中的类和组合其实例定制该框架以生成特定的应用。”

从某种意义上来说,框架是可以通过某种回调机制进行扩展的软件系统或子系统的半成品。也就是说,首先,框架是半成品,这是它和其它所有软件组件的本质区别。而某种回调机制,通常面向过程编程使用函数指针作为参数实现回调机制,比如,冒泡排序和快速排序中的compare的形参就是一个函数指针,开发者只需知道自己实现特定的比较函数即可。而面向对象框架的组成部分包括具体类、抽象类和接口,使用抽象方法——多态支持回调机制实现逆向工程。

显然,创建可重用代码的一种方法就是创建框架,框架规定应用的体系结构,它定义了整体结构,类和对象的分割,各部分的主要职责,类和对象如何协作,以及控制流程。框架预定义了这些设计参数,便于设计者聚焦于应用本身的特定细节。但框架使开发程序变得更加容易,因此程序设计需要的许多能力都来源于大量可用的框架。

与代码重用紧密相关的一个概念是标准化,有时也称为即插即用,框架思想围绕的就是这些即插即用和重用原则。在GUI应用程序中,用户界面视为视图。而实际上在MVC框架中,视图是一个接口,一个抽象的概念,因此视图可能是一个用户界面,也可能是一个终端,但只要实现了update接口的类,都可以将它们看作视图,从而全面扩展了MVC框架的应用范围。因为无论怎么改变,MVC框架的模型与视图始终是不变的,可变的是具体模型和具体视图。

以温控器为例,通过传感器的温度检测是具体模型,而监听传感器的LED、数码管和蜂鸣器是具体视图。当温度达到或超过上限值时,则数码管更新显示,LED持续闪烁,蜂鸣器持续报警。根据开闭原则,可以继续重用MVC框架的抽象模型与视图。如果后续只要开发与温度检测相关的系列产品,就可以重用该温度检测模型。

如果设计的系统必须使用不可移植的代码,那么应该将这些代码抽象到类中,通过抽象将这些不可移植的代码隔离到各自的类中。比如,针对基于M0+、M3、M4、ARM9、A7、A8内核的ARM和DSP的不兼容性,周立功单片机公司开发的AWetal和AWorks就是一个将所有的接口、外围器件和组件全部都实现归一化,且与MCU和OS完全无关的框架,从而实现了“一次编程、终生使用、跨平台”,详见《面向接口的编程》系列图书。

由此可见,由于软件的整体框架结构是一样的,因此用户不必学习新的框架;其次,开发人员只要遵循框架文档提供的类或类库的公共接口,以及应用编程接口API等规则,就可以充分利用原有的代码。

>>>   4.5.2 契约

抽象类与接口是实现代码重用的强大机制,为一个重要的概念“契约”奠定了基础。那什么是契约?契约是两方或多方完成或不完成某个指定工作所达成的协议——这是一个由法律保证的协定,因此契约是要求开发人员遵守应用编程接口规范所需的机制。

一般来说,API就是指一个框架,开发人员使用API时,必须遵守框架所定义的规则。比如,方法名和参数个数等。如果没有强制性的措施,一些比较差劲的程序员可能会私下编写他自己的代码,而不使用框架提供的规范。如果人们总是忽视或不考虑标准化,那么标准也就没有什么意义了。

面向对象的设计一个重要的目标就是将接口从实现中分离出来,一个类的接口提供了它的外部视图,记录了所有相关对象的共同属性和行为。其强调的是抽象,隐藏了它的属性和行为的秘密,不需要提供其内部关于该操作的实现(结构)。

接口在很大程度上可以认为是类的外部视图的设计者和类的内部实现的实现者之间的一种“契约”,同时也是需要(使用)该接口的类(如调用该接口所提供的操作)和提供该接口的类之间的一种约定。即将一个较大问题的不同功能通过子契约被分解为小问题,没有别的情况比在设计类时更能体现这种思想。一个单独的对象就是一个具体的实体,在系统中扮演某个角色。

将接口从实现中分离出来是通过抽象类来实现的,抽象类包含一个或多个没有提供任何具体实现的方法。Validator之所以是一个抽象类,因为无法对它实例化。比如:

程序设计

这与契约有什么关系?首先,希望所有与视图对应的显示函数都使用相同的语法调用,比如,实现的每一种视图都包含一个名为validate的方法。其次,每个类都要对自己的动作负责,因此类不仅要提供相应的方法,还必须提供它自己的实现代码。比如:

程序设计

由此可见,采用这种方式,就有了一个真正多态的Validator框架。系统中每个与视图对应的显示函数都可以调用validaate方法,而调用每个与视图对应的显示函数时都会得到不同的结果。实际上,向一个对象发送一个消息时,会根据对象的不同而产生不同的响应,这正是多态的根本所在。

>>>   4.5.3 建立契约

定义契约的规则是通过抽象类提供一个未实现的方法,当设计一个子类实现某个契约时,它必须为父类中未实现的方法提供实现,因为契约带来的好处可以标准化代码。如果开发人员不遵循契约设计类,那么使用类的所有人都必须查看文档。比如:

程序设计

虽然这样做也能够实现范围值和奇偶校验功能,但不符合契约。因为面向对象的主要优势之一是可以重用类,重用的高层次的抽象接口比高度具体的接口更有用。

>>>   4.5.4 教条的危害

著名的语言专家王垠认为,“很多编程的人喜欢鼓吹各种各样的原则,并将那些所谓的原则奉为教条或者秘方。以为兢兢业业地遵循这些原则,空喊几句口号,就可以写出好的代码。同时对违反这些原则的人嗤之以鼻——你不知道,不遵循或藐视这些原则,那么你就是菜鸟。”因此不要盲目地迷信各种各样的原则,比如,DRY原则(Don’t Repeat Yourself,不要重复你自己)在实际的工程中带来了各种各样的问题,却经常被忽视。DRY原则说,如果你发现重复的代码,就提取它们为一个父类。 然而“避免重复”并不等于“抽象”,有时候适当的重复代码是有好处的。

代码的“抽象”和它的“可读性”,其实是相互矛盾的关系。适度的抽象和避免重复可以提高代码的可读性,如果你尽“一切可能”从代码里提取共性,甚至将一些微不足道的“共性”也提出“共享”,反而破坏了程序的可阅读性。如果盲目地将以下代码:

程序设计

修改为:

程序设计

当你看到TypeA和TypeB的定义时,再也不能一目了然地看到int a。其实完全没有必要提取这中无关紧要的共性,造出一个新的父类,因为可见性是程序员产生直觉的关键。奉行DRY原则的人存在的问题,在于是他们随时都在试图发现“将来可能重用”的代码,而不是等到真的出现重复的时候再去做抽象。

抽象思想的关键在于“发现两个东西是一样的”,然而很多时候,开始时觉得两个东西是一回事,最后发现它们其实只是肤浅的相似,而本质完全不同。同一个int a,其实可以表示很多种风马牛不及的性质。你看到都是int a就提出来为父类,反而让程序的概念变得混乱。有些东西开始时貌似同类,当添加了新的逻辑之后,发现它们的用途开始特殊化了。因此过早地提取共性,反而捆住了手脚,为了所谓的“一致性”,而重复一些没用的东西。这样的一致性,其实还不如针对每种情况分别做特殊处理。

防止过早抽象的方法其实很简单,它的名字叫做“等待”。其实就算你不重用代码,也不会影响程序的准确性和可读性,时间能够告诉你一切。如果你发现自己仿佛正在重复以前写过代码,请先不要停下来,坚持将这段重复的代码写完。如果你不将它写出来,你将无法准确地发现重复的代码,因为它们很有可能到最后其实是不一样的。

我们应该避免没有实际效果的抽象,如果代码才重复了两次,就开始提取共性,也许到最后会发现,这个模板总共也就只用了两次。只重复了两次的代码,大部分时候是不值得为它提取模板的。因为模板本身也是代码,而且抽象思考本身是需要一定代价的。所以最后总的开销,也许还不如就让那两段重复的代码待在里面。

而优秀的程序员等到事实证明重用一定会带来好处时,才会开始提取共性进行抽象。实践经验证明,每一次积极地寻找抽象,最后的结果都是制造一些不必要的框架,搞得自己的代码自己都看不懂。如果过度地强调DRY,强调代码的“重用”,随时随地想着抽象,结果就会被这些抽象搅混了头脑,bug百出寸步难行。如果你不能写出“可用”的代码,又何谈“可重用”的代码呢?

其实人们写程序本来自然而然就会在合适的时候进行抽象避免重复,因此千万不要迷信某个大师或专家起了一个DRY这样的名字,就将我们绕进去了,反而使我们丧失了透过现象看本质的思维能力。

回头来看,里氏替换原则也没有什么特别之处,无论是否有人提出这样的原则,子类对象与父类对象的地址值相等且类型相同,这是在语言层面天生就支持的行为。比如,虽然&rangeValidator与&rangeValidator.isa的类型不同,但它们的值相等。由于&rangeValidator.isa与pThis不仅类型相同它们的值相等,因此子类对象替换父类对象也就成为了事实。

当你看透了问题的本质之后,也就具备了洞穿一切的能力。显然里氏替换原则只是套了一个马甲而已,因此人们常说,“尽信书不如无书”,由此可见不无道理。

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分