登入帳戶  | 訂單查詢  | 購物車/收銀台( 0 ) | 在線留言板  | 付款方式  | 聯絡我們  | 運費計算  | 幫助中心 |  加入書簽
會員登入 新註冊 | 新用戶登記
HOME新書上架暢銷書架好書推介特價區會員書架精選月讀2023年度TOP分類閱讀雜誌 香港/國際用戶
最新/最熱/最齊全的簡體書網 品種:超過100萬種書,正品正价,放心網購,悭钱省心 送貨:速遞 / EMS,時效:出貨後2-3日

2024年03月出版新書

2024年02月出版新書

2024年01月出版新書

2023年12月出版新書

2023年11月出版新書

2023年10月出版新書

2023年09月出版新書

2023年08月出版新書

2023年07月出版新書

2023年06月出版新書

2023年05月出版新書

2023年04月出版新書

2023年03月出版新書

2023年02月出版新書

『簡體書』松本行弘的程序世界

書城自編碼: 1779551
分類: 簡體書→大陸圖書→計算機/網絡程序設計
作者: [日]松本行弘
國際書號(ISBN): 9787115255075
出版社: 人民邮电出版社
出版日期: 2011-08-01
版次: 1 印次: 1
頁數/字數: 389/611000
書度/開本: 16开 釘裝: 平装

售價:NT$ 713

我要買

share:

** 我創建的書架 **
未登入.



新書推薦:
带献帝去旅行--历史书写的中古风景(论衡系列)
《 带献帝去旅行--历史书写的中古风景(论衡系列) 》

售價:NT$ 325.0
爱的能力:为什么我们既渴望爱,又害怕走进爱(第13版)
《 爱的能力:为什么我们既渴望爱,又害怕走进爱(第13版) 》

售價:NT$ 391.0
环艺设计手绘 景观 室内马克笔手绘效果图技法精解
《 环艺设计手绘 景观 室内马克笔手绘效果图技法精解 》

售價:NT$ 447.0
明清与李朝时代
《 明清与李朝时代 》

售價:NT$ 381.0
商业人像摄影
《 商业人像摄影 》

售價:NT$ 447.0
抗争表演
《 抗争表演 》

售價:NT$ 347.0
咏春八斩刀
《 咏春八斩刀 》

售價:NT$ 391.0
聊不完的艺术家:跨界设计师穆夏
《 聊不完的艺术家:跨界设计师穆夏 》

售價:NT$ 549.0

建議一齊購買:

+

NT$ 573
《 重构 改善既有代码的设计 》
+

NT$ 711
《 Ruby基础教程(第4版) 》
+

NT$ 405
《 程序员的呐喊 》
+

NT$ 735
《 代码的未来(Ruby之父剖析云计算、大数据时代下的技术) 》
+

NT$ 532
《 Ruby元编程(松本行弘作序推荐,改善代码结构一本通) 》
內容簡介:
本书是探索程序设计思想和方法的经典之作。作者从全局的角度,利用大量的程序示例及图表,深刻阐述了Ruby编程语言的设计理念,并以独特的视角对与编程相关的各种技术进行了考察。阅读本书不仅可以深入了解编程世界各个要素之间的关系,而且能够学到大师级的程序思考方法。
本书面向各层次程序设计人员和编程爱好者,也可以供相关技术人员参考。
關於作者:
松本行弘
Ruby语言的发明人,在1993年发布了Ruby语言的第一个版本,以后一直从事Ruby的设计与开发。2011年加入著名SaaS厂商Salesforce旗下PaaS公司Heroku,任首席Ruby架构师,致力于加快Ruby
Core的开发。他还是NaCI及乐天技术研究所的研究员。著有Ruby in a Nutshell和The Ruby
Programming Language
等书。他的博客地址为http:www.rubyist.net~matz。
目錄
第1章 我为什么开发Ruby 1
1.1 我为什么开发Ruby 2
1.1.1 编程语言的重要性 2
1.1.2 Ruby的原则 3
1.1.3 简洁性 4
1.1.4 扩展性 5
1.1.5 稳定性 6
1.1.6 一切皆因兴趣 7
第2章 面向对象 9
2.1 编程和面向对象的关系 10
2.1.1 颠倒的构造 10
2.1.2 主宰计算机的武器 11
2.1.3 怎样写程序 12
2.1.4 面向对象的编程方法 12
2.1.5 面向对象的难点 13
2.1.6 多态性 13
2.1.7 具体的程序 14
2.1.8 多态性的优点 15
2.2 数据抽象和继承 16
2.2.1 面向对象的历史 16
2.2.2 复杂性是面向对象的敌人 18
2.2.3 结构化编程 18
2.2.4 数据抽象化 19
2.2.5 雏形 21
2.2.6 找出相似的部分来继承 22
2.3 多重继承的缺点 23
2.3.1 为什么需要多重继承 23
2.3.2 多重继承和单一继承不可分离 24
2.3.3 goto语句和多重继承比较相似 25
2.3.4 解决多重继承的问题 25
2.3.5 静态语言和动态语言的区别 26
2.3.6 静态语言的特点 26
2.3.7 动态语言的特点 27
2.3.8 静态语言和动态语言的比较 27
2.3.9 继承的两种含义 28
2.3.10 接口的缺点 28
2.3.11 继承实现的方法 29
2.3.12 从多重继承变形而来的Mix-in 29
2.3.13 积极支持Mix-in的Ruby 30
2.4 两个误解 31
2.4.1 面向对象的编程 31
2.4.2 对象的模板=类 33
2.4.3 利用模块的手段=继承 33
2.4.4 多重继承不好吗 34
2.4.5 动态编程语言也需要多重继承 35
2.4.6 驯服多重继承的方法 35
2.4.7 Ruby中多重继承的实现方法 37
2.4.8 Java实现多重继承的方法 38
2.5 Duck Typing诞生之前 39
2.5.1 为什么需要类型 39
2.5.2 动态的类型是从Lisp中诞生的 40
2.5.3 动态类型在面向对象中发展起来了 41
2.5.4 动态类型和静态类型的邂逅 42
2.5.5 静态类型的优点 42
2.5.6 动态类型的优点 43
2.5.7 只关心行为的Duck Typing 44
2.5.8 避免明确的类型检查 45
2.5.9 克服动态类型的缺点 46
2.5.10 动态编程语言 46
2.6 元编程 46
2.6.1 元编程 46
2.6.2 反射 47
2.6.3 元编程的例子 48
2.6.4 使用反射功能 48
2.6.5 分布式Ruby的实现 49
2.6.6 数据库的应用 50
2.6.7 输出XML 51
2.6.8 元编程和小编程语言 51
2.6.9 声明的实现 52
2.6.10 上下文相关的实现 52
2.6.11 单位的实现 53
2.6.12 词汇的实现 53
2.6.13 层次数据的实现 54
2.6.14 适合DSL的语言,不适合DSL的语言 54
第3章 程序块 57
3.1 程序块的威力 58
3.1.1 把函数作为参数的高阶函数 58
3.1.2 C语言高阶函数的局限 59
3.1.3 可以保存外部环境的闭包 60
3.1.4 块的两种使用方法 60
3.1.5 最终来看,块到底是什么 61
3.1.6 块在循环处理中的应用 62
3.1.7 内部迭代器和外部迭代器 62
3.1.8 在排序和比较大小中的应用 63
3.1.9 用块保证程序的后处理 63
3.1.10 用块实现新的控制结构 64
3.1.11 在回调中使用块 65
3.1.12 块处理的特别理由 65
3.2 用块作循环 66
3.2.1 块是处理的集合 67
3.2.2 块应用范围的扩展 68
3.2.3 高阶函数和块的本质一样 69
3.2.4 用Enumerable来利用块 69
3.2.5 Enumerable的局限 74
3.3 精通集合的使用 77
3.3.1 使用Ruby的数组 77
3.3.2 修改指定范围的元素内容 78
3.3.3 Ruby中的哈希处理 78
3.3.4 支持循环的Enumerable 79
3.3.5 用于循环的each方法 81
3.3.6 使用inject、zip和grep 81
3.3.7 用来指定条件的select方法 82
3.3.8 排序与比较大小的方法 83
3.3.9 在类中包含includeEnumerable模块 84
3.3.10 List的内部包和块的区别 85
第4章 设计模式 89
4.1 设计模式1 90
4.1.1 设计模式的价值和意义 91
4.1.2 设计模式是程序抽象化的延伸 92
4.1.3 Ruby中的设计模式 92
4.1.4 Singleton模式 92
4.1.5 Proxy模式 94
4.1.6 Iterator模式 95
4.1.7 外部与内部,哪一个更好 96
4.1.8 内部迭代器的缺陷 97
4.1.9 外部迭代器的缺陷 98
4.2 设计模式2 98
4.2.1 模式与动态语言的关系 99
4.2.2 重复使用既存对象的Prototype模式 99
4.2.3 亲身体验Io语言 100
4.2.4 Ruby中的原型 101
4.2.5 编写抽象算法的Template Method模式 101
4.2.6 用Ruby来尝试TemplateMethod 102
4.2.7 动态语言与Template Method模式 104
4.2.8 避免高度依赖性的Observer模式 104
4.2.9 Observable模块 105
4.2.10 Observer模式与动态语言 107
4.3 设计模式3 107
4.3.1 软件开发的悲剧 108
4.3.2 开放-封闭原则 108
4.3.3 面向对象的情况 109
4.3.4 非面向对象的情况 110
4.3.5 OCP与Template Method模式 111
4.3.6 Observer模式 113
4.3.7 使用Strategy模式 114
4.3.8 Strategy模式与OCP 116
第5章 Ajax 119
5.1 Ajax和JavaScript前篇 120
5.1.1 通信及异步页面更新 120
5.1.2 技术要素之一:JavaScript 122
5.1.3 技术要素之二:XML 122
5.1.4 XML以外的数据表现形式 123
5.1.5 技术要素之三:DHTML 124
5.1.6 JavaScript技术基础 124
5.1.7 原型模式的面向对象编程语言 126
5.1.8 使用prototype.js库 127
5.1.9 prototype.js的功能 127
5.2 Ajax和JavaScript后篇 130
5.2.1 巧妙使用DHTML 131
5.2.2 获取document节点 132
5.2.3 获取和更新标签数据 133
5.2.4 设定事件处理程序 133
5.2.5 追加标签节点 135
5.2.6 本地HTML应用 135
5.2.7 和服务器间的通信 137
5.2.8 使用Prototype.js的优点 138
5.2.9 在服务器上保存数据 138
5.2.10 Web应用的脆弱性 140
5.2.11 使用JavaScript的感觉 141
第6章 Ruby on Rails 143
6.1 MVC和Ruby on Rails 144
6.1.1 模型、视图和控制的作用 144
6.1.2 用秒表的例子来学习MVC模式 145
6.1.3 生成视图和控制部分 147
6.1.4 GUI工具箱与MVC 148
6.1.5 同时使用工具箱和MVC 149
6.1.6 MVC的优缺点 151
6.1.7 Web应用中的MVC 152
6.2 开放类和猴子补丁 153
6.2.1 开放类 154
6.2.2 猴子补丁的目的 154
6.2.3 猴子补丁的技巧 155
6.2.4 灵活使用开放类的库 159
6.2.5 猴子补丁的几点问题 161
6.2.6 其他办法 162
6.2.7 Ruby on Rails和开放类 165
6.2.8 ActiveSupport带来的扩展 166
6.2.9 字节单位系列 168
6.2.10 复数形和序数 168
6.2.11 大规模开发和Ruby 169
6.2.12 信赖性模型 170
6.2.13 猴子补丁的未来 170
第7章 文字编码 173
7.1 文字编码的种类 174
7.1.1 早期的文字编码 174
7.1.2 纸带与文字表现 175
7.1.3 文字是什么 176
7.1.4 走向英语以外的语言欧洲篇 177
7.1.5 英语以外的语言亚洲篇 177
7.1.6 Unicode的问世 180
7.1.7 统一编码成16位的汉字统合 181
7.1.8 Unicode的两个问题 181
7.1.9 Unicode的文字集 182
7.1.10 文字表示的不确定性 183
7.1.11 Unicode的字符编码方式 183
7.2 程序中的文字处理 185
7.2.1 文字编码有多个意思 185
7.2.2 只能处理文字集中包含的文字 185
7.2.3 纷繁复杂的文字编码方式 186
7.2.4 影响力渐微的Shift_JIS与EUC-JP 186
7.2.5 Unicode有多种字符编码方式 186
7.2.6 为什么会发生乱码 188
7.2.7 字符编码方式错误 188
7.2.8 没有字体 189
7.2.9 变换为内部码时出错 189
7.2.10 发生不完全变换 189
7.2.11 文字集的不同 190
7.2.12 字节顺序错误 191
7.2.13 从编程语言的角度处理文字 191
7.2.14 以变换为前提的UCS方式 191
7.2.15 原封不动处理的CSI方式 192
7.2.16 使用UTF-16的Java 192
7.2.17 使用UTF-8的Perl 193
7.2.18 用UTF-16的Python 194
7.2.19 采用CSI方式的Ruby 1.8 194
7.2.20 强化了功能的Ruby 1.9 195
7.2.21 是UCS还是CSI 196
第8章 正则表达式 199
8.1 正则表达式基础 200
8.1.1 检索“像那样的东西” 200
8.1.2 正则表达式的语法 200
8.1.3 3个陷阱 203
8.1.4 正则表达式对象 204
8.1.5 选项 205
8.1.6 正则表达式匹配的方法 206
8.1.7 特殊变量 207
8.1.8 字符串与正则表达式 207
8.1.9 split的本质 207
8.1.10 字符串的扫描 208
8.1.11 置换 208
8.2 正则表达式的应用实例与“鬼车” 210
8.2.1 解析日志文件的方法 211
8.2.2 避免使用$的方法 213
8.2.3 从邮件中取出日期的方法 215
8.2.4 典型拼写错误的检索方法 216
8.2.5 Ruby 1.9的新功能“鬼车” 216
第9章 整数和浮点小数 219
9.1 深奥的整数世界 220
9.1.1 整数是有范围的 221
9.1.2 尝试位运算 222
9.1.3 操作特定的位 223
9.1.4 表示负数的办法 225
9.1.5 Ruby的整数 226
9.1.6 挑战公开密钥方式 227
9.2 扑朔迷离的浮点小数世界 228
9.2.1 计算机对小数的处理 229
9.2.2 固定小数点数不易使用 229
9.2.3 科学计数法也有问题 229
9.2.4 小数不能完全表示 230
9.2.5 有不能比较的时候 231
9.2.6 误差积累 232
9.2.7 不是数的特别“数” 232
9.2.8 计算误差有多种 233
9.2.9 误差导致的严重问题 235
9.2.10 BigDecimal是什么 236
9.2.11 能够表示分数的Rational类 236
第10章 高速执行和并行处理 239
10.1 让程序高速执行前篇 240
10.1.1 是不是越快越好 240
10.1.2 高速执行的乐趣与效率 240
10.1.3 以数据为基础作出判断 241
10.1.4 改善系统调用 241
10.1.5 数据可靠吗 243
10.1.6 只需改善瓶颈 243
10.1.7 profiler本身成了累赘 245
10.1.8 算法与数据结构 245
10.1.9 理解O记法 245
10.1.10 选择算法 246
10.1.11 调查算法的性能 246
10.1.12 高速执行的悲哀 247
10.1.13 性能优化的格言 248
10.2 让程序高速执行后篇 248
10.2.1 确认程序概要 249
10.2.2 发现瓶颈 250
10.2.3 使用更好的profiler 251
10.2.4 高速优化之一:削减对象 252
10.2.5 高速优化之二:利用立即值 254
10.2.6 高速优化之三:利用C语言 255
10.2.7 高速优化之四:采用合适的数据结构 256
10.2.8 全部以C语言计算 257
10.2.9 还存在其他技巧 257
10.3 并行编程 258
10.3.1 使用线程的理由 258
10.3.2 生成线程 259
10.3.3 线程的执行状态 260
10.3.4 传递值给线程的方法 261
10.3.5 信息共有所产生的问题 262
10.3.6 数据完整性的丧失 262
10.3.7 死锁 263
10.3.8 用锁来实现对资源的独占 264
10.3.9 二级互斥 265
10.3.10 用队列协调线程 265
10.3.11 锁模型与队列模型的比较 267
10.4 前景可期的并行编程技术,Actor 268
10.4.1 何谓Actor 268
10.4.2 操作Actor的3种处理系统 269
10.4.3 Erlang的程序 270
10.4.4 Pingpong处理的开始 270
10.4.5 启动pingpong程序 271
10.4.6 Erlang的错误处理 272
10.4.7 Erlang的使用场所 273
10.4.8 面向Ruby的库“Revactor” 273
10.4.9 Revactor的应用场合 274
10.4.10 另一个库Dramatis 275
第11章 程序安全性 279
11.1 程序的漏洞与攻击方法 280
11.1.1 4种软件漏洞 280
11.1.2 因权限被窃取而成为重大问题 281
11.1.3 安全问题的根源 281
11.1.4 “守护神”引起的问题 282
11.1.5 多 样化的攻击手段 282
11.1.6 缓冲区溢出 283
11.1.7 整数溢出 284
11.1.8 SQL注入 285
11.1.9 Shell注入 286
11.1.10 跨站点脚本攻击 287
11.1.11 跨站点伪造请求 288
11.1.12 社会工程 289
11.2 用异常进行错误处理 290
11.2.1 异常的历史 292
11.2.2 Java的受控异常 292
11.2.3 Icon的面向目标判断 293
11.2.4 Ruby的异常 294
11.2.5 异常发生 295
11.2.6 异常类 296
11.2.7 异常处理的设计方针 297
11.2.8 异常发生的设计原则 298
第12章 关于时间的处理 301
12.1 用程序处理时刻与时间 302
12.1.1 时差与时区 302
12.1.2 世界协调时间 302
12.1.3 夏令时DST 303
12.1.4 改历 304
12.1.5 日期与时间的类 305
12.1.6 2038年问题 308
12.1.7 DateTime类 309
12.1.8 Time与DateTime的相互变换 310
第13章 关于数据的持久化 313
13.1 持久化数据的方法 314
13.1.1 保存文本 314
13.1.2 变换成文本的Marshal 314
13.1.3 使用Marshal模块 315
13.1.4 复制有两种方式 316
13.1.5 仔细看Marshal的格式 316
13.1.6 不能保存的3类对象 317
13.1.7 制作面向对象数据库 318
13.1.8 试用PStore 318
13.1.9 变换为文本的YAML 320
13.1.10 用YAML制作数据库 321
13.2 对象的保存 322
13.2.1 高速的Object Prevalence 322
13.2.2 Object Prevalence的问题点 323
13.2.3 使用Madeleine 323
13.2.4 访问时刻信息 325
13.2.5 让Madeleine更容易使用 326
13.2.6 Madeleine的实用例Instiki 328
13.3 关于XML的考察 328
13.3.1 XML的祖先是SGML 329
13.3.2 XML是树结构的数据表现 329
13.3.3 优点在于纯文本 330
13.3.4 缺点在于冗长 331
13.3.5 不适合重视效率的处理 331
13.3.6 适合于信息交换的格式 332
13.3.7 XML的解析 332
13.3.8 XML处理库REXML 333
13.3.9 XML的代替 336
第14章 函数式编程 339
14.1 新范型——函数式编程 340
14.1.1 具有多种函数式性质的Lisp 341
14.1.2 彻底的函数式编程语言Haskell 342
14.1.3 延迟计算:不必要的处理就不做 343
14.1.4 灵活的“静态多态性”类型系统 344
14.1.5 近代函数式语言之父OCaml 345
14.1.6 强于并行计算的Erlang 345
14.1.7 用Ruby进行函数式编程 346
14.1.8 用枚举器来实现延迟计算 347
14.2 自动生成代码 348
14.2.1 在商业中利用Ruby 349
14.2.2 使用Ruby自动生成代码 350
14.2.3 消除重复代码 350
14.2.4 代码生成的应用 351
14.2.5 代码生成的效果 352
14.2.6 编写代码生成器 353
14.2.7 也可以使用XML 354
14.2.8 在EJB中使用代码生成 355
14.3 内存管理与垃圾收集 356
14.3.1 内存管理的困难 357
14.3.2 垃圾收集亮相之前 358
14.3.3 评价垃圾收集的两个指标 359
14.3.4 垃圾收集算法 360
14.3.5 引用计数方式 360
14.3.6 标记和扫除方式 361
14.3.7 标记和紧缩方式 362
14.3.8 复制方式 363
14.3.9 多种多样的垃圾收集算法 364
14.3.10 分代垃圾收集 364
14.3.11 保守垃圾收集 366
14.3.12 增量垃圾收集 366
14.3.13 并行垃圾收集 367
14.3.14 位图标记 367
14.4 用C语言来扩展Ruby 368
14.4.1 开发与执行速度的取舍 368
14.4.2 扩展库 369
14.4.3 看例题学习扩展模块 370
14.4.4 QDBM函数 372
14.4.5 初始化对象 373
14.4.6 实现方法 374
14.4.7 关于垃圾收集的注意事项 376
14.4.8 其他的Ruby API 376
14.4.9 扩展库的编译 376
14.4.10 扩展库以外的工具 377
14.5 为什么要开源 380
14.5.1 自由软件的思想 380
14.5.2 自由软件的历史 381
14.5.3 Emacs事件的发生 381
14.5.4 开源的诞生 382
14.5.5 OSS许可证 383
14.5.6 开源的背景 385
14.5.7 企业关注开源的理由 386
14.5.8 Ruby与开源 386
14.5.9 选择许可证的方法 387
內容試閱
1.1我为什么开发Ruby
Ruby是起源于日本的编程语言。近年来,特别是因为其在Web开发方面的效率很高,Ruby引起了全世界的关注,它的应用范围也扩展到了很多企业领域。
作为一门编程语言,Ruby正在被越来越多的人所了解,而作为一介工程师的我,松本行弘,刚开始的时候并没有想过“让全世界的人都来用它”或者“这下子可以大赚一笔了”,一个仅仅是从兴趣开始的项目却在不知不觉中发展成了如今的样子。
当然了,那时开发Ruby并不是我的本职工作,纯属个人兴趣,我是把它作为一个自由软件来开发的。但是世事弄人,现在开发Ruby竟然变成我的本职工作了,想想也有些不可思议。
“你为什么开发Ruby?”每当有人这样问我的时候,我认为最合适的回答应该就像Linux的开发者Linus
Torvalds对“为什么开发Linux”的回答一样吧——
“因为它给我带来了快乐。”
当我还是一个高中生,刚刚开始学习编程的时候,不知何故,就对编程语言产生了兴趣。
周围很多喜欢计算机的人,有的是“想开发游戏”,有的是“想用它来做计算”,等等,都是“想用计算机来做些什么”。而我呢,则想弄明白“要用什么编程语言来开发”、“用什么语言开发更快乐”。
高中的时候,我自己并不具备开发一种编程语言所必需的技术和知识,而且当时也没有计算机。但是,我看了很多编程语言类的书籍和杂志,知道了“还有像Lisp这样优秀的编程语言”、“Smalltalk是做面向对象设计的”,等等,在这些方面我很着迷。上大学时就自然而然地选修了计算机语言专业。10年后,我通过开发Ruby实现了自己的梦想。
从1993年开始开发Ruby到现在已经过去16年了。在这么久的时间里,我从未因为设计Ruby而感到厌烦。开发编程语言真是一件非常有意思的事情。
1.1.1 编程语言的重要性
为什么会这么喜欢编程语言?我自己也说不清。至少,我知道编程语言是非常重要的。
最根本的理由是:语言体现了人类思考的本质。在地球上,没有任何超越人类智慧的生物,也只有人类能够使用语言。所以,正是因为语言,才造成了人类和别的生物的区别;正是因为语言,人和人之间才能传递知识和交流思想,才能做深入的思考。如果没有了语言,人类和别的动物也就不会有太大的区别了。
在语言学领域里,有一个Sapir-Whirf假说,认为语言可以影响说话者的思想。也就是说,语言的不同,造成了思想的不同。人类的自然语言是不是像这个假说一样,我不是很清楚,但是我觉得计算机语言很符合这个假说。也就是说,程序员由于使用的编程语言不同,他的思考方法和编写出来的代码都会受到编程语言的很大影响。
也可以这么说,如果我们选择了好的编程语言,那么成为好程序员的可能性就会大很多。
20年来一直被奉为名著的《人月神话》的作者Frederick P.
Brooks说过:一个程序员,不管他使用什么编程语言,他在一定时间里编写的程序行数是一定的。如果真是这样,一个程序员一天可以写500行程序,那么不论他用汇编、C,还是Ruby,他一天都应该可以写500行程序。
但是,汇编的500行程序和Ruby的500行程序所能做的事情是有天壤之别的。程序员根据所选择编程语言的不同,他的开发效率就会有十倍、百倍甚至上千倍的差别。
由于价格降低、性能提高,计算机已经很普及了。现在基本上各个领域都使用了计算机,但如果没有软件,那么计算机这个盒子恐怕一点用都没有了。而软件开发,就要求能够用更少的成本、更短的时间,开发出更多的软件。
需要开发的软件越来越多,开发成本却有限,所以对于开发效率的要求就很高。编程语言就成了解决这个矛盾的重要工具。
1.1.2 Ruby的原则
Ruby本来是我因兴趣而开发的。因为对多种编程语言都很感兴趣,我广泛对比了各种编程语言,哪些特性好,哪些特性没什么用,等等,通过一一进行比较、选择,最终把一些好的特性吸纳进了Ruby编程语言之中。
如果什么特性都不假思索地吸纳,那么这种编程语言只会变成以往编程语言的翻版,从而失去了它作为一种新编程语言的存在价值。
编程语言的设计是很困难的,需要仔细斟酌。值得高兴的是,Ruby的设计很成功,很多人都对Ruby给出了很好的评价。
那么,Ruby编程语言的设计原则是什么呢?
Ruby编程语言的设计目标是,让作为语言设计者的我能够轻松编程,进而提高开发效率。
根据这个目标,我制定了以下3个设计原则。
? 简洁性
? 扩展性
? 稳定性
关于这些原则,下面分别加以说明。
1.1.3 简洁性
以Lisp编程语言为基础而开发的商业软件Viaweb被Yahoo收购后,Viaweb的作者Paul
Graham也成了大富豪。最近他又成了知名的技术专栏作家,写了一篇文章就叫“简洁就是力量” 。
他还撰写了很多倡导Lisp编程语言的文章。在这些文章中他提到,编程语言在这半个世纪以来是向着简洁化的方向发展的,从程序的简洁程度就可以看出一门编程语言本身的能力。上面提到的Brooks也持同样的观点。
随着编程语言的演进,程序员已经可以更简单、更抽象地编程了,这是很大的进步。另外随着计算机性能的提高,以前在编程语言里实现不了的功能,现在也可以做到了。
面向对象编程就是这样的例子。面向对象的思想只是把数据和方法看作一个整体,当作对象来处理,并没有解决以前解决不了的问题。
用面向对象记述的算法也一定可以用非面向对象的方法来实现。而且,面向对象的方法并没有实现任何新的东西,却要在运行时判定要调用的方法,倾向于增大程序的运行开销。即使是实现同样的算法,面向对象的程序往往更慢,过去计算机的执行速度不够快,很难允许像这样的“浪费”。
而现在,由于计算机性能大大提高,只要可以提高软件开发效率,浪费一些计算机资源也无所谓了。
再举一些例子。比如内存管理,不用的内存现在可用垃圾收集器自动释放,而不用程序员自己去释放了。变量和表达式的类型检查,在执行时已经可以自动检查,而不用在编译时检查了。
我们看一个关于斐波那契(Fibonacci)数的例子。图1-1所示为用Java程序来计算斐波那契数。算法有很多种,我们用最常用的递归算法来实现。
图1-2所示为完全一样的实现方法,它是用Ruby编程语言写的,算法完全一样。和Java程序相比,可以看到构造完全一样,但是程序更简洁。Ruby不进行明确的数据类型定义,不必要的声明都可以省略。所以,程序就非常简洁了。
算法的教科书总是用模拟语言来描述算法。如果像这样用实际的编程语言来描述算法,那么像类型定义这样的非实质代码就会占很多行,让人不能专心于算法。
如果可以把模拟编程语言中非实质的东西去掉,只保留描述算法的部分就直接运行,那么这种编程语言不就是最好的吗?Ruby的目标就是成为开发效率高、“能直接运行的模拟编程语言”。
1.1.4 扩展性
下一个设计原则是“扩展性”。编程语言作为软件开发工具,其最大的特征就是对要实现的功能事先没有限制。“如果想做就可以做到”,这听起来像小孩子说的话,但在编程语言的世界里,真的就是这么一回事。不管在什么领域,做什么处理,只要用一种编程语言编写出了程序,我们就可以说这种编程语言适用于这一领域。而且,涉及领域之广会远远超出我们当初的预想。
1999年,关于Ruby的第一本书《面向对象脚本语言Ruby》出版的时候,我在里面写道,“Ruby不适合的领域”包括“以数值计算为主的程序”和“数万行的大型程序”。
但是几年后,规模达几万行、几十万行的Ruby程序被开发出来了。气象数据分析,乃至生物领域中也用到了Ruby。现在,美国国家海洋和大气管理局(NOAA,National
Oceanic and Atmospheric Administration)、美国国家航空和航天局(NASA,National
Aeronautics and Space Administration)也在不同的系统中运用了Ruby。
情况就是这样,编程语言开发者事先并不知道这种编程语言会用来开发什么,会在哪些领域中应用。所以,编程语言的扩展性非常重要。
实现扩展性的一个重要方法是抽象化。抽象化是指把数据和要做的处理都封装起来,就像一个黑盒子,我们不知道它的内部是怎么实现的,但是可以用它。
以前的编程语言在抽象化方面是很弱的,要做什么处理首先要了解很多编程语言的细节。而很多面向对象或者函数式的现代编程语言,都在抽象化方面做得很好。
Ruby也不例外。Ruby从刚开始设计时就用了面向对象的设计方法,数据和处理的抽象化提高了它的开发效率。我在1993年设计Ruby时,在脚本编程语言中采用面向对象思想的还很少,用类库方式来提供编程语言的就更少了。所以现在Ruby的成功,说明当时采用面向对象方法的判断是正确的。
Ruby的扩展性不仅仅体现在这些方面。
比如Ruby以块这种明白易懂的形式给程序员提供了相当于Lisp高阶函数的特性,使“普通的程序员”也能够通过自定义来实现控制结构的高阶函数扩展。又比如已有类的扩展特性,虽然有一定的危险性,但是程序却可以非常灵活地扩展。关于这些面向对象、程序块、类扩展特性的内容,后面的章节还会详细介绍。
这些特性的共同点是,它们都表明了编程语言让程序员最大限度地获得了扩展能力。编程语言不是从安全性角度考虑以减少程序员犯错误,而是在程序员自己负责的前提下为他提供最大限度发挥能力的灵活性。我作为Ruby的设计者,也是Ruby的最初用户,从这种设计的结果可以看出,Ruby看重的不是明哲保身,而是如何最大限度地发挥程序员自身的能力。
关于扩展性,有一点是不能忽视的,即“不要因为想当然而加入无谓的限制”。比如说,刚开始开发Unicode时,开发者想当然地认为16位(65
535个字符)就足够容纳世界上所有的文字了;同样,Y2K问题也是因为想当然地认为用2位数表示日期就够了才导致的。从某种角度说,编程的历史就是因为想当然而失败的历史。而Ruby对整数范围不做任何限定,尽最大努力排除“想当然”。
1.1.5 稳定性
虽然Ruby非常重视扩展性,但是有一个特性,尽管明知道它能带来巨大的扩展性,我却一直将其拒之门外。那就是宏,特别Lisp风格的宏。
宏可以替换掉原有的程序,给原有的程序加入新的功能。如果有了宏,不管是控制结构,还是赋值,都可以随心所欲地进行扩展。事实上,Lisp编程语言提供的控制结构很大一部分都是用宏来定义的。
所谓Lisp流,其语言核心部分仅仅提供极为有限的特性和构造,其余的控制结构都是在编译时通过用宏来组装其核心特性来实现的。这也就意味着,由于有了这种无与伦比的扩展性,只要掌握了Lisp基本语法S式(从本质上讲就是括号表达式),就可以开发出千奇百怪的语言。Common
Lisp的读取宏提供了在读取S式的同时进行语法变换的功能,这就在实际上摆脱了S式的束缚,任何语法的语言都可以用Lisp来实现。
那么,我为什么拒绝在Ruby中引入Lisp那样的宏呢?这是因为,如果在编程语言中引入宏的话,活用宏的程序就会像是用完全不同的专用编程语言写出来的一样。比如说Lisp就经常有这样的现象,活用宏编写的程序A和程序B,只有很少一部分是共通的,从语法到词汇都各不相同,完全像是用不同的编程语言写的。
对程序员来说,程序的开发效率固然很重要,但是写出的程序是否具有很高的可读性也非常重要。从整体来看,程序员读程序的时间可能比写程序的时间还长。读程序包括为理解程序的功能去读,或者是为维护程序去读,或者是为调试程序去读。
编程语言的语法是解读程序的路标。也就是说,我们可以不用追究程序或库提供的类和方法的详细功能,但是,“这里调用了函数”、“这里有判断分支”等基本的“常识”在我们读程序时很重要。
可是一旦引入了宏定义,这一常识就不再适用了。看起来像是方法调用,而实际上可能是控制结构,也可能是赋值,也可能有非常严重的副作用,这就需要我们去查阅每个函数和方法的文档,解读程序就会变得相当困难。
当然了,我知道世界上有很多Lisp程序员并不受此之累,他们正是通过面向特定程序定制语言而最大限度地提高了开发效率。不过在我个人看来,他们只是极少数的一部分程序员。
我相信,作为在世界上广泛使用的编程语言,应该有稳定的语法,不能像随风飘荡的灯芯那样闪烁不定。
1.1.6 一切皆因兴趣
当然,Ruby不是世界上唯一的编程语言,也不能说它是最好的编程语言。各种各样的编程语言可以在不同的领域中应用,各有所长。我自己以及其他Ruby程序员,用Ruby开发效率很高,所以觉得Ruby“最为得心应手”。当然,用惯了Python或者Lisp的程序员,也会觉得那些编程语言是最好的。
不管怎么说,编程语言存在的目的是让人用它来开发程序,并且尽量能提高开发效率。这样的话,才能让人在开发中体会到编程的乐趣。
我在海外讲演的时候,和很多人交流过使用Ruby的感想,比较有代表性的是:“用Ruby开发很快乐,谢谢!”
是啊,程序开发本来就是一件很快乐、很刺激和很有创造性的事情。想起中学的时候,用功能不强的BASIC编程语言开发,当时也是很快乐的。当然,工作中会有很多的限制和困难,编程也并不都是一直快乐的,这也是世之常情。
Ruby能够提供很高的开发效率,让我们在工作中摆脱很多困难和烦恼,这也是我开发Ruby的目的之一吧。



4.1设计模式

设计模式是个编程术语,它是指设计上经常反复使用的模式。这个词本来用于建筑界,表示各种各样的建筑物、街道的设计上共通的创意及构成的组合。即使在建筑界,这个词也是近些年来才开始使用的。设计模式这一思想好像起源于Christopher
Alexander的The Timeless Way of Building
(牛津大学出版社,1979)一书。
通常,建筑物的设计各不相同,另外加上用途、建筑条件等各种制约因素,一个设计是不能一成不变地套用到别的建筑物上的。建筑设计师只是通过重用积累的设计模式,试图缩短设计所花费的时间。
话说回到软件上来,重用的手法在软件行业比在建筑界中得到了更广泛的使用。一般是通过库的形式,各种软件共有处理过程、数据结构以及类等。实际上,Linux等操作系统提供了数不清的各种库。
但是,只有库还不能达到充分地共有。软件设计中有些东西虽然不能以库的形式来把它独立出来,但是多个软件共通利用的东西是确实存在的。这样的东西只能称之为固定形
式——模式。
比如,让我们来看一个最简单的模式吧。
for int i=0; i
...
}
这是称之为for循环的固定形式。在索引变量i从0到len变化的过程中进行相应的处理。有C、C++或Java等编程经验的人,同样的代码恐怕见过成百上千遍了。但是,这个简单的处理并不能以库的形式来共有。这不是库,而应该称之为模式。
在软件设计中像这样的模式数不胜数。不过,设计人员自己很少能够意识到模式的存在,一般是在积累了很多经验之后,才几乎在无意识之中利用模式提高了软件开发的效率。
Erich
Gamma与几位合作者一起,精选了软件设计中,特别是面向对象软件设计中反复出现的各种模式,比照着Alexander提倡的建筑上的概念,称之为“设计模式”。他们从自己及周围的开发经验中把模式总结出来并进行分类,出版了《设计模式》一书。《设计模式》中介绍了表4-1中列出的23种模式。
表4-1 《设计模式》中介绍的23种设计模式
模 式 名 内容
Abstract Factory(抽象工厂) 用可配置的方法生成有关的对象群
Adapter(适配器) 变换对象的接口
Bridge(桥接) 分离类之间的实现
Builder(生成器) 分离复杂对象的生成过程
Chain of Responsibility(职责链) 用多个对象来处理请求
Command(命令) 把请求封装成对象
Composite(组合) 用树结构来构成对象
Decorator(装饰) 给对象动态增加新的功能
Facade(外观) 隐藏子系统的详细内容,提供统一的接口
Factory Method(工厂方法) 在父类只定义生成对象的接口,具体的生成过程由派生类来实现
Flyweight(享元) 以共有的方式提高大量小对象的实现效率
Interpreter(解释器) 语言解释器
Iterator(迭代器) 提供按顺序访问一组对象的方法
Mediator(中介者) 封装对象之间的相互作用
Memento(备忘录) 记录对象的内部状态
Observer(观察者) 把对象的状态变更通知给其他对象
Prototype(原型) 提供生成对象的原型
Proxy(代理) 提供控制对象访问的代理(容器)
Singleton(单件) 用来保证某个类的实例只有一个
State(状态) 把对象的内部状态独立出来,封装状态变化
Strategy(策略) 封装算法,使之具有可变换性
Template Method(模板方法) 父类定义框架,派生类具体实现其中一部分
Visitor(访问者) 对集合的元素进行操作
4.1.1 设计模式的价值和意义
Gamma他们并没有发现新的模式。总结出来的23种设计模式也都是软件开发中早就存在并反复使用的模式,因此并不能说是Gamma他们的首创。但即使是这样,设计模式也还是得到了大家的关注。这到底是为什么呢?
首先,它明确了各种模式的效果和适用状况,给众多模式命名并进行分类,这本身就是非常有意义的。如果没有名字的话,即使使用了模式,程序员也大都没有明确意识到使用了模式。因为意识不到应该使用的模式,就会错过最合适的设计。
但是,他们最大的功绩并不在于把设计模式进行分类,而是明确了“软件中可以分类的设计模式”的存在。有了这样的认识之后,设计模式就不仅限于书中介绍的23个,人们开始发现和定义更多设计模式。
设计模式有了名字,人们就可以认识到它的存在。对于没有名字的东西,人们几乎不可能认识到它的存在,并对之进行讨论。这种不能用语言表达的知识我们称之为内隐知识。给这些在软件中经常反复出现的模式命名,使得这些本来只有经验丰富的程序员才能认识到的软件设计模式能被广泛认识和讨论。这样的知识称为形式知识。把迄今为止普通人难于学习和吸收的内隐知识变成形式知识的功绩是无与伦比的。
4.1.2 设计模式是程序抽象化的延伸
从软件设计进化的观点来看设计模式的话,可以把设计模式看成是软件抽象化的新工具。
到目前为止,把共通处理进行抽象化,结果产生了例程,把数据构造也包括在一起进行抽象化,结果产生了抽象数据类型,把抽象数据类型的共通部分再进行抽象,因而产生了继承这一工具。就像这样,软件设计总是在不断地导入新工具,以实现更高度的抽象化。
继承是把单一类的共通部分抽象化,以达到再利用的目的,但是,面向对象的系统很少是由单一的类来实现的,几乎都是由很多类组合构成的。在这种类的组合中,就会有一些大同小异的模式出现在各种不同的系统中。这些模式是无法用类库来抽象的,为达到再利用的目的,设计模式这种形式是最有效果的。
有了设计模式这种方法,这些用类库所实现不了的类构成的模式就可以在各种各样的状况下得到应用。模式的分类是个很抽象的概念,的确是比单纯的例程或类库要难得多,但设计模式有可能带来极高的生产效率,这是其他方法所不能达到的。
类设计本质上是非常困难的,构思出来最优的一组类来达到目的,这件事并不是随便谁都能做到的。但是,一旦有了设计模式,只要把过去优秀的人们考虑出来的模式拿来应用一下,谁都可以做出优秀的设计。
4.1.3 Ruby中的设计模式
《设计模式》一书是用C++和Smalltalk介绍模式实例的。看了那些例子,大家都会感觉到,绝大多数的模式用Smalltalk实现起来非常简单。这是为什么呢?
因为Smalltalk没有静态类型,所以也就不需要匹配类型的模板等机制,也不需要仅仅为满足类型要求而进行继承,这就是Smalltalk用起来很简单的原因。而且,由于语言本身的动态性质,有些模式根本不必要以模式的形式来抽象出来就可以得到简洁的实现,这也是Smalltalk显得简单的原因之一。
Ruby在很多方面很像Smalltalk,实现设计模式也毫无困难。一般说来,用Ruby来实现设计模式的场合,要比C++简洁得多,有些模式用现成的库就足以表现。
下面以Ruby为中心,让我们来看几个在实际设计中应用设计模式的例子。
4.1.4 Singleton模式
首先,让我们来看一下最简单的一个设计模式,Singleton(单件)模式。Singleton模式用来保证某个类的实例只有一个。
为什么需要Singleton模式呢?比如作为其他对象的雏形而存在的对象(用于Prototype模式),以及系统全体只存在唯一一个的对象等,都要用到Singleton模式。
用Ruby实现Singleton模式的方法有几个,让我们按顺序来逐一说明。
使用singleton库的方法
Ruby已经以库的形式实现了Singleton模式。如图4-1所示,使用singleton库的话,在任意的类里只要包含(include)上Singleton模块,那个类就变成了Singleton模式的对象。
要想取得Singleton模式的类的对象,像图4-1最后一行那样,使用该类的instance方法。如果该类对象还没有生成,instance方法会生成该类对象并返回。如果该类对象已经生成,instance方法就返回既有对象。
使用类或模块
C++和Java是不能把类作为对象来使用的,与之不同的是,Smalltalk或Ruby能把类也作为对象来处理。因此,在类或模块里定义一个方法就可以实现Singleton模式(参见图4-2)。
require ''singleton''
class PrintSpooler
include Singleton
...
end

PrintSpooler.instance.spooldocument class PrintSpooler
def PrintSpooler::spooldoc
...
end
end

PrintSpooler.spooldocument
图4-1 使用singleton库的Ruby代码 图4-2 利用类定义来实现Singleton模式的代码
把一般的对象作为Singleton来使用
为了把一个类的对象限制成只有一个,并不一定需要对对象的一般生成方法加以限制。我们可以生成一个一般的对象,然后遵守绅士协定,不要再生成其他更多个对象,也就行了(参见图4-3)。
使用对象和特异方法
其实还有不用类就可以实现的方法。Ruby可以在对象生成之后再增加新的方法,这样我们就可以生成一个Object类的对象,然后给它追加必要的功能(参见图4-4)。
class PrintSpooler
def spooldoc
...
end
end

# 把唯一的对象赋值给一个固定变量
Spooler = PrintSpooler.new
Spooler.spooldocument Spooler = Object.new
def Spooler.spooldoc
...
end
Spooler.spooldocument
图4-3 在编程上下点工夫来实现Singleton模式 图4-4 利用特殊方法来实现Singleton模式的代码
这种使用特异方法的办法是很符合Ruby特征的。Ruby自身的main(最高层的self)及ARGF(虚拟文件,用来代表参数所指定的文件)等也都是用这种方法实现的。
4.1.5 Proxy模式
Proxy(代理)模式是为某个对象提供代理对象的模式。为什么需要Proxy模式呢?
假设有个生成代价非常大的对象。如果在还不知道是否真正需要该对象的时候就事先生成它的话,可能会带来很大的浪费。但话虽这么说,不生成对象的话什么事也做不了。这时候代理对象就有用武之地了。
比如字处理软件,它利用Proxy对象来处理嵌入图像,把嵌入图像的生成处理延迟到需要表示的瞬间才来进行。
Ruby的库中也有使用Proxy模式的。比如tempfile库,它不用指定文件名就可以生成临时的工作文件(参见图4-5)。
Tempfile类与实际负责文件输出的IO类没有继承关系,它的有关输入、输出处理的方法都通过Proxy转送到实际的IO类对象。因此,通过使用Tempfile类的对象,在任何有必要的时候也都可以使用相关的IO对象。
Proxy模式也可以用Ruby的库来实现。使用delegate库就可以了。delegate是委托的意思。Tempfile类也是用delegate库来实现的(参见图4-6)。
# 生成Template
f = Tempfile.new"foo"
# 往Tempfile 输出
f.print"foo\n"
f.close
# 再打开
f.open
# 读取数据
p f.gets # = "foo\n" require ''delegate''

# 生成obj的代理对象
proxy = SimpleDelegator.newobj

# 通过proxy来调用obj的some_method
Proxy. some_method
图4-5 采用Proxy模式的tempfile库的使用示例 图4-6 使用delegate库来实现Proxy模式的例子
看一下就知道,delegate库的源代码是相当复杂的,但基本上只是把被调用的方法都转送到本来的对象那里去。这里使用的是Ruby的method_missing方法。
Ruby中对对象A调用它所不知道的方法的时候,A的method_
missing方法就会被调用。传递给method_missing的参数是在原来调用方法的参数之前加上不存在的方法名。利用这一框架就可以很简单地实现Proxy模式(参见图4-7)。
怎么样,真的是非常简单吧。但是,这种实现方式也有不灵光的时候。Proxy类固有的方法被调用的时候,是不会转送到method_missing方法的。也就是说,Proxy类的父类Object类的方法是转送不了的。
如果这样的情况也要对应的话,就会稍微麻烦一些。实际上,delegate库除去空行和注释以外还长达114行。比图4-7要复杂得多。
delegate库使用起来虽然很简单,但方法转送的对象仅限于既有的对象。因此,在最开始举的字处理软件例子中,要想达到延迟图像生成的目的,直接使用delegate是不行的。我们可以像图4-8那样,从Delegator派生一个子类(ImageProxy)来达到这一目的。
__getobj__方法是Delegator对象取得方法转送对象的方法。通过重写这个方法,ImageProxy会在实际访问图像对象的时候才来生成图像对象。C++会用operator-或者operator*来代替__getobj__。
4.1.6 Iterator模式
Iterator(迭代器)模式提供按顺序访问集合对象中各元素的方法。即使不知道对象的内部构造,也可以按顺序访问其中的每个元素。
Iterator模式是为集合对象另外准备用来控制循环处理的对象,就像C++或Java一样。我们称这个循环控制对象为Iterator,也称为光标。
图4-9是Iterator模式的类构成图。调用集合对象(图4-9的Iteratable)的Create-
Iterator方法,就会返回自己对应的Iterator对象。Iterator对象会记住现在所指向的Iteratable元素,调用Next方法可以返回集合的下一个元素。要想知道集合中是否还有别的元素,可以调用IsDone方法来确认。图4-10是利用Iterator模式的程序示例。Iterator模式实现的是所谓外部迭代器的循环控制抽象化。
IteratableArray array = CreateArray;
Iterator it = array.CreateIterator;
for it.First; !it.IsDone; it.Next{
System.out.printlnStringit.CurrentItem;
}
图4-9 Java版Iterator模式的类构成图 图4-10 Java版外部迭代器的用法
而Ruby是用块来对集合的各元素进行循环处理的。作为设计模式,使用块进行循环的抽象化属于Visitor(访问者)模式。但因为语言本身就支持这样的循环,所以也就不需要Iterator这样的对象了。这实在是太基本的东西了,也许都不应该称之为设计模式了。
4.1.7 外部与内部,哪一个更好
比较外部迭代器和内部迭代器,很难说哪一个更好。它们都有方便的一面,也都有不方便的另一面。比如,在没有闭包的语言中,要使用内部迭代器的话,就不能用块来实现,而是要传递给它函数指针,而且如果需要传递数据的话,就必须以参数的形式从外部明确传递过来,程序变得非常麻烦。
请看图4-11。这是以Ruby和C为例编写的对哈希表进行循环的内部迭代器。Ruby的内部迭代器是用块来实现的,代码看起来非常自然。但C是用函数指针来实现的,就比较难以理解。C语言没有闭包,循环处理所需要的数据都要以参数的形式明确地从外面传递进去。说实话真是麻烦。如果把图4-11中省略的循环处理的实现部分也考虑进来的话,两者的差别是一目了然的。在没有闭包的语言中,实现内部迭代器是很不现实的。
# 用Ruby对哈希表进行循环处理
h = Hash.new...
h.each {|k,v| ...}

用C对哈希表进行循环处理
static int
each_funcst_data_t key, st_data_t value, st_data_t arg {
...
return ST_CONTINUE;
}
int main {
h = st_init_table...
st_foreachh, each_func, arg
}
图4-11 C和Ruby的内部迭代器
因此,没有闭包的C++或者Java语言通常使用另外准备的外部迭代器对象来实现循环控
制。用Java来实现Iterator模式的程序大约80行左右。图4-12仅是迭代器对象的实现部分的代码。程序变得如此长的原因主要是要解决类型的匹配问题。在Ruby中,对Iterator模式的类构成而言,当然是内部迭代器比较方便,但这并不表示不能使用外部迭代器。把刚才的Java版程序移植成Ruby,程序还不到50行。图4-13是对应于图4-12的Ruby程序,Ruby外部迭代器的使用方法如图4-14所示。
与Java版相比,你觉得怎么样?我喜欢更清楚易懂的Ruby版。

abstract class Iterator {
abstract public void First;
abstract public void Next;
abstract public boolean IsDone;
abstract public Object CurrentItem;
}

class ArrayIterator extends Iterator {
private IteratableArray _array = null;
private int _current = -1;
public ArrayIteratorIteratableArray array {
_array = array;
_current = 0;
}
public void First {
_current = 0;
}
public void Next {
++_current;
}
public boolean IsDone {
return _current = _array.Size;
}
public Object CurrentItem {
return array.Get_current;
}
} class ArrayIterator
def initializearray
@array = array
@current = 0
end
def first
@current = 0
end
def next
@current += 1
end
def is_done
return @current = @array.size
end
def current_item
return @array.get@current
end
end
图4-12 Java迭代器对象的例子 图4-13 Ruby外部迭代器的例子
4.1.8 内部迭代器的缺陷
在有闭包的语言中,内部迭代器具有容易理解、容易实现以及自然封装等许多非常可取的性质。
但是,内部迭代器并不是完美无缺的。内部迭代器的缺陷是,由于不能同时进行多个循环,也就无法实现按顺序比较两个集合元素的处理。容易使用的东西也有它没有任何使用价值的领域。
比如,如果要从多个数组中把一个个元素取出来进行排列的话,就不能使用内部迭代器,而要写成图4-15的样子。
但是Ruby也在改善之中。在1.8.7版本以后,几乎所有对块进行循环的方法,在没有块的时候,会返回像外部迭代器一样动作的Enumerator。有了它,不用再特意准备外部迭代器,就可以把它作为外部迭代器来使用。
图4-16使用Enumerator实现了图4-15同样的处理。请注意,它比使用外部迭代器(图4-15的后半部分)的程序还要简单。
# 内部迭代器无法实现多个数组要素的并列处理
a = [1,2,3]
b = [9,8,7]
i=0
result = []
while i3
result.pusha[i]
result.pushb[i]
end
result # =[1,9,2,8,3,7]

# 用外部迭代器就很简单
ia = a.create_iterator;
ib = b.create_iterator;
ia.first; ib.first
until ia.is_done or ib.is_done
result.pushia.next
result.pushib.next
end # 继续图4-15的程序
result = []
ia = a.each
ib = b.each
loop do
result.pushia.next
result.pushib.next
end#
图4-15 内部迭代器的缺陷 图4-16 使用Enumerator进行外部迭代
实际上Enumerator对所有元素循环完了的时候会抛出StopIteration异常。loop方法收到该异常后停止循环,而不需要像外部迭代器那样每次去问“还有别的元素吗?”,所以使用Enumerator的程序变得更简单。
4.1.9 外部迭代器的缺陷
那么,外部迭代器是不是就没有问题了呢?外部迭代器的缺陷在于迭代器(光标)对象需要参照集合对象的内部信息。为了按顺序访问集合对象的各个元素,迭代器对象需要访问集合的内部构造。这就破坏了隐蔽集合内部构造的封装性原则。因为集合类与迭代器类非常紧密地关联在一起,就需要特别注意它们内部构造的更新。
比如,C++使用friend修饰词来允许迭代器访问集合的内部构造。这就破坏了对象的封装性原则。于是C++只好使用friend来实现。
4.2 设计模式(2)
前一节学习了“设计模式是什么”。我们把设计上反复出现的模式称为设计模式。最简单的例子就是for循环。《设计模式》一书把设计模式进行了明确分类,极具参考价值。
上节讲述过Singleton、Proxy及Iterator各模式,本节再来考察几个别的设计模式。下面按顺序来考察Prototype、Template
Method和Observer这三个设计模式。
4.2.1 模式与动态语言的关系
《设计模式》一书介绍了23个设计模式。这些设计模式可以分为3大类:有关生成的模式(5个),有关构造的模式(7个)以及有关行为的模式(11个)。如果把上节中三个模式进行这种分类的话,那么Singleton模式属于有关生成的模式,Proxy模式属于有关构造的模式,而Iterator模式则属于有关行为的模式。
设计模式是从(面向对象)编程中经常出现的程序构造中抽象出来的,所以它与语言无关,可以适用于任何编程语言。《设计模式》的例题主要是用C++写成的,其中也有用Smalltalk写的例题,但同样的原则对Java也完全适用。当然对Ruby也是完全一样的。
但是,Ruby或Smalltalk这样的动态语言,与C++或Java这样的静态语言相比,即使是同样的设计模式,使用方法也会略有不同。这次就着重从这一点来考察几个设计模式。
4.2.2 重复使用既存对象的Prototype模式
引用《设计模式》一书中的解释,Prototype(原型)模式“明确一个实例作为要生成对象的种类原型,通过复制该实例来生成新的对象”。
《设计模式》中这一模式的例题使用的是迷宫生成类MazeFactory。这一例子通过拥有生成墙、房间、门等对象的原型,不需要派生就可以生成各种各样的迷宫。但是,说实话,这个例题并没有让人真正感觉到原型的宝贵之处。
实际上,Prototype模式本来并不太适用于C++这样的静态语言。在动态语言中,Prototype模式才能够真正发挥它的巨大威力。
通常在面向对象的语言中,首先准备好所谓的类,也就是对象的雏形,然后从类来生成实际的对象,这些做法都是理所当然的。在面向对象编程中,类是最为基本的存在。
但是,类真的是必需的吗?Smalltalk的“亚种”Self语言的设计人员就认为类并不是必需的。Self中不存在所谓的类,基本操作并不是从类来生成对象,而是复制对象。
基本思想就是如此。
在需要新种类对象的时候,首先复制一个既存的对象,给复制的对象直接增加方法或实例变量等功能,生成最初的第一个新种类对象。如果该对象需要不止一个的话,那就从第一个雏型来复制,需要几个就复制几个。最初一个虽然是雏形,但它并不是所谓类这种人为生成的特别对象,而是一个普通的对象,只不过偶尔被用来复制而已。
雏形这个词对应的英语是Prototype。像这样的不用类,而是用原型模式和复制方法的编程语言称为原型模式的编程语言。实际上,Prototype模式不单是一种设计模式,也许称为一种编程范例才更为合适。
相对于类模式的编程,原型模式的编程的构成元素比较少,具有简单实现面向对象功能设计的倾向。因此,最近有越来越多的规格较小的编程语言采用这种模式。比如,大多数Web浏览器中嵌入的JavaScript的面向对象功能就是原型模式的。最近,受到一部分人关注的Io语言也是原型模式的。
4.2.3 亲身体验Io语言
只讲理论还是难以得到具体的印象,那就让我们边看实际程序,边来亲身体验原型模式的编程吧。虽然用Ruby也可以进行原型模式的编程,但这次我们使用更为彻底的Io语言(参见图4-17)。
让我们来仔细看看图4-17吧。这是描述狗的简单例子。虽然没有实用性,但从中应该能体会到原型模式的气氛。
1的部分生成新的对象,赋值给名为Dog的变量。请注意,这里出现的Object和Dog都不是类。在Io语言中,Object只是有代表性的对象,除最基本的以外其他什么都不知道。以它为基础可以生成各种雏形。这里调用clone方法来复制一个基础对象,并给它起个名字,叫做Dog。刚刚复制出来的Dog对象跟Object是一样的,没有狗的任何功能。
只是Object的话,什么功能都没有,是没有任何使用价值的,让我们来教给它狗的功能吧。2部分中先是给它定一个“坐下”的sit方法。把一个method对象赋值给Dog对象的sit属性,就给Dog对象追加了一个方法。
调用sit方法就等于调用"I''m sitting\.n"print,显示I''m
sitting.。这一部分相当于调用字符串对象的print方法,从中可以体会到彻底的面向对象编程。这个例子中仅仅增加了一个sit方法,实际上根据需要想追加几个方法就可以追加几个。
你看,Dog对象就是这样实现的。再强调一遍,其中作为雏形的是狗对象,而不是类。因此,3部分中对Dog对象调用sit方法的时候,结果与其他狗一样显示出I''m
sitting.。
在像Ruby这样类模式的语言中,作为对象雏形的类拥有与对象完全不同的方法(类的方法),而与之相对的是,原型模式的语言中根本就没有类的存在,雏形与基于它生成的对象是完全一样的。
4部分使用clone方法基于雏形生成新的狗对象。这里请注意,不管是生成新的雏形,还是从雏形生成新的对象,都是仅仅使用clone方法实现的。在类模式的语言中,用子类化和实例化这两个不同的概念实现的程序,这里仅仅用clone这样一个简单的程序就实现了,真让人感动。
当然,简单并不都是一切,原型模式有原型模式的优点,类模式也有类模式的优点,因为原型模式的语言还不太广为人知,这里特意选它作为例子,就是为了让大家体会一下它的单纯。
4.2.4 Ruby中的原型
基本上讲Ruby是类模式的语言,但也拥有支持原型模式编程的功能,具体来说有以下3种功能。
1 复制对象的clone方法。
2 给个别对象增加方法的特异方法功能。
3 给个别对象增加一组功能的extend方法。
图4-18是用Ruby重写的图4-17的程序。
因为Ruby中没有像Io语言中Object这样一个对象原型,所以作为准备,程序一开始(object = Object.
new)先是生成一个Object类的实例。在这以后,除了语法上细微的区别之外,差不多是把Io程序照搬过来的。
从中我们可以看到动态语言中Prototype模式这种令人吃惊的力量。这在必须明确对象类型的静态语言中是实现不了的。因为在静态语言中没有原型编程,是不可能给复制的对象增加新方法的。
4.2.5 编写抽象算法的Template Method模式
接着来看Template Method(模板方法)吧。还是引用《设计模式》中的解释,Template
Method模式是:“在父类的一个方法中定义算法的框架,其中几个步骤的具体内容则留给子类来实现。使用Template
Method模式,可以在不改变算法构造的前提下,在子类中定义算法的一些步骤。”
这实际上是面向对象编程中使用继承的一般技巧。
作为例子,让我们来看一下Ruby的p方法吧。p方法是程序调试中用来显示对象内容的方法。显示调试信息的算法主要是:
1 把对象的调试用输出信息转换成字符串;
2 调用puts方法输出。
这就是调试输出的算法,因为实在是太简单了,称之为算法简直有些过分。
但实际上令人意外的是,为了输出调试信息,分别为各种对象定义调试用输出字符串是非常困难的。针对各种不同的类都要分别进行不同处理,这就很困难了,每次增加新类的时候,为支持新类也都需要做大量的工作。
这时候使用Template Method模式,问题就变得简单了。使用Template
Method模式,输出调试信息的p方法会变成如下的代码,简单得出乎意料。
def pobj
puts obj.inspect
end
这个简单的方法只是把算法的(1)和(2)原封不动地换成了程序语言。在这个定义中,各种对象中准备调试信息的具体处理是由该对象的inspect方法来实现的。在定义新类的时候,只要给它定义了合适的inspect方法,就可以在任何时候使用p方法来输出适当的调试信息。
在父类中定义抽象化的算法,调用隐藏了实现细节的方法,然后在子类中实现具体的细节,这就是Template
Method模式。
4.2.6 用Ruby来尝试Template Method
Ruby的类库中最大限度灵活运用Template
Method模式的部分,应该说是Enumerable模块和Comparable模块了。
Enumerable模块中实现循环的each方法采用了Template
Method模式。表4-2是Enumerable模块的方法一览。
表4-2 Enumerable提供的方法
方 法 名 功能
all? 是否所有元素都为真
all?{|x|...} 块是否对所有元素都为真
any? 是否至少有一个元素为真
any?{|x|...} 块是否对至少有一个元素为真
collect{|x|...} 对各元素进行块中的计算,返回结果的数组
detect{|x|...} 返回使块为真的第1个元素
each_with_index{|x,i|...} 对各元素和下标进行块中的计算
entries 返回元素的数组
find{|x|...} 返回使块为真的第1个元素
find_all{|x|...} 返回使块为真的所有元素的数组
greppattern 返回匹配检索模式的所有元素的数组
greppattern{|x|...} 对匹配检索模式的所有元素进行块中的计算
include?x 是否有元素与x相等
inject{|x,y|...} 返回对各元素进行块中的计算的结果
injectinit{|x,y|...} 返回对各元素进行块中的计算的结果
map{|x|...} 对各元素进行块中的计算,返回结果的数组
max 返回最大的元素
max{|a,b|...} 使用块中的比较方法,返回最大的元素
(续)
方 法 名 功能
max_by{|x|...} 对各元素进行块中的变换,返回结果最大的元素(Ruby 1.9)
member?x 是否有元素与x相等
min 返回最小的元素
min{|a,b|...} 使用块中的比较方法,返回最小的元素
min_by{|x|...} 对各元素进行块中的变换,返回结果最小的元素(Ruby 1.9)
partition{|x|...} 把使块为真的元素和使块为假的元素分离开
reject{|x|...} 返回使块为假的元素的数组
select{|x|...} 返回使块为真的元素的数组
sort 对元素排序
sort{|a,b|...} 使用块中的比较方法,对元素排序
sort_by{|x|...} 对各元素进行块中的变换,按照变换结果对元素进行排序
to_a 返回元素的数组
zipa,... 返回各集合串接后的数组
zipa,...{|arr|...} 对集合进行串接,然后进行块中的计算

这些方法的定义都是仅仅依赖于each方法。因此,在用户定义的类中,只要定义了each方法,一旦把Enumerable模块包含(include)进来,就都可以使用表中的32个方法了。
Enumerable模块实际上是用C编写的,用Rudy也可以简单地定义同样的模块,那就让我们边看Ruby的定义,边来考虑一下如何更好地使用Template
Method模式 。
其中一个实现起来最为简单的方法,应该是收集所有元素的entries方法了。entries方法的实现代码如图4-19所示。看一下图4-19就能明白,处理内容是很简单的。
1 生成数组。
2 用each取出每个元素。
3 把元素追加到数组里。
4 最后返回数组。
从中可以看出,这个方法只是定义了自己处理的框架,对应于每个对象的处理则由each方法来提供。
像Template Method模式的这种使用方法,在Comparable模块中也是一样的。
Comparable模块利用基本的比较大小方法=,提供各种比较演算。=方法把自身与参数相比较,如果自身较大,则返回正整数;若二者相等,则返回0;若自身较小,则返回负整数。以这个方法为基础,Comparable模块提供了==、、=、、=以及between?共6种比较运算。
作为Comparable提供的比较运算的代表,我们来看一看方法的实现吧(参见图4-20)。实际上方法还要加上错误处理,但基本处理如图4-20所示。
像这样使用Template模式,可以不涉及各种数据结构细节,而只在抽象的水平上编写算法的程序。也就是说,算法是在抽象水平很高的状态下表述的,同样的代码能够适用于各种各样的情况。
这样避免了代码的重复,从DRY原则的观点来看 ,也是很优秀的。
4.2.7 动态语言与Template Method模式
一般Template
Method模式与继承往往是成对讨论的,但像Enumberable那样,只需要包含(include)进来,不管继承关系如何,即可以向任何类里追加功能,这一点很有魅力。本来,Ruby的include就是一种受限的多重继承,这没有什么不可思议的。
Template
Method模式的这种优秀性质与语言是不是静态没有关系。像Java那种含有静态类型,而且不允许多重继承的语言,必须强制性地拥有继承关系。所以,像Enumerable这样在各种各样的类中都能利用的算法集,使用Template
Method模式很难实现(interface与委托的组合也不是不可能),但这不是静态语言的问题。
但是,像Io与Ruby这种也善于用原型模式编程的语言,往前进化了一步,可以往特定对象里追加算法集。图4-21表示往特定对象里追加Enumerable功能,虽然这个例子有点牵强。
尽管哪儿也没定义类,但使用extend,骰子对象中就能够利用Enumerable模块的功能了。用extend及特异方法往特定对象里追加功能的做法,也能够用来实现Singleton模式。
4.2.8 避免高度依赖性的Observer模式
Observer(观察者)模式是当某个对象的状态发生变化时,依存于该状态的全部对象都自动得到通知,而且为了让它们都得到更新,定义了对象间一对多的依存关系。
这是控制类与类之间依存关系的一种模式。举一个例子,想想微软的Excel软件吧。以表中的数据为基础表示图形的时候,编辑了表中的数据之后,自然希望图形的内容也跟着变化。或者,从同一组数据,也经常想同时看到直方图和扇形图等多种图形。
能够实现这一要求的最简单的方法,应该是在表编辑功能里附加更新图形显示的处理。但是这样做的话,附加的是与表编辑在本质上不同的处理手段,使事情复杂化,更重要的是,当想要再利用表编辑功能时,还要牵连到不一定必要的图形显示功能。表编辑功能与图形显示功能之间的这种关系称为高度依赖性。
一般地说,高度依赖性不好。从本质上讲,软件是个复杂的东西,为了控制复杂性,有效的方法是将整体分割成几个相互独立的部分进行开发。但是,有了高度依赖性,就不能将组成程序的“零件”(类以及子程序)进行分解,一个一个的“零件”会很大,结果复杂性就很难控制。
Observer模式是一种避免这种高度依赖性的手段。构成观察者模式的有两个对象,一个称为Observer(观察者),接受变更通知;另一个称为Subject(对象)或Observable(被观察者),发出变更通知。
说说刚才的表编辑的例子,表数据就是Subject,图形就是Observer。从观察者与被观察者这两个名字上,被观察者让人得到被动的印象,在实际处理中,被观察者会发出通知“我已经变化了哦”。
4.2.9 Observable模块
Ruby中为实现Observer模式提供了名为observer的库。observer库提供Observer模块。Observer模块的API请参见表4-3。实际的库使用如图4-22所示。
表4-3 Observable模块的API(应用编程接口)
方 法 名 功能
add_observerobserver 增加观察者
delete_observerobserver 删除特定观察者
delete_observers 删除观察者
count_observers 观察者的数目
changedstate = true 设置更新标志为真
changed? 检查更新标志
notify_obserlers*arg 通知更新。如果更新标志为真,调用观察者带参数args的方法

解释一下图4-22中的程序。首先是require observer库。利用库的时候,这是必须写的。
然后是定义被观察者类Tick。注释中也写道,该类每秒发送1次更新通知。它是相当于时钟的类。Tick的发音,好像是时钟的滴答滴答声。Tick是被观察者,所以要将Observable模块包含进来。仅一句话就能让任意类成为被观察者,这正是Ruby的威力。
tick方法是主循环。有了这个处理,每隔1秒,循环就发出更新通知。虽然仅仅睡眠1秒,但为了保证能在整秒发出更新通知,便以微秒为单位进行了补正(sleep1.0
- Time...的部分)。
实际的更新通知只是调用changed方法设置更新标志,然后用notify_observers方法通知观察者。它们都写在loop(循环)内。
虽然在这种每次肯定都要定期发出更新通知的情况,把changed与notify_observers分离开来没有意义,但是考虑到会有频繁变化、每次更新处理的花费都比较大的情况,还是将二者分离开了。比如刚才的表编辑的例子中,与其在每次细微的变化后都要更新图形,不如在键盘输入告一段落时再集中更新图形,应该更有效率。
require "observer"

# 更新通知者(Observable)
# 这个类每秒发送1次更新通知
class Tick

include Observable
def tick
loop do
now = Time.now
changed
notify_observersnow.hour,
now.min, now.sec
sleep 1.0 - Time.now.usec
1000000.0
end
end
end

# 观察者(Observer)
# 依照通知,表示现在时刻的类(文字版)
class TextClock

def updateh,m,s
printf "\e[8D%02d:%02d:%02d", h, m, s
STDOUT.flush
end
end

tick = Tick.new
tick.add_observerTextClock.new
tick.tick
图4-22 使用observer库的Ruby程序,观察者与被观察者不相互依存
后半部分的TextClock类是观察者类。依照Tick发送的通知,在控制画面上显示现在时刻。TextClock类不是特定类的子类或者别的什么,只是拥有被更新通知调用的update方法。update方法接受Tick类notify_observers方法传过来的时、分、秒三个整数参数。
实际显示用了ANSI的转义字符(printf以下的部分)。用ESC[8D]将光标移到行首,后面显示时刻。为避免缓冲问题,每次都调用STDOUT.flush。
定义了Tick(被观察者)与TextClock(观察者)两个类之后就简单了。先生成Tick类的对象(图4-22倒数第3行的部分),然后使之与TextClock类相关联,最后启动Tick类的主循环(图4-22的末尾部分),就这么多。
执行图4-22的程序,控制画面上就会显示出一个数字时钟。因为是无限循环,想要停止时,请按下Ctrl+C。
这次只做了一个观察Tick类的对象TextClock,如果愿意,可以添加任意多个观察者。比如,对同一个Tick,不光能添加文字时钟,还可以添加图形时钟。
这个程序最应该注意的一点是,Tick类与TextClock之间的关联,只用一行(图4-22倒数第2行)就完成了。Tick类与TextClock类之间,只有“更新以后,调用update方法”以及“在update方法中,传递时、分、秒”这种简单的约定,不存在别的关系。只要是遵守相同约定的类,都可以简单地进行交换。
可以看出,使用Observer模式,显然能够降低相互依赖性。既可以将观察者类做成零部件,又可以根据需要更换被观察者(比如测试用的假程序)。这个性质对于提高软件的开发效率和测试效率,都是很有用的。
4.2.10 Observer模式与动态语言
动态语言的性质在Observer模式中也很有用。由于Ruby的动态性质,observer库具有以下几方面的灵活性。
1 观察者类不必是特定类的子类。
2 观察者类不必实现特定的接口(本来在Ruby中也没有接口)。
3 观察者类的更新方法名可以自由决定(Ruby 1.9的功能)。
4 观察者类更新方法的参数可以自由决定。
5 被观察者类不必是特定类的子类。
6 对被观察者类的要求,只是将Observerable模块包含进来。
我想Java那种静态语言也具有与Ruby的observer库相同功能的库。事实上,有几种DI容器(Dependency
Injection Container)框架,也具有与observer库相类似的处理。
但是,如果编码太繁杂了,或者需要用XML文件代替Java来描述类之间关联的话,我认为就没有Ruby这么好用了。
***
本节从与动态语言相关联的观点解释了设计模式中的Prototype模式、Template
Method模式和Observer模式。作为对设计模式的总结,下面看一看设计模式与软件开发原则(Open-Close
principle)。
4.3 设计模式(3)
很久以前,技术人员将计算机的机械部分称为硬件,这是计算机所有实体部件和设备的统称。与此相对应,没有实体的程序被称为软件。如今硬件和软件作为计算机关联用语已经固定下来了,但当初却是技术人员之间的俗语。
说起软件,会让人想起“柔软灵活”,但事实上,缺乏灵活性的东西有很多。程序规模小的时候,还能够简单地更改,让人觉得有灵活性。但对于那些大规模商用软件,各部分依存关系很紧密,改动一个地方就会对别的地方有影响,总是不能随心所欲地更改。
4.3.1 软件开发的悲剧
在软件开发过程中,会遇到各种各样的问题,原因归结起来主要有两个方面,一个是复杂性,一个是变化性。
软件的规模越大,各个部分之间的牵连越复杂,更改也就越难。如果软件单纯而且规模小,更改还相对容易。随着计算功能的提高,交给计算机的任务规模也越来越大。几乎所有的软件,都随着用户需求的提高而得以扩展,变得越来越复杂。
如果只是增加软件功能,也不会引起那么多的问题。但是,在软件开发过程中,需求变更几乎是不可避免的。在洽谈时,即便已经同意了画在纸上的软件模型,可一旦见到了程序,“还是感觉不对劲,希望再改一改”,很多用户都会这么说。我自己作为一个多年的职业程序员,对于用户想到哪儿就是哪儿的作法,也经常会发牢骚。
话虽这样说,前几天,我委托同事写了一个程序,尽管事前已经同意了需求,但看了实际程序以后,还是忍不住说:“与想象的稍微不同,能这样改一改吗?”自己竟然也跟那些被我发过牢骚的用户完全一样了,唉,只能感叹人是多么地自私任性。
先不说这些了,尽管软件越来越复杂,更改所需要的花费越来越大,但用户要求却越来越多样化,对软件的变更要求也越来越频繁。长此以往,软件开发肯定要在什么地方失败。
4.3.2 开放—封闭原则
面对以上情况,有用的原则是开放—封闭原则(open-closed
principle)。开放—封闭原则是Eiffel语言的设计者Bertrand
Meyer在其著作《面向对象的软件构造》中介绍的原则。其定义如下,非常简单。
对模块扩展必须开放(Open),对修改必须封闭(Closed)。
所谓“对模块扩展必须开放”,是指模块可以扩展。比如,如果数据结构能够追加新的字段,或是能够追加新的功能,就可以称模块是开放的。某一模块会被用到什么地方,不可能完全预测。为了应对将来的需要,对于扩展必须是开放的。
所谓“对修改必须封闭”,是指某一模块被别的模块参照时的要求。必须做成这个样子:即使被参照一方的实现细节发生变化,也不会带来问题。
也就是说,即使某一模块的内部结构改变了,对外接口也应当是不变的。如果对外接口不能保持不变,模块就不能稳定使用。使用不稳定的模块,别的模块也必须时常跟着改变,软件的复杂性和维护成本都要增加。
把open-closed
principle译作“开放—封闭原则”,感觉有点生硬。不管怎样,一次又一次重复“开放—封闭原则”都显得有点冗长,以下简称为OCP。
4.3.3 面向对象的情况
既要开放,又要封闭,这看起来互相矛盾。但是面向对象编程语言能够很彻底地消除这个矛盾。
请看图4-23。这个程序里有3种箱子(普通的箱子、上了锁的箱子及扎着彩带的箱子),要根据箱子的种类来打开箱子。
可以看出,这是一个很漂亮的程序,只用最少的代码就能实现想做的事。如果想让这个程序对应新种类的箱子,只需要生成新的箱子对象,为这个箱子定义open方法就行了。所以,可以说,这个程序对于修改而言是封闭的。
面向对象的方法(OCP)
# 变量box1、box2和box3分别是3种箱子

def box1.open
puts"打开箱子"
end

def box2.open
puts"解锁,打开箱子"
end

def box3.open
puts"解开彩带,打开箱子"
end

box1.open # 显示“打开箱子”
box2.open # 显示“解锁,打开箱子”
box3.open # 显示“解开彩带,打开箱子”
图4-23 3种打开箱子的代码,面向对象的情况,满足OCP原则
3种箱子是各不相同的对象,打开箱子只要调用
box.open
就行了,对哪个箱子都一样。即使将来箱子种类增加了,只要那个对象有open方法,就可以同样处理。结果,即使追加了箱子,也不用更改现有代码。所以,这个程序对于修改而言是封闭的。
OCP初看起来似乎是自相矛盾的,但如果使用面向对象语言来编写程序的话,就完全能够达到它的要求。
4.3.4 非面向对象的情况
那么,让我们来看看用非面向对象语言编写程序来进行相同处理的情况(参见图4-24)。在这个程序中,将来箱子种类增加时,需要更改box_open子程序。这次与箱子关联的子程序只有box_open,如果有多种子程序,单纯考虑各种组合,就会有以下这么多。
子程序的种类 × 箱子的种类
箱子种类增加了,组合的种类也会急剧增加。这样就不能随便增加箱子种类,对于扩展而言,不能说是“开放”的。
反之,根据箱子种类的不同而调用不同的子程序,会怎样呢?(参见图4-25)这样增加箱子种类,对各种箱子定义了独立的子程序,因此可以说对于修改而言是封闭的。但是,对于调用子程序的那一侧而言,需要考虑到箱子的种类。因此,很难说它对于扩展而言是开放的。
def box_openbox

# box_typebox 假定由子程序能知道箱子种类
if box_typebox == "plain"
puts"打开箱子"
elsif box_typebox == "lock"
puts"解锁,打开箱子"
elsif box_typebox == "ribbon"
puts"解开彩带,打开箱子"
else
puts"不知道打开方法"
end

end

# 变量box1,box2,box3分别是3种箱子

box_openbox1 # 显示“打开箱子”
box_openbox2 # 显示“解锁,打开箱子”
box_openbox3 # 显示“解开彩带,打开箱子” def plain_box_openbox
puts"打开箱子"
end
def locked_box_openbox
puts"解锁,打开箱子"
end
def ribbon_box_openbox
puts"解开彩带,打开箱子"
end

plain_box_openbox1
locked_box_openbox2
ribbon_box_openbox3
图4-24 3种打开箱子的代码,非面向对象的情况,不满足OCP原则图4-25 打开3种箱子的代码,别的非面向对象型代码
使用面向对象语言,功能使用方可以不必知道功能提供方各种类的详细内容,而只需要着眼于它们具有什么样的接口就可以。功能扩展以后,由于多态,扩展后的功能能够自动使用,使用方只要知道接口就行了。功能提供方提供了新功能,或是内部有了更改,功能使用方都不用做任何更改。也就是说,从功能的使用方来看,模块对于修改是封闭的。
另一方面,功能的提供方只要生成与既存对象具有相同接口的对象,任何时候都能追加新的功能。也就是说,对于功能扩展而言是开放的。这种情况下的接口,对于静态型语言来说,就是相同的类型;而对于动态型语言来说,就是有没有相同的方法(换个说法就是Duck
Typing )。
很多面向对象语言,通过使用继承,只要添加一个子类,就可以往模块(类)里随时追加功能。因为有继承而允许功能的追加(对功能扩展而言是开放的),因为有多态而维持模块接口的稳定性(对修改而言是封闭的),开放和封闭能同时实现。
从实用主义的观点看,面向对象的精髓就在于对OCP的实践。至于把对象看做物体理解起来比较容易,能够建立现实世界的模型等,这些都只不过是些锦上添花的东西。
“数据与函数没有一体化,所以不是面向对象”,“封装得不充分,所以不是面向对象”等,世上有不少人作出类似这样的判断。道理上也许的确是这样,但面向对象无非是编程的一种工具,是不是理论上正确的面向对象不重要,是否符合OCP,生产性高不高,维护性好不好,能否适应将来的更改,等等,这些才是重点。
4.3.5 OCP与Template Method模式
虽说使用面向对象语言的功能,可以实现OCP,但也只是说有这种可能性,并不是说什么时候都能实现。当然,虽然使用了面向对象语言,却做成了一个糟糕的设计,这种情况也是屡见不鲜的。
于是设计模式登场了。正如上节所示,设计模式就是给做得好的设计起个名字,并将它们进行分类。分类中的很多设计模式之所以优秀,是因为能够经得起OCP所要求的变化。
那么,现在实际看看几种设计模式,考察一下它们都是怎样满足OCP的。
上节介绍的Template
Method模式,是满足OCP的基本手段。之所以这么说,是因为其他的设计模式都是利用多个类的关联来实现的,而Template
Method模式则仅仅使用了继承,基本上无非就是实现一个抽象类而已。
上节利用Ruby标准模块的Enumerable和Comparable解释了Template
Method模式,这次从既存类中提取抽象类(Ruby中是模块)的观点来考察一下。
表4-4列举了Ruby标准类IO的方法中用于输出的方法。
这些方法在与IO类有互换性的StringIO类及ARGF对象中也有实现。但是,仔细想一想,不管哪种方法都进行类似的处理,所以可以用Template
Method模式归纳一下。
例如,选择write作为基本处理,其他方法可以用表4-5所示的处理来实现。将这些处理作为一个模块分割开以后(参见图4-26),将命令行参数指定的多个文件结合成一个虚拟文件的ARGF,在对字符串进行输入输出的StringIO类中,就没有必要再重复这些定义。
module Writable

def obj
self.writeobj.to_s
end
def print*args
args.each do |obj|
self.writeobj.to_s
end
end
def printffmt, *args
self.writesprintffmt, *args
end
def putcc
self.writesprintf"%c",c
end
def putss
self.write"%s\n",s
end

end
图4-26 Ruby中Writable模块的定义
也就是说,有了Writable模块,IO类以外的类中,只需定义适合各自实现方式的write方法,然后将Writable模块包含进来就可以了。
有了Writable模块,处理内容相似的方法就不需要分别定义,而且,在以后开发需要具备这种输出方法的类时,还可以再利用。
现在的Ruby还没有这样的Writable模块,实际做一做,觉得特别好。也许在将来的版本中会导入这一模块。
同样在处理中重复编程、代码复制,都是违反软件开发中重要的DRY原则的。从OCP的观点来看,重复也是非常恶劣的。同样的代码反复出现,如果要对代码进行某种修改,那么全部的代码都必须修改。不能算作“对于修改而言是封闭的”。
数据结构的扩展也有影响复制代码全体的危险性,所以也不能算“对于扩展而言是开放的”。OCP与DRY,这两个原则实际上具有相同的意思。
4.3.6 Observer模式
Template
Method模式说到底无非是继承,现在调查一下关联到多个类的设计模式与OCP的关系吧。作为最简单的例子,还以上节讲解过的Observer模式为例。
Observer模式中,有降低多个对象间依赖性的机制。请看图4-27。那是上节最后所用的程序示例。由每秒产生一次事件的被观察者(更新通知者)和显示现在时刻的观察者所构成的简单时钟程序。
程序的细节上节已经讲解过了,就省略不讲了,这里重要的是末尾3行。更新通知者与的关系仅用add_observer一行来定义,其他部分全部都是互相独立的。
所以,即使以后要求变化了,需要更改时钟的外观时,只需要生成新的观察者类来代替TextClock,并用add_observer登录就行了。很简单地就能进行功能的追加和更改,可以认为这个设计模式对于更改是开放的。
require "observer"

# 更新通知者(Observable)
# 每秒发送1次更新通知的类
class Tick

include Observable
def tick
loop do
now = Time.now
changed
notify_observersnow.hour,
now.min, now.sec
sleep 1.0 - Time.now.used
1000000.0
end
end
end

# 观察者(Observer)
# 依照通知,表示现在时刻的类(文字版)
class TextClock

def updateh, m, s
printf "\e[8D%02d:%02d:%02d", h, m, s
STDOUT.flush
end
end
tick = Tick.new
tick.add_observerTextClock.new
tick.tick
图4-27 Observer模式的程序示例,显示字符式时钟
另外,更新通知者与观察者之间的消息只有通过add_observer而建立的唯一关系,不必担心有别的恶劣影响。这意味着对于修改而言是封闭的。
由此可知,Observer模式是满足OCP的。
不仅限于Observer模式,很多被认为优秀的设计模式,都可以基于OCP来说明它们为什么优秀。
那么,不使用Observer模式的情形会怎么样呢?编写一个不用Observer模式的程序,实现与图4-26相同的处理吧(参见图4-28)。
看起来比图4-27要简短得多。但能否经得住将来的更改呢?TextClock类专门设计为用文字显示时刻。Observer模式版本中更新通知者所进行的处理,也就是计秒数处理和观察者的时刻显示处理(在图4-27中)相互连接起来了。所以,TextClock处理的一部分不能被其他应用程序所沿用。估计只能将TextClock复制,然后进行改造了。复制,也就是将进行相同处理的代码分散到几处,是违反DRY原则的。
class TextClock
def start
loop do
now = Time.now
printf "\e[8D%02d:%02d:%02d",
now.hour, now.min, now.sec
STDOUT.flush
sleep 1.0 - Time.now.used
1000000.0
end
end
end
TextClock.new.start
图4-28 不使用设计模式的文字时钟的代码,乍一看是清晰而良好的代码
但是,如果事先知道文字时钟的规格将来也会一成不变,图4-28的程序还是很称心的。因为它简短而直接,执行效率也高。
归根结底,DRY也好,OCP也好,都不过是原则,根据具体情况,还是要做适当的选择。如果代码没有再利用的打算,也没有将来要扩展其功能的打算的话,也就没有必要生搬硬套设计模式。使用设计模式时有必要先做判断。
4.3.7 使用Strategy模式
介绍一个新的设计模式。根据《设计模式》,Strategy(策略)模式是这样解释的:“定义算法的集合,将各算法封装,使它们能够交换。利用Strategy模式,算法和利用这些算法的客户程序可以分别独立进行修改而不互相影响。”
Strategy模式,就是将容易变化的处理归纳为独立的对象,然后使它们能够互相交换。使用方法与将容易变化的处理交给子类的Template
Method模式相类似。两个模式最大的区别在于,Strategy模式是独立的对象,能够动态交换处理逻辑(参见图4-29)。
图4-29 Template模式与Strategy模式
静态型语言中,Strategy对象需要一个共同的父类,实现这个父类往往要用到Template
Method模式。
通过Ruby库中使用Strategy模式的例子来介绍cgisession库。cgisession库是CGI程序中,识别某一特定用户的一连串操作(session)的库。
使用cgisession库,CGI里有必要保存session固有的数据,但保存方法因应用程序的不同而有所不同。一个应用程序中可能是往临时目录里放一个文件,另一个应用程序中或许是将session数据放在数据库里。想使用的数据库管理器也是千差万别。
像这样的情形,不知道会有什么要求发生,库对于变化应该有开放性。cgisession库使用Strategy模式,对应预想中的未来变化。
图4-30是cgisession的使用示例。其中重要的是第6行(即''database_manager''那一行)。
require ''cgi''
require ''cgisession''
require ''cgisessionpstore'' # 提供CGI::Session::PStore
session = CGI::Session.newcgi,

''database_manager'' = CGI::Session::PStore, # 使用PStore,

''session_expires'' = Time.now + 30 * 60 #
30分钟超时
if cgi.has_key?''user_name'' and cgi[''user_name''] != ''''
session[''user_name''] = cgi[''user_name''].to_s
elsif !session[''user_name'']
session[''user_name''] = "guest"
end
session.close
''database_manager'' = CGI::Session::PStore,
图4-30 cgisession库的使用例子
CGI::Session类在生成对象时,用哈希表指定选项,可以指定database_manager为保存实际数据的类(数据库管理器)。这个例子中,指定了利用Ruby标准版附带的简易面向对象数据库PStore的数据库管理器CGI::Session::PStore。
除此之外,Ruby标准版还提供了将session数据保存在普通文件的CGI::Session::
FileStore类,以及保存在内存的CGI::Session::MemoryStore类等。Ruby标准版中没有附带将session信息保存在关系数据库(RDBMS)的数据库管理器类,但在RAA
中,记录有利用MySQL、PostgreSQL以及dRuby进行网络通信的类。
cgisession对象在初始化处理中生成数据库管理器类的对象,保存在类实例变量@dbman中。在需要访问session数据的时候,调用数据库管理器的方法进行实际的处理(参见表4-6)。
数据库管理器的restore方法返回表示实际数据库的对象,CGI::Session对象使用与该数据库对象数组相同的接口([]方法和[]=方法)来访问。update方法将数据库对象的状态写到实际文件里。Ruby这样的动态语言,只要有了表4-6所示的这些方法,不管什么都能作为数据库管理器类来使用。
4.3.8 Strategy模式与OCP
最后来验证一下Strategy模式是如何满足OCP的。
首先看是不是开放的。使用Strategy模式时,只要更换封装了算法的Strategy对象,就可以追加及修改功能。cgisession的情形,只要更改了选项所指定的数据库管理器类,就能够改变session数据的保存方法。所以,可以说Strategy模式对扩展而言是开放的。
另一方面,即使扩展或修改了功能,利用Strategy模式的一方也没有必要改变。cgisession的情形,即使开发了新的保存session数据的方法,既有的代码也没有必要变更,而且利用新的数据库管理器时,除了CGI::Session对象的初始化选项以外,别的代码也没有更改的必要。所以,Strategy模式对于更改而言是封闭的。
因此,Strategy模式完全满足OCP。
由于篇幅的关系,设计模式中只验证了很小一部分,世上很多设计模式,为了能应对将来可能的修改,都是按照OCP的要求来设计的。
***
本节从OCP的观点,重新审视了一下设计模式。由继承实现的OCP,通过使用设计模式而变得更强有力。我想大家可以认识到设计模式是应变能力很强的工具,得到了广泛应用。
实际上,OCP与设计模式相结合,并不是我的首创。据我所知,日本国内将这两者结合起来讲解的,还有石井胜先生。石井先生的讲解详见http:www.objectclub.jpcommunitymemorial
homepage3.nifty.commasarlarticledp-ocp-2.html。
词汇与共通语言的重要性

 

 

書城介紹  | 合作申請 | 索要書目  | 新手入門 | 聯絡方式  | 幫助中心 | 找書說明  | 送貨方式 | 付款方式 香港用户  | 台灣用户 | 海外用户
megBook.com.tw
Copyright (C) 2013 - 2024 (香港)大書城有限公司 All Rights Reserved.