设计模式解析(第2版•修订版)

978-7-115-41014-6
作者: 【美】Alan Shalloway(艾伦•沙洛维) James R.Trott(詹姆斯•R.特罗特)
译者: 徐言声
编辑: 杨海玲

图书目录:

详情

本书首先概述了模式的基础知识,以及面向对象分析和设计在当代软件开发中的重要性,随后使用易懂的示例代码阐明了12个最常用的模式,使读者能够理解模式背后的基本原则和动机,理解为什么它们会这样运作。 本书适合软件开发专业人士,以及计算机专业、软件工程专业的高校师生阅读,也可作为面向对象分析与设计课程的参考教材。

图书摘要

软件开发方法学精选系列

Design Patterns Explained:A New Perspective on Object-Oriented Design,Second Edition

设计模式解析(第2版·修订版)

[美]Alan Shalloway James R.Trott 著

徐言声 译

人民邮电出版社

北京

图书在版编目(CIP)数据

设计模式解析/(美)沙洛维(Shallowav,A.),(美)特罗特(Trott,J.R.)著;徐言声译.--2版(修订本).--北京:人民邮电出版社,2016.1

(软件开发方法学精选系列)

书名原文:Design Patterns Explained:A New Perspective on Object-Orientde Design,Second Edition

ISBN 978-7-115-41014-6

Ⅰ.①设… Ⅱ.①沙…②特…③徐… Ⅲ.①软件设计 Ⅳ.①TP311.5

中国版本图书馆CIP数据核字(2015)第279417号

内容提要

本书以作者自身学习、使用模式和多年来为软件开发人员(包括面向对象技术老兵和新手)讲授模式的经验为基础撰写而成。首先概述了模式的基础知识,以及面向对象分析和设计在当代软件开发中的重要性,随后使用易懂的示例代码阐明了12个最常用的模式,包括它们的基础概念、优点、权衡取舍、实现技术以及需要避免的缺陷,使读者能够理解模式背后的基本原则和动机,理解为什么它们会这样运作。

本书适合软件开发专业人士,以及计算机专业、软件工程专业的高校师生阅读,也可作为面向对象分析与设计课程的参考书。

◆著 [美]Alan Shalloway James R.Trott

译 徐言声

责任编辑 杨海玲

责任印制 张佳莹 焦志炜

◆人民邮电出版社出版发行  北京市丰台区成寿寺路11号

邮编 100164  电子邮件 315@ptpress.com.cn

网址 http://www.ptpress.com.cn

北京艺辉印刷有限公司印刷

◆开本:800×1000 1/16

印张:19.5

字数:335千字  2016年1月第2版

印数:3001-5500册  2016年1月北京第1次印刷

著作权合同登记号 图字:01-2012-7096号

定价:55.00元

读者服务热线:(010)81055410 印装质量热线:(010)81055316

反盗版热线:(010)81055315

广告经营许可证:京崇工商广字第0021号

版权声明

Authorized translation from the English language edition,entitled:Design Patterns Explained:A New Perspective on Object-Oriented Design,Second Edition,0321247140 by Alan Shalloway,James R.Trott,published by Pearson Education,Inc.,publishing as Addison-Wesley Professional,Copyright © 2005 Pearson Education,Inc.

All rights reserved.No part of this book may be reproduced or transmitted in any form or by any means,electronic or mechanical,including photocopying,recording or by any information storage retrieval system,without permission from Pearson Education,Inc.

CHINESE SIMPLIFIED language edition published by PEARSON EDUCATION ASIA LTD.and Posts & Telecommunications Press Copyright © 2015.

本书中文简体字版由Pearson Education Asia Ltd.授权人民邮电出版社独家出版。未经出版者书面许可,不得以任何方式复制或抄袭本书内容。

本书封面贴有Pearson Education(培生教育出版集团)激光防伪标签,无标签者不得销售。

版权所有,侵权必究。

其他

献给Leigh、Bryan、Lisa、Michael和Steven,为了他们的爱、支持、

鼓励和牺牲精神。

——Alan Shalloway

献给Jill、Erika、Lorien、Mikaela和Geneva,你们是我生命花园中的玫瑰。

唯上天得荣耀。

——James R.Trott

前言

如果我已经有了第1版,还需要买第2版吗?

回答当然是“需要”!原因如下。

自完成第1版的写作之后,我们对设计模式又有了大量更深入的理解,包括以下一些方面。

如何使用共性和可变性分析来设计应用程序的架构。

设计模式与极限编程(eXtreme programming,XP)和敏捷开发的关系,以及设计模式如何有助于二者的实施。

为什么测试是高质量编程的一个优先原则。

为什么使用工厂(factory)实例化和管理对象至关重要。

对帮助学生理解如何用模式思考而言,哪些模式是必不可少的。

本书探讨了所有这些主题。我们进一步深化和澄清了前一版阐述过的主题,并增加了一些非常有用的新内容,包括:

第15章,共性和可变性分析;

第20章,来自设计模式的教益:各种工厂模式;

第22章,Object Pool模式(《设计模式》一书中没有讨论的模式);

第24章,工厂模式的总结。

我们改变了一些模式的阐述顺序,据参加该课程的学生反映,这样的顺序更有助于掌握模式背后的思想。

所有章节的内容都进行了少量修改,综合了过去三年来从许多读者那里收到的各种反馈意见。而且,为了帮助学生学习,我们为每一章都编写了复习题(答案在本书配套网站可以找到)。

我们可以非常坦率地说,本书无疑是少数值得拥有的第2版,即使读者已经购买了第1版。

非常乐于倾听您宝贵的建议。

——Alan和Jim

设计模式和面向对象程序设计曾经做出过这样的承诺:要简化软件设计人员和开发人员的工作。技术媒体甚至大众媒体每天都在传播相关的术语。然而,要学习这两种技术,熟练掌握它们并且知其所以然,可能非常困难。

你使用某种面向对象或者基于对象的语言可能已经多年,可你是否知道,对象真正的威力并不在于继承,而是来自封装行为。你可能对设计模式很好奇,而且感觉相关的著作都有些太过深奥和夸张。倘若如此,本书正适合你。

本书是以作者多年来为许多软件开发人员(包括面向对象技术老兵和新手)讲授模式的经验为基础撰写而成的。我们相信,而且我们的经验也已经证明,如果能够理解模式背后的基本原则和动机,理解它们为什么会这样运作,那么你的学习曲线将不可思议地缩短。而且在我们对设计模式的讨论中,可以懂得真正的面向对象思维定式,这正是登堂入室的必由之路。

通过阅读本书,读者能够完整地理解12个核心设计模式和1个分析模式,也将了解到设计模式并不是孤立存在的,多个设计模式协同工作才能帮助我们创建更加“健壮”的应用程序。你还可以获得阅读其他设计模式文献所需的足够基础知识,如果愿意,可能还能够自己发现新的模式呢。最重要的是,你将为创建灵活、完善而且更易维护的软件做好准备。

虽然这里所讲授的12个模式并没有涵盖所有应该学会的模式,但是理解了这12个模式,就能够举一反三,更加容易地自学其他模式。我们没有讨论入门所需之外的更多模式,而是讲述了更加有用的与模式相关的若干问题。

从面向对象到模式再到真正的面向对象

从很多方面来看,本书实际上是在复述我自己学习设计模式的经历。我是在学习模式本身之后,再学习模式背后的思想。然后,又将这种理解扩展到分析和测试领域,也扩展到学习模式与敏捷编程方法的关系中。本书第2版中包含了许多第1版出版后我的一些领悟。在学习设计模式之前,我自认为已经是一个很不错的面向对象分析和设计专家了,我曾经设计和实现了几个针对许多不同行业的非常出色的项目。我使用C++并且正在学习Java,代码中的对象可以说是中规中矩[1]、封装严密,而且还能为继承层次结构设计出优秀的数据抽象。我想自己应该已经得面向对象之道了。

现在回想起来,我发现自己那时虽然总是遵循大多数专家的建议行事,但是并没有真正理解面向对象设计的全部威力。直到开始学习设计模式,我的面向对象设计能力才得以拓展和加强。即使还没有直接使用模式,理解设计模式也已经使我成为更加出色的设计人员。

我开始学习设计模式是在1996年。那时我还是美国西北部一家大型航天公司的一名C++和面向对象设计讲师。有几个人要求我领导一个设计模式学习小组,正是在那里我遇到了本书的另一位作者Jim Trott。学习小组中发生的几件事情很有意思。一开始,我就对设计模式着了迷。我很喜欢能够将自己的设计与其他经验更多的人的设计进行比较。然后,我就发现自己并没有发挥“按接口设计”(designing to interface)的全部威力,而且并不总是关注是否存在“一个对象在还不知道另一个对象的类型时就使用这个对象”的情况。我还注意到,刚刚从事面向对象设计的人(一般总是认为这时就学习设计模式过早)从学习小组所得的获益,居然同专家差不多。设计模式展示了优秀的面向对象设计实例,而且阐明了基本的面向对象设计原则,这些有助于初学者更快设计出成熟的方案。到整个学习结束时,我已经完全相信:设计模式是面向对象设计发明以来软件设计领域出现的最伟大的东西。

可是,在我审视当时自己的工作时,却看到代码中并没有使用任何设计模式。或者说,至少还没有有意识地使用任何一个模式。后来,随着对模式学习的深入,我发现自己开始在代码中使用许多设计模式了,不再仅仅是一个好的编程匠。当然,现在我对模式的理解更加深入,使用起它们来也更加得心应手了。

我当时只是觉得自己可能对设计模式还了解得不够,应该学习更多。那时我只了解其中6个模式。然后,我突然顿悟。当时我是一个项目的面向对象设计顾问,而且应要求为项目做高层设计。这个项目的负责人极为聪明,但在面向对象设计方面却是一个新手。

问题本身并不怎么难,但是对代码维护性的要求很高。我花两分钟查看了一下问题,然后就照本宣科地按照通常使用的数据抽象方法提出了一个设计。很不幸,我自己也清楚这不会是什么好的设计。只用数据抽象使我无功而返,必须另寻良策。

两个小时之后,我用尽了自己知道的所有设计技术,已经黔驴技穷,情况却毫无好转的迹象。我的设计没有本质上的变化。最让人感到灰心的是,我知道肯定有更好的设计方案,只是我想不出来。更具讽刺意味的是,我还知道这个问题里藏着4个设计模式,可是我不知道怎么使用。于是,我——一个自封的面向对象设计专家,被一个简单问题生生噎住了!

我感到失望之极,只好休息一下,出去走走,清醒清醒头脑。我告诉自己,至少10分钟之内不要再想这个问题了。可是,才过30秒钟,我就忍不住又思考起来!这次我突生灵感,刹那间,自己的设计模式观改变了:不能将模式作为一个单独的东西使用,应该把它们结合起来。

模式应该相互配合,共同解决问题。

这话以前我也听说过,但是当时并没有真正理解。因为软件中的模式最初以设计模式为名引入,我想当然地认为它们主要都是关于设计的,并一直囿于这样的想法而碌碌无为。我曾认为,在设计领域,模式就是类之间合乎规则的关系。后来读到Christopher Alexander的奇书The Timeless Way of Building(牛津大学出版社,1979)[2],我才知道所有层次——分析、设计和实现都存在模式。Alexander曾阐述了使用模式有助于理解问题域(甚至有助于描述它),而不仅仅是用来在理解问题域之后完成设计。

我错就错在试图在问题域中创建类,然后再将它们结合起来形成一个系统,这正是Alexander所说的非常糟糕的做法。我从没有自问过这些类是否正确,因为它们看上去很好,显而易见,类从一开始分析就马上浮现于我的脑海,而且它们正是按教科书一直教的那样应该在系统描述中寻找的那些“名词”。但是试图将它们组合起来时却遇到了重重困难。

当我再回到办公室,在设计模式和Alexander方法的指导下重新创建类时,仅仅几分钟时间,一个大为改观的解决方案就展现出来。这真是一个好设计,我们最后根据它实现了产品。我真的很兴奋——兴奋于自己设计了如此优秀的方案,也兴奋于设计模式的威力。从那时起,我开始将设计模式融入开发工作和教学中。

我发现,不熟悉面向对象设计的程序员也可以学习设计模式,而且,通过学习设计模式,他们能够掌握基本的面向对象设计技术。对我而言就是如此,对我教授的学生而言亦然。

想象一下我是多么惊讶吧!我曾经读过的设计模式图书,曾经与之交谈过的设计模式专家都说,在开始设计模式研究之前,需要扎扎实实地打好面向对象设计基础。可是,我自己的亲身经验却表明,在学习面向对象设计的同时学习设计模式的学生,比仅学习面向对象设计的学生进步更快,他们掌握设计模式的速度甚至与有经验的面向对象老手一样。

我开始使用设计模式作为自己教学的基础,并且将自己的课程称为“面向模式的设计:设计模式从分析到实现”。

我希望我的学生能理解这些模式,继而可以发现使用一种探索式的方法是有助于促进这种理解的最佳方式。例如,我发现,先提出问题,然后通过大多数模式中都适用的一些指导性原则和策略,帮助学生尝试为此问题设计出解决方案,这样阐述Bridge(桥接)模式更好。在实际探索中,学生找到了解决之道——其实就是Bridge模式,并牢牢地记在心中。

设计模式与敏捷方法/极限编程

设计模式之下所隐藏的指导性原则和策略现在对我而言已经非常清楚了。《设计模式》一书中肯定提到了这些内容,但是讲得太简洁,以至于我第一次阅读时完全没有体会到其价值。我相信《设计模式》一书实际上是以Smalltalk社区为目标读者而写的[3],这些原则在Smalltalk社区可以说是根深蒂固,所以不需要太多背景。但是因为自己对面向对象范型的理解很有限,我理解这些原则花了很长时间。直到我将《设计模式》四位作者的工作与Alexander的工作、Jim Coplien关于共性和可变性分析的工作、Martin Fowler关于方法学和分析模式的工作结合起来之后,这些原则对我而言才变得足够清晰,我甚至能够向其他人讲解。这对我自己的教学生涯也很有帮助——我再也不能像自己工作时那样容易地想当然,而又能侥幸无事了。

自本书第1版出版以来,我一直在进行大量的敏捷开发实践,具有了较多的极限编程实践、测试驱动开发(TDD)和Scrum的经验。刚开始,在结合设计模式和极限编程、测试驱动开发时还是经历了一段困难时期。但是,我很快认识到它们都非常重要,而且植根于一些相同的原则(虽然设计方法并不相同)。事实上,在敏捷软件开发训练课上,我明确说明,如果正确使用设计模式将为引入敏捷开发打下很好的基础。

贯穿本书始终,我讨论了许多设计模式与敏捷管理和编程实践的关联。如果读者对极限编程、测试驱动开发或者Scrum不熟悉,可以不用太在意这些论述。但是,如果真的如此,我建议你下一步就去读一本有关的著作。

我发现无论何种情况下,都可以用这些指导性原则和策略来“推导”出几个设计模式。这里“推导出设计模式”的意思是,如果看到可能用设计模式解决的问题,就可以使用从模式中学到的这些指导性原则和策略,得到以模式表达的解决方案。我明确地告诉学生们,我们并不是用这种方法真正得出设计模式;相反,我只是要说明一种可能的思考过程,最终成为设计模式的那些解决方案的提出者使用的也是这样的过程。

我解释这些数量不多但很强大的原则和策略的能力与日俱增。随之而来的,是我发现自己解释《设计模式》一书中的模式时,它们更加有用了。事实上,我在设计模式课上用这些原则和策略能够阐述几乎所有的模式。

我还发现,无论是否使用设计模式,自己在设计中都在使用这些原则。我对此并不感到惊讶。如果使用这些原则和策略能够得到后来才发现等效于设计模式的设计,那么就说明这些原则和策略已经给了我一种自己做出优秀设计的方法(因为根据定义,模式代表着优秀设计)。有了这些技术难道还会只是因为不知道对应模式(可能已知也可能还没有发现)的名字而得出较差的设计吗?

这些认识帮助我很好地磨砺了自己的培训过程(和现在的写作)。我现在的教学工作已经有了好几个层次。教授面向对象分析和设计基础时,我教设计模式,并用它们作为优秀面向对象分析和设计的例子。此外,通过设计模式来教授面向对象概念,学生们还能更好地理解面向对象原则。而且教授指导性原则和策略,使学生们可以自己创建出质量能够与模式媲美的设计。

在此讲述这些,是因为本书正是沿袭了我所教授课程的模式,几乎所有的材料就是我们目前的课程之一——“设计模式、测试驱动开发或者敏捷开发最佳实践”[4]的内容。

通过阅读本书,你将学习到这些模式。但尤其重要的是,你将学到模式为何有效和如何协同工作,以及模式背后的原则和策略,这将有助于充分利用你自身的经验。当本书提出一个问题时,如果你能够联想到一个曾经碰到的类似问题,将极其有益。本书并没有讲述什么新知识或者如何应用新模式,而是提供了一种考虑面向对象软件开发的新视角。我希望你的自身经验能够与设计模式的原则结合,成为学习过程中强大的助力。

Alan Shalloway

2000年12月第1版

2004年10月第2版

从人工智能到模式再到真正的面向对象

我的设计模式历程与Alan的设计模式历程可以说是殊途同归,具有下述同样的结论。

基于模式的分析能够使我们成为更高效也更有效的分析人员,因为它使我们能够更加抽象地处理模型,因为它代表了许多其他分析人员的集体经验。

模式能够帮助人们学习面向对象的原理,并有助于解释我们处理对象的方式。

我的职业生涯开始于人工智能领域,工作是创建基于规则的专家系统。这其中涉及倾听专家们的讲述,为其决策过程建立模型,然后将这些模型编码成基于知识的系统中的规则。在构建这些系统时,我发现了一些反复出现的主题。对于一些相同类型的问题,专家们总是用类似的方法去解决。例如,诊断设备问题的专家往往首先寻找简单、快速的解决方案,然后再系统化一些,通过系统分析将问题分解为多个子问题。在系统诊断时,他们也往往在进行其他形式的测试之前,先尝试成本低或者能排除较大范围问题的测试。其实无论诊断的是计算机中的还是某个油田设备的问题,这种方法都适用。

要换在今天,我会把这些反复出现的主题称为“模式”。设计新的专家系统时,我很自然地也开始寻找这些反复出现的主题了。对于模式思想,我当时是非常开放而且心存好感的,虽然还不知道它们是什么。

后来到了1994年,我发现欧洲的研究者们已经整理了这些专家行为的模式,放入名为“知识分析和设计支持”(简称KADS)的软件包中。Karen Garder博士——一位才华横溢的分析师、建模专家、顾问,一个天才,开始在美国将 KADS 应用于她的工作中。她扩展了欧洲研究者的工作,将 KADS 应用于面向对象系统。她使我的眼界大开,看到了软件界正在形成的一个全新领域——基于模式的分析和设计,这很大程度上都源自 Christopher Alexander 的工作。Karen Garder的书Cognitive Patterns(剑桥大学出版社,1998年)叙述了这些思想。

突然之间我有了一个为专家行为建模的框架,能够不至于过早地陷入复杂和异常情况。借助这种框架完成接下来的三个项目时,我花费的时间缩短了,重复工作减少了,而最终用户的满意度却提高了,原因如下。

我能更快地设计模型,因为模式已经事先告知会出现什么情况。它们能够告诉我基本的对象是什么,什么应该特别注意。

我与专家沟通的成效大增,因为对于处理细节和异常情况我们有了结构化更强的方法。

通过模式,能够针对系统设计更好的最终用户培训方案,因为模式已经预先告知系统最重要的特性。

最后一点意义重大。模式之所以能够帮助最终用户理解系统,是因为它们提供了系统的来龙去脉,说明了为什么我们会这样设计。我们可以用模式描述系统的指导性原则和策略。我们可以用模式开发最佳范例,从而帮助最终用户理解系统。

我被这一切吸引住了。

因此,当我就职的公司组织了一个设计模式学习小组时,我当然积极参与。于是我遇到了本书的另一位作者Alan Shalloway,他作为一位面向对象设计师和顾问也殊途同归地在工作中体会到了类似的境界。于是也就有了本书的诞生。

自完成第1版以来,我进一步领悟到这种分析方法能够多么深刻地增进我们的理解。我参与了许多不同类型的项目,其中许多甚至与软件开发无关。我看到许多一起协作、相互交换知识、交换思想、生活在不同地方的人所组成的各种系统。模式和面向对象的原则依然有助于我。和计算机系统中一样,减少工作系统之间的依赖性也能获得很好的效果。

我衷心希望本书中讲述的原则能够对各位读者成为更有效而且更高效的分析人员有所帮助。

James R.Trott

2000年12月第1版

2004年12月第2版

本书约定

在本书写作过程中,我们选用了一些特定的风格和版式约定。读者可能对其中一些不太习惯。因此我们对如此选择的原因作以下说明。

第一人称

本书是两位作者的合作作品。为了找到阐述这些概念的最佳方式,我们经常争论并且不断做出改进。Alan 在他的课程中曾使用这些方式试讲,然后我们又共同进行了改进。本书主体部分中我们选择使用第一人称单数,因为这能够如我们希望的那样,以更生动和自然的方式娓娓道来。

便于浏览

我们试图使本书易于浏览,读者能够在不全部阅读正文的情况下获得要点,也可以迅速找到需要的信息。我们大量使用了表格和带有项目符号的列表并在页边加上了总结相应段落的文字。讨论每个模式时,提供了模式关键特性总结表。我们相信这些措施能够使本书的可读性大大提高。

示例代码

本书讲述的是分析和设计,而不是具体的代码实现,其目的是帮助读者在面向对象开发中积累的真知灼见和最佳实践经验的基础上,思考如何做出优秀的设计,就像用设计模式所表示的那样。所有程序员都会遇到的挑战之一就是:不能过早开始进行实现,需要三思而后行。既然如此,本书有意地尽量避免过多讨论实现。我们的示例代码可能看上去有些分量不足而且不够完整。比如说,代码都没有提供错误检查。这是因为使用它们只是为了说明概念。然而,本书的配套网站http://www.netobjectives.com/dpexplained中存放了更完整的示例代码 ①,书中的代码是从中摘出来的。C++语言和C#语言的示例代码也可以在这个网站中找到。

策略和原则

本书是一本介绍性读物,有助于你快速学习设计模式,并从中理解启发了设计模式的原则和策略。阅读本书之后,可以继续阅读更学术化的模式图书或参考书。本书最后一章将列出许多可能对你有用的参考书。

展现广度,形成认识

本书努力使读者能够对设计模式有所认识,书中将展现模式领域的广度,但不会很深入地讲述任何一个模式(参见上一点)。

如果带一个人去美国旅游两个星期,你会带他去哪里呢?也许是几个主要景点,可以帮助他了解一些建筑、风土人情、城市及城市之间的广袤原野的风光、高速公路和咖啡厅,但是不可能向他展示一切。为了丰富他的知识,可以选择性地给他播放一些其他景点和城市的宣传片,使他有所认识。以后他就可以自己计划未来的旅游行程。我们试图让你对设计模式有一个基本认识,因而与此类似,本书也将带你参观设计模式中的主要景点,然后使你对其他方面也有所认识,这样读者就可以自己计划进一步的设计模式学习旅程了。

C#开发人员如何阅读Java代码

本书中的所有代码都是用Java语言编写的。如果你没有使用Java的经验,但是能够阅读C#代码,需要了解以下几点:

Java使用 extends和 implements分别表示扩展另一个类或者实现一个接口的类,而不是像在C#中那样两种情况都用冒号(:)。

因此,在Java程序中,我们会看到:

public class NewClass extends BaseClass

或者

public class NewClass implements AnInterface

而在C#程序中,看到的则是:

public class NewClass : BaseClass

或者

public class NewClass : AnInterface

Java中的所有方法都是虚拟的,因此不用指定方法是new还是overridden。Java中没有这样的关键字,所有子类方法都会改写它们从基类继承的方法。虽然还有其他区别,但是我们的代码示例中不会出现。

C++开发人员如何阅读Java代码

C++开发人员阅读Java要困难一些,但是也不算太难。最明显的区别是Java没有头文件。但是阅读组合了头文件和代码的Java式文件,其实非常直观,无需解释。除了上述与C#的区别之外,Java是不在栈中存储对象的。Java在堆存储区中存储对象,栈中只存储保存着对象引用(指针)的变量。所有对象都必须用new关键字创建。

因此,在Java程序中,我们会看到:

MyClass anObject= new MyClass();

anObject.someMethod();

而在C++中,看到的则是:

MyClass *anObject= new MyClass();

anObject->someMethod();

因此如果在每个引用对象的变量名声明中添加一个星号(*),然后将句点(.)转换为连字符加右尖括号(->),Java代码看上去就像C++代码了。

反馈

设计模式仍然在发展当中,它们其实就是发现最佳实践以及面向对象基本原则的专业人员之间的行话。

因此,我们非常重视你对本书的反馈意见。

我们什么地方做得比较好,什么地方做得还不够?

有没有需要改正的错误?

有没有什么地方写得不够清晰?

请访问Net Objectives公司为本书英文版设立的配套网站,网址是http://www.netobjectives.com/dpexplained。在这个网站上,能够找到我们的最新研究成果,以及与本书和一般性软件开发问题相关的讨论组。请在讨论组中发表改正意见、评价、真知灼见和心得体会。

第2版的新内容

第2版相对第1版而言有许多变化和改进。它反映了我们过去几年使用和教授设计模式中的收获,也加入了从读者那里收集到的无私和非常有价值的反馈意见。

主要的变化有下面几个方面。

重新组织了章节顺序(例如,将对Strategy模式的叙述提前)。

扩展了对共性和可变性分析(CVA)的讨论。

将极限编程和设计模式结合起来。

所有示例现在都是完整的可执行代码,而不再是示意性的或者片段代码了。配套网站还有C#和C++语言示例代码。

增加了对为什么将工厂用作实例化器/管理器的解释,这部分内容极为有用。

新增一个《设计模式》书中没有讲到的模式:Object Pool。

讨论了模式的缺陷,包括关于“将模式作为辅助思考指导”的警告。模式并不是真理!

在语法和风格方面也进行了大量小的订正。

致谢

几乎每本书前言的最后都会有致谢,感谢帮助该书出版的人。直到自己写书,我们才如此真切地体会到其中缘由。出一本书的确是集体劳动的结晶。我们要感谢的人,可以列出一个长长的清单。

以下诸位对我们的帮助尤其重要。

Addison-Wesley公司的Debbie Lafferty,她永不疲倦对我们进行鼓励和督促。

我们的同事Scott Bain,他耐心地审阅了本书,并提供了许多真知灼见。他和Alan在Net Objectives公司的合作对于本书第2版中新增的许多内部都很有启发性。

我们的审稿团队:James Huddleston、Steve Metsker和Clifton Nock。

特别要提到Leigh和Jill——她们是极富耐心的妻子,容忍而且鼓励我们完成本书,实现梦想。

审阅本书第1版和第2版多个草稿版本的人很多,他们提供了许多极好的评论。我们尤其要提到 Brian Henderson、Bruce Trask、Greg Frank和Sudharsan Varadharajan,他们无私而且耐心地让我们分享了他们自己的所知所想。

Alan要特别感谢——

我以前的几个学生,他们对我的影响之大,可能他们自己都永远不会知道。在我对是否在课程中引入新想法犹豫再三,感觉似乎应该墨守成规的时候,是他们在我刚开始讲述课程时对新概念的热情,鼓励我在课程中越来越多地加入了自己的想法。感谢 Lance Young、Peter Shirley、John Terrell和Karen Allen。他们的行动对我来说是一种不断的提醒,说明鼓励的作用是多么深远。

John Vlissides,感谢他富于思想的意见和启发性的询问。

第2版还要加上对另一位Net Objectives公司的同事Dan Rawsthorne博士的感谢。他从事敏捷开发的方法对我影响很大。对另一位同事Jeff McKenna的支持和肯定我也感激不尽。我还要感谢Kim Aue,我们 Net Objectives公司的“大总管”,她在各方面的支持对我帮助极大。

我要特别感谢Martin Fowler、Ken Schwaber、Ward Cunningham、Robert Martin、Ron Jeffries、Kent Beck和Bruce Eckel就本书相关问题与我的交谈(有时候是通过电子邮件),当然,这并不是意味着本书内容他们都同意或者认可。

Jim要特别感谢——

Karen Gardner博士,人类思维模式顾问和良师。

Marel Norwood博士和Arthur Murphy,我在KADS和基于模式分析方面的最初合作者。

Brad VanBeek,他为我提供了在本学科中发展的空间。

Alex Sidey,他指导我掌握技术写作的规律和诀窍。

Sharon和Bob Foote博士,现在任教于西点军校,使我具备了对人永不知足的好奇心和持久的兴趣。他们的爱和鼓励已经成为模式永存我心,无论是作为一个人、一位父亲和丈夫,还是作为一位分析师。

千禧救助与开发服务组织(www.mrds.org)的Bill Koops和Lindy Backues,他们帮助我看到,基于模式的方法甚至能够用于救助贫穷和边缘化的人。他们真是好伙伴,好导师。

[1].原文为well-formed,这个术语许多文献译为“格式/形式良好的”、“良构”等等,但根据C++标准1.4.1的定义:“按照语法规则、可诊断语义规则和定义一次规则构造的C++程序”,似乎按照数学界的习惯译为“合式的”或“合乎规则的”更好。此处因为并不强调技术语义,所以随文就译了。——译者注

[2].中文版《建筑的永恒之道》,由知识产权出版社出版。——译者注

[3].此说法不确切,设计模式的起源与量子力学殊途同归的群英会非常相似,得多人之力,而众多先驱中只有 Kent Beck、Ward Cunningham和Ralph Johnson等是纯Smalltalk社区背景的,Eric Gamma、Jim Coplien、John Vlissides等则来自C++社区。事实上,《设计模式》一书本身是用C++作为描述语言的。——译者注

[4].参见本书英文版配套网站http://www.netobjectives.com/dpexplained,还有更多有关课程的信息。

第一部分 面向对象软件开发简介

概览

本部分内容

本部分将介绍一种以模式(众多设计人员和用户多年来所获得的领悟和最佳实践经验)为基础的面向对象软件开发方法,以及支持此方法的建模语言(UML)。

我不会再采用20世纪80年代的方式,只是告诉开发人员“在需求表述中寻找名词,并将它们转化为对象”。在这种方式中,将“封装”定义为“数据隐藏”,将“对象”定义为“含有数据和用来访问、操作数据的行为的一些东西”。这在当时和现在都是一种局限性很大的观点,其局限在于只是关注“如何实现对象”,这是很不完整的。

本部分将对这些概念的定义进行扩展,并以此为基础讨论另一种面向对象范式。这些扩展的定义都是源自设计模式研究的一些策略和原则的结果。这反映了一种更完整的面向对象观。

章 讨论的主题

1 面向对象范型

介绍对对象的最新理解。

统一建模语言(UML)为我们提供了工具,能够以一种图形化的、更易理解的方式描述面向对象设计。

第1章 面向对象范型

1.1 概览

本章内容

本章将通过与大家都熟悉的范型——标准结构化程序设计比较异同的方式,来介绍面向对象范型。

当年,面向对象范型正是为了应对使用标准结构化程序设计遇到的诸多挑战才应运而生的。弄清楚这些挑战,我们才能够更好地看到面向对象程序设计的优点,并更好地理解这一机制。

本章无法使你成为面向对象方法的专家,甚至不会介绍所有基本的面向对象概念。但是,本章将使你为阅读本书其他部分做好准备。本书其他部分将阐释如何像专家所做的那样正确使用面向对象设计方法。

本章中,我们将:

讨论一种常用的分析方法,名为功能分解(functional decomposi-tion);

探讨需求方面问题和应对需求变更的需要(这可是程序设计中罪恶的渊薮!);

叙述面向对象范型,并展示其实际应用;

指出一些特殊的对象方法;

提供一个面向对象术语表,列出了本章所用到的重要对象术语。

1.2 面向对象范型之前:功能分解

功能分解是一种处理复杂问题的自然方法

让我们从对一种常用的软件开发方法的考察开始吧。如果给你一个任务,要编写一段代码,访问在数据库中存储的形状描述然后显示出来。按照所需要的步骤来思考,是一种很自然的选择。比如,你可能认为应该按照以下步骤解决这个问题。

1.在数据库中找到形状列表。

2.打开形状列表。

3.按某种规则将列表排序。

4.在显示器上显示各个形状。

还可以选取以上任意一个步骤,进一步分解成实现所必需的若干步。例如,可以将步骤 4 分解。对于列表中所有形状,都可以按照以下步骤进行。

4a.识别形状的类型。

4b.获取形状的位置。

4c.以形状的位置作为参数,调用显示形状的函数。

这种方法就称为“功能分解”,因为分析人员将问题分解成了多个功能步骤(这些步骤就构成了这个问题)。你我都会这样做,因为解决更小的问题,比解决整个问题更简单。这种方法与我写制作意大利肉末番茄烤面条的烹饪过程,或者装配自行车指南所用的方法是一样的。这种方法我们使用得如此驾轻就熟,以至于我们很少对它有所怀疑,或者自问是否还有其他的选择。

这种方法的挑战:能者多责

功能分解方法的一个问题在于,它通常会导致让一个“主”程序负责控制子程序,这是将功能分解为多个子功能的自然结果。但是,主程序所承受的责任太多了:要确保一切正确工作,还要协调各函数并控制它们的先后顺序,因此经常会产生非常复杂的代码。如果让一些子函数负责自己的行为,而且能够告知主函数执行某些任务,并信任它知道如何执行,这种方式比功能分解的方式要容易得多。叱咤疆场的将军和家庭中成功的父母对这种经验都了然于胸。现在,程序员也学会了,这就是所谓委托(delegation)。

这种方法的难题:应对变化

功能分解方法的另一个问题在于,它在为未来可能出现的变化未雨绸缪方面,在对代码合适地改进方面,都于事无补。变化是无法避免的,经常是因为要为已有的主题增加新的变体。例如,我可能不得不处理新的形状,或者需要显示形状的新方法。如果将实现各步骤的所有逻辑代码都放在一个大函数或大模块中的话,那么这些步骤的任何实质性变化,都必须对这个函数或模块进行修改。

而且变化还会为bug和意料之外的结果创造机会。或者,像我喜欢说的:

许多bug都源于代码修改。

自己去验证这句断言吧。考虑这样的情景:想对代码进行修改,但又害怕这样做,因为你知道修改一个地方的代码可能会破坏其他地方。怎么会出现这种情形呢?代码非要关注所有函数和使用它们的方式吗?函数应该怎样和另一个函数交互呢?函数要关注的细节是否太多了,比如要实现的逻辑、要交互的东西、要使用的数据?和人一样,如果程序试图同时关注过多的东西,一旦有变化出现,就只能坐等bug来到。程序设计可是一种复杂、抽象和动态的活动啊。

而且,无论多么努力工作,无论分析做得多么好,也是永远无法从用户那里获得所有需求的,因为关于未来有太多未知,万物皆变化。不是吗,它们总是在变化之中……

对于阻止变化,我们无计可施。但是我们对变化本身却并非无能为力。

1.3 需求问题

需求总在变化

问问软件开发人员,对于从用户那里获取的需求,他们认为有哪些说法正确。他们经常像下面这样回答。

需求是不完整的。

需求经常是错误的。

需求(和用户)容易让人误解。

需求并不会告诉你全部情况。

只有一种回答你是听不到的:“我们的需求不仅完整、清晰、易于理解,而且还说明了我们今后五年需要的所有功能!”

在 30 年编写软件的经历中,关于需求我所学会的主要一点就是,需求总在变化。

我还了解到,大多数开发人员都认为这是一件坏事。但是很少有人能够编写可以很好地处理需求变更的代码。

需求之所以变化,有如下几个简单原因。

用户对自己需求的看法,会因为与开发人员的讨论以及看到软件新的可能性而发生变化。

开发人员对用户问题领域的看法,会在开发使该领域自动化的软件的过程中,因为对它更加熟悉而发生变化。

我可能无法知道什么将会变化,但是我能够猜到在哪里会变化

在刚入行的时候,我有一个师傅总爱说:“第二次既然总能编写正确,你第一次也应该能编写正确!”我经常会想起这条忠告。我曾经认为这句话的意思是尝试预期所有可能发生的变化,并相应地构建代码。这当然再好不过,但是通常结果都会令人失望的,因为很少能够预测整个过程所有可能的变化。

最后,我认识到,虽然无法预测会发生什么变化,但是通常可以预期哪里会发生变化。面向对象的巨大优点之一,就是可以封装这些变化区域,从而更容易地将代码与变化产生的影响隔离开来。

软件开发的环境发生了变化。(5年前谁能想到Web开发能有今日?)这并不意味着我们可以不去收集好的需求。这只是说明我们编写的代码必须要能适应变化,说明我们(可能还有我们的客户)不应该为阻止那些自然而然会发生的事情而庸人自扰。

发生变化了!从容应对

在所有情况下(最简单的除外),需求总会变化的,无论最初的分析做得多好!

与其抱怨需求总是变化,不如改变开发过程,从而更有效地应对变化。

代码可以设计得使需求的变化不至于产生太大影响。代码可以逐步演进,新代码可以影响较少地加入。

1.4 应对变化:使用功能分解

用模块化封装变化

更进一步地来看“显示形状”问题。怎样编写代码才能更容易地应付多变的需求呢?与其编写一个大函数,不如使之更加模块化。

例如,在前面提到的步骤4c“以形状的位置作为参数,调用显示形状的函数”中,可以写一个例1-1所示的模块。

例1-1 用模块化封装变化

函数:显示形状

输入:形状类型,形状描述

操作:

switch (形状类型)

case 方形:调用显示方形的函数

case 圆形:调用显示圆形的函数

功能分解方法中模块化的问题

然后,在接到一个需求,要显示新的形状(例如三角形)时,我只需改变这个模块即可。(希望如此!)

但是这种方法仍然存在问题。比如,前面说过,这个模块的输入是形状的类型和描述。然而,在不同的形状存储方式下,对所有形状都适用的一致描述可能存在,也可能不存在。如果形状的描述有时以包含坐标点的数组方式存储,有时以其他方式存储,怎么办呢?这种方法还适用吗?

模块化肯定有助于提供代码的可理解性,而容易理解将使代码更容易维护。但是模块化并不总是有助于代码应对所有可能遇到的变化。

低内聚,紧耦合

这种方法我一直在使用,我发现它主要有两个问题,按术语来说就是低内聚(weak cohesion)和紧耦合(tight coupling)。在Code Complete一书(Microsoft Press, 1993)中,Steve McConnell对内聚性和耦合性有很精彩的描述。他说:

内聚性(cohesion)指的是“例程中操作之间联系的紧密程度”[1]

我还听说过有人将内聚性称为清晰性(clarity),因为例程(或类)中的多个操作联系越紧密,就越容易理解其含义。说一个类低内聚,指的就是它任务很多而且互不相关,代码经常看上去像是令人疑惑的一大团。最极端的情形下,这些类会与系统中差不多所有东西都纠缠在一起,据说有人称之为“上帝对象”,因为它们好像是万能的(或许是因为只有上帝才能理解它们)。

耦合性(coupling)指的是“两个例程之间联系的紧密程度。耦合性与内聚性是相辅相成的关系。内聚性描述的是一个例程内部组成部分之间相互联系的紧密程度,而耦合性描述的是一个例程与其他例程之间联系的紧密程度。软件开发的目标应该是创建这样的例程:内部完整(高内聚),而与其他例程之间的联系则是小巧、直接、可见、灵活的(松耦合)。”[2]

修改一个函数甚至是函数所用的数据,都可能对其他函数产生严重破坏

大多数程序员都会有这样的经验:在代码的某个地方修改了一个函数或一个数据,后来却对代码的其他地方造成了意想不到的影响,这种bug称为“不良副作用”。这是因为,虽然我们获得了希望的结果(进行了修改),但是也得到了不需要的结果——bug!更糟糕的是,这些bug经常难以发现,因为我们一开始往往不会注意到那些导致副作用的代码联系(如果能够注意到这些联系,就不会用这种方式修改程序了)。

事实上,这种bug使我有了一个非常惊人的发现:我们实际上并没有花费很多时间改正程序的bug。

我认为,在维护和调试过程中,改正bug只需要花费很少的时间。维护和调试的绝大多数时间都被用于努力弄清代码的运作机理、寻找bug和防止出现不良副作用上了,真正的改正时间却相当短!

因为不良副作用经常是最难发现的bug,所以如果让一个函数处理很多不同的数据,一旦需求发生变化,就更可能出问题。

问题就出在副作用中

只关注函数,就可能引起难以发现的副作用。

维护和调试中所耗费的大多数时间不是花在修改bug上,而是花在寻找bug,弄清如何避免在修改代码时导致不良副作用上了。

功能分解将注意力放在错误的地方

使用功能分解时,需求变更会对软件开发和维护工作产生极大影响。这时候,主要的精力都放在函数上了,而对一组函数或者数据的修改会影响到其他函数和数据,并依此类推地影响到其他必须修改的函数。就像一个雪球滚下山来,一路裹挟了更多的雪一样,只关注函数,将导致一连串变化,而且难以避免。

1.5 应对需求变更

日常生活中人们如何做事?

为了找出解决需求变更问题的办法,弄清功能分解有没有其他替代方法,我们先来看看日常生活中人们是如何做事的。假设你是要在一个会议[3]上担任讲师,听课的人在课后还要去听其他课,但他们不知道下一堂课的听课地点。你的责任之一,就是确保大家都知道下一堂课去哪里上。

如果按照结构化程序设计的方法,可以按以下的要求做。

1.获得听课人的名单。

2.对于名单上的每个人,做以下工作。

a.找到他或者她要听的下一堂课。

b.找到该课的听课地点。

c.找到从你的教室到下一堂课地点怎么走。

d.告诉这个人怎样去上下一堂课。

为了完成以上工作,你可能需要编写以下内容。

1.获得听课人名单的方法。

2.获得每个人课程表的方法。

3.告诉某个人如何从你的教室到其他任何教室的程序。

4.为听课的每个人服务的一个控制程序,它可以为每个人完成所需的步骤。

你会采用这种方法吗?

我很怀疑是否有人真的会按这样的方法去做。相反,你可能会把从这个教室到其他教室的路线贴出来,然后告诉课堂上的所有人:“我已经将下一堂课的地点和其他教室的位置都贴在教室后面了。请根据它找到你们下一堂课的教室。”可以预期每个人都知道自己的下一堂课是什么,而且他们都能从你提供的列表中查到正确的教室,然后按照指示找到它。

这两种方法之间的区别何在呢?

第一种方法——直接给每个人都提供指示,你必须密切关注大量细节,除你之外没有其他人负责。这样你会疯掉的!

第二种方法中,你只给出通用的提示,然后期待每个人会自己弄清怎样完成任务。

责任从你自己转移到每个人……

其中最大的区别就是这种责任的转移。在第一种情况下,你要对一切负责;而在第二种情况下,学生对自己的行为负责。两种情况下,要实现的目的相同,但组织方式差异很大。

这样有什么影响呢?

为了看到这种责任重新安排带来的影响,我们考虑一下在指定了新的需求时情况如何。

假设我被告知,需要给承担助教工作的研究生一些特殊指示。他们可能需要在上下一堂课之前收集本节课的学生评价,并交到会议办公室。在上面的第一种情况下,我将不得不对控制程序进行修改以区分研究生和本科生,然后给研究生特殊指示,从而可能不得不对程序做相当大幅度的修改。

……可以尽量减少变化

而在每个人都各司其责的第二种情况下,我只需要为研究生再编写一个程序,而控制程序仍然只需说“找到你们下一堂课的教室”。每个人只要按此指示相应行事即可。

这代表控制程序的责任发生了明显变化。在第一种情况下,每次需要增加新的一类学生时,控制程序本身都必须作修改,要负责告诉新一类学生如何去做。而在第二种情况下,新一类的学生不会影响控制程序,由学生自己负责弄清如何去做。

为什么会有这种区别呢?

存在不同的原因

这是因为第二种方法有以下三方面不同。

人们对自己的行为负责,而不再由一个中央控制程序负责决定他们的行为。(请注意,为此人们还必须知道自己是什么类型的学生。)

控制程序可以与不同类型的人(研究生和普通学生)交流,好像他们都一样。

控制程序不需要知道学生从此教室到彼教室可能需要采取的任何特殊步骤。

不同的视角

为了完整理解其中的含义,创建一些术语非常重要。在UML Distilled一书(Addison-Wesley, 1999)中,Martin Fowler描述了软件开发过程中的三个不同视角(perspective)[4],如表l-1所示。

视角有何用?

我们再来看看前面那个“去下一堂课教室”的例子。请注意,作为讲师的你是在概念层次上与人交流。换句话说,你告诉学生的是“你要他们做什么”,而非“如何去做”。但是,他们如何去下一堂课的教室则是非常明确的,因为他们遵循着明确的指令,而这是在实现层次进行的。

在一个层次(概念)上交流,而在另一个层次(实现)上执行,这样请求者(讲师)就无需准确知道具体操作细节了,只需一般性——概念性地知道即可。这一点的效力可能非常大:只要概念不变,请求者就与实现细节的变化隔离开了。接下来我们来看如何描述这些概念,以及如何编写使用这些概念的程序。

1.6 面向对象范型

使用对象将责任转移到更局部的层次

面向对象范型以对象概念为中心,一切都集中在对象上。编写代码时是围绕对象而非函数进行组织的。

对象是什么?对象传统上被定义为带有方法(面向对象领域称呼函数的术语)的数据。糟糕的是,这是一种非常有局限性的对象观。稍后我会给出一个更好的对象定义(在第8章中还会谈到)。我说到对象的数据时,可能指数值和字符串这样的简单事物,也可能指其他对象。

使用对象的优点在于,可以定义自己负责自己的事物(参见表1-2)。对象天生就知道自己的类型。对象中的数据能够告诉它自己的状态如何,而对象中的代码能够使它正确工作(也就是说,做要求它做的事情)。

在这种情况下,对象是通过寻找在问题领域中的实体而被发现的。然后再通过查看这些实体需要做些什么,为每个对象确定责任(或者称方法)。这与通过在需求中寻找名词发现对象和通过寻找动词发现方法的技术是一致的。随着所遇到问题更加复杂,我们将看到,这种技术存在很大局限性,本书中我会给出一种更好的方式。但是现在我们还要从此方式开始入手。

怎么理解对象?

理解对象的最佳方式,是将其看成“具有责任的东西”。有一条好的设计规则:对象应该自己负责自己,而且应该清楚地定义责任。这就是我之所以说“Student 对象的责任之一是知道怎样从一个教室去下一个教室”的原因。

或者,使用Fowler的视角

还可以用Martin Fowler的视角框架来观察对象:

在概念层次上,对象是一组责任;[5]

在规约层次上,对象是一组可以被其他对象或对象自己调用的方法(也称行为);

在实现层次上,对象是代码和数据,以及它们之间的计算交互。

糟糕的是,人们对面向对象设计的教学和讨论更多的是停留在实现层次上——也就是只考虑代码和数据,对概念层次和规约层次重视很不够。然而,从后两种层次去思考对象其实也具有巨大的效力。

对象具有供其他对象使用的接口

因为对象具有责任而且自己负责自己,所以必须有办法告诉对象要做什么。还记得吗?对象含有说明自己状态的数据,还有实现必要功能的方法。对象的很多方法都将标识为可被其他对象调用。这些方法的集合就称为对象的公开接口(public interface)。

例如,在教室的例子中,我可以编写含有一个gotoNextClassroom()方法的 Student 对象。我不需要向这个方法传递任何参数,因为每个Student对象都自己负责自己。也就是说,Student对象知道:

为了能够找到下一个教室,它需要什么;

怎样为完成这个任务获取所需的其他信息。

围绕类组织对象

刚开始,只有一种学生——普通学生需要从一个教室到另一个教室去。请注意,在我的教室(我的系统)中可能有很多这样的“普通学生”。当然可以每个学生都有一个对象对应,从而能够容易地和分别地跟踪每个学生的状态。但是,要求每个 Student 对象都有自己的一组方法,告诉它能做什么和怎样做,显然效率很低,尤其是在对所有学生而言任务都一样的时候。

一种效率更高的办法是,让所有学生与一组方法关联起来,每个学生都可以根据自己的需要使用或修改这些方法。我希望定义一个“一般学生”来包含这些公共方法的定义。然后,可以有各种各样特殊的学生,每个特殊学生都必须掌握自己的私有信息。

在面向对象术语中,这种“一般学生”被称为类(class)。类就是对对象行为的定义,它包含以下内容的完整描述:

对象所包含的数据元素;

对象能够操作的方法;

访问这些数据元素和方法的方式。

因为对象所包含的数据元素可以不同,所以同一类型的对象可以含有不同数据,但它们都具有相同的功能(如方法所定义)。

对象是类的实例

要获得一个对象时,我告诉程序需要某个类型(type,也就是对象所属的类)的一个新对象,这个新对象称为类的一个实例(instance)。创建类实例的过程称为实例化(instantiation)。

在例子中使用对象

使用面向对象方法为“去下堂课教室”的例子编写代码比以前的方法简单多了。步骤如下所示。

1.开始控制程序。

2.实例化室中学生的集合。

3.告诉此集合,让学生去自己下堂课的教室。

4.集合让每个学生去自己下堂课的教室。

5.每个学生都:

a.找到自己下堂课的教室在哪里;

b.决定怎么去;

c.去那里。

6.完成。

抽象类型的需要

在需要加入另一个学生类型比如研究生之前,这一方式能够很好地工作。

遇到难题了。看起来我必须允许任何类型的学生(普通学生或者研究生)加入这个集合。但我面临的问题是,怎样让集合引用其元素呢?因为我是在讨论如何用代码实现,这时集合实际上将是包含某个类型对象的数组或其他容器。如果集合命名为RegularStudent(普通学生)之类,我就不能将 GraduateStudent(研究生)类型的对象放入集合。如果我说集合仅仅是一组对象,我又怎么确定其中不包含类型错误的对象(即不能“去下堂课的教室”)呢?

解决方案很直截了当。我需要一个能包容多种具体类型的一般类型。在本例中,我需要一个包含RegularStudent对象和GraduateStudent对象的Student类型。在面向对象的术语中,我们称Student类为抽象类(abstract class)[6]

抽象类定义了一组类可以做什么

抽象类定义了其他一些相关类的行为。这些“其他”类是代表了某种特殊类型的相关行为的类。这样的类通常被称为具体类(concrete class),因为它代表着一个概念特定的、不变的实现。

在本例中,Student就是抽象类。具体类RegularStudent和GraduateStudent则代表了两种类型的Student。RegularStudent是一种Student,GraduateStudent也是一种Student。

这种关系叫做is-a(是一个/种)关系,是我们称之为继承(inheritance)关系的一种特例。于是,我们说RegularStudent类继承自Student类。其他类似的说法还有:GraduateStudent派生自Student类,Graduate-Student特化(specialize)了Student类,或者GraduateStudent是Student的子类。

而另一方面,我们说Student类是GraduateStudent类和Regular-Student的基类,Student类泛化(generalize)了二者,或者Student类是GraduateStudent类和RegularStudent的超类(superclass)。

抽象类可以充当其他类的占位符

抽象类可以充当其他类的占位符。可以使用抽象类定义其派生类必须实现的方法。抽象类还可以包含所有派生类都能够使用的公共方法。[7]派生类是使用抽象类的默认行为还是使用自己的有所变化的行为,由派生类自己决定(这与“对象自己负责自己”的要求一致)。

这就意味着,我可以在控制程序中编写一些对象,它们的引用类型都是Student。编译器能够检查Student引用所指向的是否真的是一种Student。这种机制使我们能够实现鱼与熊掌兼得,同时获得了以下两方面的优点。

集合只需处理 Student对象(从而使 Instructor对象也只需要处理Student对象)。

但是类型检查仍然存在(只有能够“去下堂课教室”的 Student对象会包含进来)。

而且每一种Student都可以按自己的方式实现功能。

抽象类不只是不能实例化

抽象类经常被描述为“不能实例化的类”。这个定义本身没错——在实现层次上。但是局限性太大了。在概念层次上定义抽象类会更有帮助。在概念层次,抽象类就是(实现抽象类所代表的概念的)其他类的占位符。

也就是说,抽象类为我们提供了一种方法,能够给一组相关的类赋予一个名字。这使我们能够将这一组相关类看成一个概念。

在面向对象范型中,必须总是从概念、规约和实现所有三个视角层次来思考问题。

可见性

因为对象都自己负责自己,所以有很多东西不需要暴露给其他对象。前面我曾提到公开接口——可以被其他对象访问的方法的概念。在面向对象系统中,可访问性主要分为以下几种类型[8]

公开(public)——任何对象都能够看见。

保护(protected)——只有这个类及其派生类的对象能够看见。

私有(private)——只有这个类的对象能够看见。

这就引出了封装(encapsulation)的概念。封装经常被简单地描述成“数据隐藏”。一般而言,对象不应该将内部数据成员暴露给外部世界。(也就是说,其可见性是protected或private。)

封装

但封装可不只是指数据隐藏。封装一般意味着各种隐藏。

在本例中,讲师不知道哪些是普通学生,哪些是研究生。所以学生的类型对讲师隐藏了。(也就是说,我封装了学生的类型。)在面向对象语言中,抽象类Student将隐藏从其派生的类的类型。你将在本书后面看到,这是一个非常重要的概念。

多态

另一个要理解的术语是多态(polymorphism)。

在面向对象语言中,我们经常用抽象类类型的引用来引用对象。但是,我们真正引用的是从抽象类派生的类的具体实例。

因此,当我通过抽象引用概念性地要求对象做什么时,将得到不同的行为,具体行为取决于派生对象的具体类型。“多态”这个词来源于“poly”(意为“很多”)和“morph”(意为“形态”)。因此,它的意思是“很多形态”。这个名称非常合适,因为同一个调用能够获得很多不同形态的行为。

在本例中,讲师告诉学生“去下堂课的教室”。但是,根据学生类型的不同,他们会采取不同的行为(因此出现了多态)。

(续)

* 有些面向对象分析人员说万事万物皆对象:类是对象,实例也是对象。这在技术上可能是正确的,但是却成了混淆和发生争议的地方。本书所称的对象是类的实例。

1.7 面向对象程序设计实践

新实例

我们再次考察一下本章开始讨论的形状实例。怎样用面向对象的方式实现它呢?请记住,我们必须完成以下任务。

1.在数据库中找到形状列表。

2.打开形状列表。

3.按某种规则将列表排序。

4.在显示器上显示各个形状。

为了用面向对象方式解决这个问题,我需要定义一些对象和这些对象具有的责任。

在Shape程序中使用对象

所需要的对象如下表所示。

运行程序

现在主程序的步骤应该与下面给出的类似。

1.主程序创建一个数据库(ShapeDataBase)对象的实例。

2.主程序要求数据库对象找到我感兴趣的一组形状,然后实例化一个保存这些形状的 Collection 对象(实际上,它还将实例化Collection对象中存放的Circle对象和Square对象)。

3.主程序要求Collection对象将所存放的形状排序。

4.主程序要求Collection对象显示形状。

5.Collection对象要求所存放的所有形状显示自己。

6.每个形状根据形状种类显示自己(使用Display对象)。

为什么这有助于应对新需求

我们来看这个方案怎么会有助于我们应对新的需求(请记住,需求总在变化)。例如,考虑如下的新需求。

增加新种类的形状(例如三角形)。为了引入一种新的形状,只需两步:

创建Shape类的一个新的派生类,来定义这个新形状;

在新的派生类中,实现与该形状对应的display方法。

修改排序算法。为了修改形状排序方法,只需一步:

修改Collection的sort方法。这样所有形状都将使用新算法。

结论:面向对象方法有效地限制了需求变更所带来的影响。

再谈封装

封装有几个优点。“对用户隐藏”这一事实直接蕴涵了以下优点。

使用更容易,因为用户不需要再操心实现问题了。

可以在不考虑调用者的情况下修改实现。(因为调用者从一开始就不知道对象是如何实现的,它们之间不应该存在任何依赖关系。请记住,在维护中时间往往花在了解和留心这些依赖关系上,而不是实际添加新功能。)

其他对象对该对象内部是未知的——这些外部对象往往用来帮助实现该对象接口所指定的功能。

优点:减少副作用

最后,考虑功能改变时引起的不良副作用问题。这种bug通过封装有效地解决了。对象内部对于其他对象是未知的。如果使用封装,并遵循“对象自己负责自己”的策略,那么唯一能影响对象的办法就是调用该对象的方法。对象的数据和实现其责任的方式都与其他对象所带来的变化屏蔽开来。

封装拯救了我们

对象对自己行为所负的责任越多,控制程序需要负的责任就越少。

封装使对象内部行为的变化对其他对象变得透明了[9]

封装有助于防止不良副作用。

值得注意的是封装与耦合的关系。封装什么东西时,必然将使其耦合变松。隐藏实现(即封装它们)有助于松耦合。

1.8 特殊对象方法

创建和销毁

我已经讨论了可能被其他对象或对象自己调用的方法,但是当对象创建时到底发生了什么事情?当它消亡时又发生了什么?如果对象应该是自成一体的单位,那么它自己包含处理这些情况的方法,将是一个不错的主意。

这些特殊方法事实上确实存在,它们就是构造函数(constructor)和析构函数(destructor,或者终结方法,finalizer)。

构造函数负责初始化或创建一个对象

构造函数是对象创建时自动调用的一个特殊方法,它的目的是处理对象起始时的工作,这是对象“自己负责自己”所要求的。构造函数是一个进行初始化、设置默认信息、设定与其他对象关系或创建一个明确的对象所需的其他工作的天然场所。所有面向对象语言都会在创建对象时查找并执行相应的构造函数。

通过正确使用构造函数,可以更容易消除(或者至少最大程度地减少)未初始化变量,这种错误通常源于开发者的粗心大意。如果代码中有一个固定且一致的地方(即对象的构造函数)进行所有的初始化工作,可以更容易地确保初始化。未初始化变量所引起的错误很容易改正,但很难发现,因此这种约定(以及构造函数的自动调用)能够提高程序员的效率。

析构函数(终结方法)在对象不再需要时(已被删除时)将其清除

大多数面向对象语言都提供了对象不再存在时清除该对象的方式。在C++和C#中称之为析构函数,在Java中称之为终结方法。本书中,我将采用通用术语析构函数称呼这一概念。

所有面向对象语言都会在对象删除时查找并执行相应的析构函数。与构造函数一样,析构函数的使用也是对象“自己负责自己”所要求的。

析构函数通常用于在对象不再需要时释放资源。因为Java有垃圾收集机制(对象不再使用时自动清除)[10],析构函数在Java中不像C++中那么重要。在C++中,由对象的析构函数负责销毁只由这个对象使用的其他对象是很常见的。

1.9 小结

本章内容

本章中我说明了面向对象技术是怎样帮助我们最大程度地减少系统需求变更带来的影响,以及面向对象与功能分解的异同。

我还讨论了面向对象程序设计的许多基本概念,介绍和描述了主要术语。表1-3总结了这些概念,表1-4总结了面向对象程序设计的主要术语。

复习题

简答题

1.叙述功能分解中使用的基本方法。

2.导致需求变更的三个原因是什么?

3.我提倡用责任而不是功能来思考。这意味着什么呢?请举出一个例子。

4.给出耦合和内聚的定义。什么是紧耦合?

5.对象接口的目的是什么?

6.给出类实例的定义。

7.类是一个对象行为的完整定义。这句话说明了对象的哪三个方面?

8.抽象类的作用是什么?

9.对象可能具有的三种主要可访问性[11]是什么?

10.给出封装的定义,并举出一个行为封装的例子。

11.给出多态的定义,并举出一个多态的例子。

12.观察对象的三种视角是什么?

阐述题

1.有时候,程序员使用“模块”来隔离不同区域的代码。这是应对需求变更的有效方式吗?为什么?

2.将抽象类定义为不能实例化的类局限性很大,为什么呢?抽象类更好的(或者至少,另一种)理解方式是什么?

3.行为的封装是怎样帮助限制需求变更带来的影响的?它又怎样挽救程序员免于无意导致的副作用?

4.接口怎样有助于保护对象不受其他对象变化的影响?

5.在一个系统中要使用教室作为描述对象。请从概念视角描述教室。

观点与应用题

1.需求变更是系统开发人员所面临的最大挑战之一。请从你自己的亲身经历中找出一个支持这一说法的例子。

2.功能分解方法在遇到需求变更时存在本质上的弱点。你同意这种说法吗?为什么?

3.你认为应对需求变更的最佳方法是什么?

第2章 UML

2.1 概览

本章内容

本章将简单概述UML(统一建模语言),这是面向对象界主要使用的一种建模语言。如果你还不知道 UML,阅读本章将使你具备阅读本书模型图所需的最低限度的知识。

本章中,我们将:

叙述“什么是UML”和“为什么使用UML”;

阐述本书中的基本UML图,即

类图;

交互图。

2.2 什么是UML

UML提供了多种建模图

UML 是一种用来创建程序模型的图形语言(即带有语意的一种图形记号)。在此上下文中,术语“程序模型”指的是程序的图形表示,可以说明代码中对象之间的关系。

UML 中有好几种不同的图——有些用于分析,有些用于设计,还有些用于实现 [更准确地说,是用于部署(deployment),也就是代码的发布(distribution)]。(参见表 2-1)根据图的目的不同,每个图都说明了不同实体集合之间的关系。

2.3 为什么使用UML

主要用于交流

UML 主要是用来交流的——与我自己、与我的小组成员、与我的客户。在软件开发领域中糟糕的(不完整的或者不准确的)需求无处不在,而UML为我们提供了提高需求质量的工具。

有利于清晰

UML 提供了一种方法,可以用来确定我对系统的理解是否与其他人相同。因为系统非常复杂,有许多不同种类的信息需要传递,所以 UML提供了许多不同的图专门表示不同种类的信息。

有利于精确

要认识到UML的价值,有一个简单的办法:回忆最近参加的几次设计评审。如果在某次评审中,某人在不使用UML等建模语言的情况下开始谈起自己的代码并描述它,几乎能够肯定他的发言将含混难懂,而且不必要地冗长。UML不仅仅是描述面向对象设计的上佳方法,它还使设计人员能够仔细考虑其设计中类之间的关系(因为必须将设计写下来)[12]

2.4 类图

基本的建模图

最基本的UML图是类图。它不仅描述了类,而且说明了类之间的关系。这些关系可能有以下这些类型。

当一个类是“一种”另一个类时:is-a(是一种/一个)关系。

当两个类之间存在关联时:

一个类“包含”另一个类:has-a(拥有一个)关系;

一个类“使用”另一个类:use-a(使用一个)关系;

一个类“创建”另一类。

这些类型还有一些变体。比如,说“什么东西包含另一个东西”时,我们可能是指:

被包含者是包含者的一部分(比如汽车中的发动机);

有一个集合,集合中东西可以独立存在(比如机场上的飞机)。

表示类信息的不同方法

第一种情况称为组合(composition),第二种情况称为聚集(aggrega-tion)[13]

图2-1说明了重要的几点。首先,矩形表示一个类。在UML中,我可以表示最多三个方面的类的信息:

类名;

类的数据成员;

类的方法(函数)。

表示类的信息有三种不同方式。

最左边的矩形只显示了类名。在不需要更详细信息时,可以使用类的这种表示形式。

中间的矩形显示了类名和类的方法。在本例中,Square类[14]有一个display方法。display(方法名)前的加号(+)表示此方法是公开的——也就是说,不属于此类的其他对象也可以调用。

最右边的矩形除显示了前面的信息(类名和类的方法)之外,还显示了类的数据成员。在本例中,数据成员length(它是double类型的)前的减号(−)表明这个数据成员的值是私有的,也就是说,除了它所属的对象外,它对其他对象都是不可见的。[15]

表示访问权限的UML记号

你可以控制类的数据成员和方法成员的可访问性,也可以用 UML标记所需要的每个成员的可访问性。大多数面向对象语言中都有如下三种最常见的可访问性。

公开——用一个加号(+)标记。意味着所有对象都可以访问这个数据或方法。

保护——用一个“井”号(#)标记。意味着只有该类及其所有派生类(包括其派生类的派生类)可以访问这个数据或方法。

私有——用减号(−)标记。意味着只有该类的方法可以访问这个数据或方法。(请注意:某些语言进一步将其限制为特定对象。)

表示关系的UML记号

表示关系的UML记号有如下四种:[16]

类图还可以表示关系

类图还可以表示不同类之间的关系。图2-2显示了Shape类和它的几个派生类之间的关系。

表示is-a关系

图2-2说明了几件事。首先,Shape类下面的箭头的意思是:指向Shape的那些类派生自Shape类。而且,Shape类的名字是用斜体表示的,说明它是一个抽象类。抽象类就是用来为其派生类定义接口而且存放这些派生类公共数据和方法的类。接口可以看作是没有公共数据和方法的抽象类——它只用来作为一种“为要实现它的那些类的方法进行定义”的方式而已。[17]

表示has-a关系

如前所述,有两种不同的has-a关系。一个对象可以拥有另一个对象,其中被包含的对象是包含对象的一部分——或者不是。在图2-3中,我表示出Airport“拥有”Aircraft。Aircraft并不是Airport的一部分,但仍然可以说Airport拥有Aircraft,这种关系称为聚集。

在此图中,我还表示了Aircraft要么是Jet(喷气式飞机),要么是 聚集Helicopter(直升飞机)。可以看出Aircraft类是一个抽象类或者接口[18],因为它的名字是用斜体表示的。也就是说,Airport可以拥有Jet或Helicopter,但它是以相同方式对待它们的(当作Aircraft)。Airport类右边的空心(未填充的)菱形表示聚集关系。

组合

另一种has-a关系是包含,被包含对象是包含对象的一部分,这种关系也称为组合。

组合和使用

图2-4显示了Car(轿车)拥有Tire(轮胎),后者是它的一部分(也就是说,Car由Tire和其他东西组成),这种has-a关系,称为组合关系(composition),用实心菱形表示。此图上还显示了Car使用了GasStation(加油站)类,这种使用关系用带箭头的虚线表示,也称依赖关系(dependency relationship)。

组合与聚集的异同

组合和聚集都有“一个对象包含一个或多个对象”的意思,但是,组合意味着“被包含对象是包含对象的一部分”,而聚集意味着被包含对象更像是一个集合。我们可以认为组合是一种非共享的关联,被包含对象的生存周期由包含对象控制。适当使用构造函数和析构函数在这里有助于对象的创建和销毁过程。

UML中的注释

在图2-5中有一个新记号:注释。含有“空心菱形表示聚集”信息的方框就是注释。注释记号看上去好像是右角折起的纸。经常能够看到注释通过一条线与特定的类连接起来,表示它只与此类有关。

表示另一个对象所拥有的东西的数量

类图表示的是类之间的关系,但是,对于组合和聚集来说,这两种关系更加关注该类型的具体对象。比如,Airport对象拥有Aircraft对象,但是更具体地说,是特定的机场拥有特定的飞机。于是问题出现了——“一个机场可以拥有多少架飞机呢?”这称为关系的重数(cardinality)。图2-6和图2-7说明了这一点。

重数

图2-6告诉我们,对于一个Airport对象,它可以拥有从0到任意数量(此处用星号表示,但有时候也可以用字母“n”)的Aircraft对象。Airport类旁的“0..1”意味着:对于一个 Aircraft 对象,它可以被 0个或1个 Airport对象包含。(0 表示它可以在空中飞行,不属于任何机场)。

重数续

图2-7告诉我们,对于一个Car对象,它可以拥有4个或5个Tire对象(有或没有备胎),轮胎则只能装在一辆轿车上。我曾听一些人说,如果未指定重数,就意味着只有一个对象,这种想法是不正确的。如果未指定重数,对于对象的数量不应该做任何假设。

虚线表示依赖

和前面一样,图2-7中显示的Car和GasStation之间的虚线表示两者之间存在依赖。UML 用带虚线的箭头表示两个模型元素之间的语义关系(意义)。

2.5 交互图

交互图

类图可以表示类之间的静态关系,换句话说,类图不能表示任何活动。虽然这非常有用,但有时候我需要表示这些类实例化的对象是如何实际地一起工作的。

表示对象间如何交互的UML图称为交互图(interaction diagram)。最常用的交互图是顺序图,如图2-8所示。

顺序图应该从顶到底地阅读,如下所述。

如何阅读顺序图

最上面的每个矩形都代表一个特定的对象。虽然许多矩形中有类名,但请注意在类名前有一个冒号。一些矩形还有其他名字——例如shape1:Square。

垂直线代表对象的生命线。糟糕的是,大多数UML绘图程序不支持这一点,只能绘制从顶到底的线,因此并不清楚对象实际上什么时候开始存在。

我用这些垂直线之间的水平线表示对象互相发送消息[19]

有时候返回值和/或对象会明确表示出来,而有时候只是表示它们要返回。

例如,在图2-8中,

在最上面可以看见Main向ShapeDB对象(这个对象还没有名字)发送了一个“获取形状集合”的消息。

在收到“获取形状集合”的请求之后,ShapeDB对象将:

实例化一个Collection对象;

实例化一个Square对象;

在集合中添加Square对象;

实例化一个Circle对象;

在集合中添加Circle对象;

将集合返回给调用例程(Main)。

其余操作也可以通过这种从顶到底的方式读图来了解,这种图称为顺序图(sequence diagram),因为它描述了操作的顺序。

“对象:类”记号

有些UML图中,需要用派生对象的类来表示该对象。可以通过用冒号连接二者来实现这一点。在图2-8中,我用 shape1:Square表示从Square类实例化的shape1对象。

2.6 小结

本章内容

UML 既能够充实设计,又能够用于设计的交流。不要太担心要“正确地”画图。要考虑的是什么方式最有利于交流设计中的概念。换句话说,

如果你认为有什么东西需要说,可以用注释来表达。

如果你对一个图标或符号不太确定,必须查手册才能确定其意义,还是加一条注释来解释。毕竟,其他人有可能也不清楚它的意义。

清晰为好。

当然,这也意味着你应该以规范的方式使用 UML——那样无法正常交流。在画图的时候,只考虑要传达的思想即可。

复习题

简答题

1.is-a关系和has-a关系之间的区别是什么?两种“关联”关系又是什么?

2.在类图中,类是用方框表示的,可以有三部分。请描述这三部分。

3.给出重数的定义。

4.顺序图的用途是什么?

阐述题

1.给出is-a关系和两种“关联”关系的例子。对这些例子:

(1) 在类图中画出;

(2) 在类图中显示重数。

2.图2-8是一个顺序图。此图中显示了多少步骤?显示了多少对象,都是哪些对象?

3.当对象互相交流时,为什么说“发送消息”比“调用操作”更合适?

观点与应用题

一个顺序图上应该显示多少步?

[1].McConnell S.,Code Complete:A Practical Handbook of Software Construction,Redmond: Microsoft Press,1993,p.81。(请注意,这些术语并不是McConnell发明的,发明者是Ed Yourdon 和 Constantine。我们只是碰巧更喜欢McConnell的定义而已。)

[2].McConnell S.,Code Complete:A Practical Handbook of Software Construction,Redmond: Microsoft Press,1993,p.81。(请注意,这些术语并不是McConnell发明的,发明者是Ed Yourdon 和 Constantine。我们只是碰巧更喜欢McConnell的定义而已。),第87页。

[3].应该指学校中类似于JavaOne的技术大会,有很多课程和讲座同时在不同地点开设。——译者注

[4].Fowler M.和Scott K.,UML Distilled:A Brief Guide to the Standard Object Modeling Language,Second Edition,Boston:Addison-Wesley,1999,pp.51-52。

[5].这里比较粗略地套用了Bertrand Meyer在Object-Oriented Software Construction(Upper Saddle River,N.J.: Prentice Hall,1997,p.331)中概述的“按约定设计”(design by contract)的概念。

[6].有几种语言中接口也能如此。本章中提到抽象类时,可以假定我编写的是抽象类或者接口。

[7].抽象类和接口之间有一点不同。接口只定义一组类能够做什么,而并不实现默认行为。

[8].不同的语言经常有其他类型的可访问性,但是,本质上都是这三种的变种而已。

[9].即不可见了。——译者注

[10]..NET语言亦然,包括C#、VB.NET和C++/CLI等。——译者注

[11].即可见性。——译者注

[12].有些敏捷方法专家相信,各种书面的文档都应该避免,除非绝对需要。当然,许多开发人员对UML的使用的确有些过分,而且所生成的文档实际上是阻碍而不是促进了交流。但是,只要正确地使用,UML还是能很好地促进交流的,即使在使用“结对编程(paired programming)”时,设计概念在概念层次描述通常也比在代码(即实现)层次描述更好。换句话说,应该努力同时做到“尽可能最简”和“尽可能最好”。

[13].Gamma、Helm、Johnson 和 Vlissides 的《设计模式》一书中将第一种情况称为“聚集”,而将第二种情况称为“组合”(《设计模式》一书中aggregation的确相当于本书中的组合概念,但是该书中的composition则是指对象组合,与继承相对,和本书中的聚集没有关系。——译者注)——正好与UML相反。但是,该书完成于UML标准最终定案之前,事实上书中所给出的定义是与UML一致的。这也说明了开发UML的动机。在UML出现之前已经有好几种各不相同的建模语言,每种都有自己的记号和术语。

[14].类名在文字中引用时,使用Courier字体表示。

[15].在一些编程语言中,同类型的对象可以相互共享私有数据。

[16].原书此段文字与“表示访问权限的UML记号”中的一段文字相同,估计是作者的失误。——译者注

[17].我知道自己两次使用接口一词,表示的是不同的含义。但是别为此骂我。我也希望对Java和C#的关键字——interface使用另一个名字呢。

[18].为了简明起见,我不再继续写“抽象类或者接口”了。往后我所称的“抽象类”,均可以视同为“抽象类或者接口”。

[19].当对象互相“交谈”时,我们称之为“发送消息”。你需要给一个对象发送请求,让它进行某种操作,而不是告诉其他对象做什么,其他对象会负责搞清楚如何去做。转移责任是面向对象程序设计基本原则之一。这与过程式程序设计完全不同,在后者情况下,你必须控制下一步做什么,因此可能“调用另一个对象的方法”或者“调用操作”。

相关图书

有限元基础与COMSOL案例分析
有限元基础与COMSOL案例分析
程序员的README
程序员的README
现代控制系统(第14版)
现代控制系统(第14版)
现代软件工程:如何高效构建软件
现代软件工程:如何高效构建软件
GitLab CI/CD 从入门到实战
GitLab CI/CD 从入门到实战
科学知识图谱:工具、方法与应用
科学知识图谱:工具、方法与应用

相关文章

相关课程