02 框架设计
2.1 从编程历史看面向对象编程的本质与未来
莱布尼兹的奇思怪想
人类的第一位程序员
雅卡尔 可编程织布机
Ada
什么是计算机?什么是程序?
通用计算机
现代计算机与现代的程序
冯诺依曼 ENIAC
插电线
形形色色的汇编语言
每一种 CPU 都有独特的机器语言,因而需要不同的汇编语言。
形形色色的编程语言
早期 Basic 语言
结构化的 Basic 语言
Perl 语言,“伪”面向对象语言
70 年代,软件危机
C 语言,结构化语言
C++ 语言,C++ 向后兼容 C 的所有功能,并且提供了面向对象的编程机制
Java 语言,完全面向对象的语言
编程语言的实质
编程的目的:用计算机来解决现实世界的问题
编程的过程:在计算机所能理解的“模型”(解空间)和现实世界(问题空间)之间,建立一种联系。
编程语言是一种“抽象”的机制,问题是对“谁”来抽象。
抽象的种类
机器代码和汇编语言:对基础机器进行抽象
非机构化高级语言(Basic,Fortran):对计算处理逻辑抽象
结构化的程序设计:开始对问题领域进行一定程度的抽象
面向对象程序设计:直接表达问题空间内的元素
编程方法的演进
汇编语言 → 高级语言 → 结构化程序 → 面向对象编程 OOP
编程的核心要素
人(劳动者)
客观业务领域(劳动对象)
计算机(劳动工具)
什么是面向对象编程?
Smalltalk
万物皆为对象
程序是对象的集合,他们通过发送消息来告知彼此所要做的
每个对象都有自己的有其他对象多构成的存储
每个对象都拥有其类型
某一特定类型的所有对象都可以接收同样的消息
什么是对象?
状态:表明每个对象可以有自己的数据
行为:每个对象可以产生行为
标识:表明每个对象都区别于其他的对象
面向对象编程三要素
封装 Encapsulation:隐藏实现细节(访问控制),定义接口
继承 Inheritance:IS-A 关系,HAS-A 关系(组合)
多态 Polymorphism:后期绑定(虚函数),向上转形(Up-Casting)
封装并不是面向对象编程语言独有的,C 语言可以在头文件 .h 里面定义方法,在实现文件 .c 中定义具体的结构体和方法实现
继承也不是面向对象编程语言独有的,C 语言中 A 结构体包含 B 结构体的定义,可以理解为 A 继承了 B
多态也不是面向对象独有的,C 语言中有指向函数的指针,可以实现多态(危险)
在面向对象的编程语言中,多态非常简单。
面向对象编程与面向对象分析
面向对象就是利用多态特性进行编程
面向对象分析是将客观世界,也就是编程的业务领域进行对象分析
充血模型与贫血模型
领域驱动设计
面向对象设计的目的和原则
强内聚、低耦合
易扩展:易于增加新的功能
更强壮:不容易被粗心的程序员破坏
可移植:能够在多样的环境下运行
更简单:容易理解、容易维护
设计模式 design patterns
用于解决某一种问题的通用解决方案
语言中立
贯彻设计原则
Gang of Four (Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides)
创建模式
行为模式
结构模式
框架 frameworks
用来实现某一类应用的结构性程序,对某一类结构方案可复用的设计与实现
简化应用开发者的工作
实现了多种设计模式
Web 服务器也是框架:Tomcat
框架 vs 工具
框架调用应用程序代码,应用程序代码调用工具
架构师用框架保证架构的落地,架构师用工具提高开发效率
35 分钟内讲完程序语言的历史演进,概述面向对象编程要点
2.2 设计臭味:糟糕的代码有哪些特点?
UML 小练习
在面向对象分析的时候,把名词作为对象的候选
软件开发的上下文,不断需求变更
其实我没怎么看出来那个拨号程序有什么问题,虽然从直觉上感觉似乎有点不对。
软件设计的“臭味”
“不好的”软件,“臭味”,“坏味道”
(Review 代码的时候,被恶心吐了)
一坨代码
僵硬 Rigidity:不易改变,每个改动都会迫使血多对系统其他部分的改动
脆弱 Fragility:只想改 A,结果 B 被意外破坏(换轮子的时候,车窗碎了),对系统的改动会导致系统中和改动地方无关的许多地方出现问题。
不可移植(牢固)Immobility:不能适应环境变化,很难解开系统的纠结,组件重用
导致无用的陷阱(粘滞)Viscosity:做错误的事比做正确的事更容易,引诱程序员破坏原有设计。做正确的事情比做错误的事情要困难。
不必要的复杂性 Needless Complexity:设计中包含不具有任何直接好处的基础结构
不必要的重复 Needless Repetition:设计中包含有重复的结构
晦涩 Opacity:代码难以理解,很难阅读、理解,没有很好的表现出意图。程序是给人看的。
过度设计、copy-paste 代码
代码腐化的例子
从键盘读入字符并输出到打印机
增加读取纸带机功能
flag 的含义不明确;
false 的含义不明确;
奇怪的 flag 加上奇怪的注释
只能通过读代码,才能知道 flag 的含义(flag 为真,则表示从纸带机读数据)
增加纸带机输出
现在你已经有了“一坨代码”
一个遵循 OOD 原则的设计
看了李老师最后对于那个拨号器设计“坏味道”的分析,考虑了一下为什么自己没能够“闻”出来,其实就是没有按照那几个标准去衡量,另外一点,就是自己没有想到如何扩展拨号器,比如用于其他类型的门锁。
这一章比较大的收获,就是以前看待设计的时候用的是静态的眼光,所以看不出有什么好处;如果采用动态的角度,考虑到软件系统后续可能的变化,那么设计模式、设计原则就都显示出了威力。
按照目前的分析,我觉的 Button 按钮接口,应该有类型(数字或者命令)和数值两个属性,并且可以有 push() 续方法。
2.3 开闭原则介绍及代码分析
OCP, Open/Closed Principle
Open for extension, Closed for modification
对于扩展是开放的,对于更改是封闭的。
不需要修改软件实体(类、模块、函数),就应该能够实现功能的扩展。
改进 Button 的第一种方法,感觉比较容易想到;而第二种方法,策略模式就稍微要费一点脑筋了。
我的理解是通过传递不同的 token,然后 Dailer 中实现的 buttonPressed(int token) 方法来处理,Dailer 成为瓶颈,而且不满足开闭原则。
然后是方法三,适配器模式。
(9 分 30 秒左右,图片里面有两个命名错误 Adepter → Adapter )
方法四,观察者模式
这一段对于几个设计模式的解说,非常精彩。要是有代码配合就更好了。
2.4 依赖倒置原则介绍及代码案例分析
DIP, Dependency Inversion Principle
高层模块不能依赖底层模块,而是大家都依赖于抽象;
抽象不能依赖实现,而是实现依赖抽象
DIP 倒置了模块或包的依赖关系,开发顺序和职责。
高层决定低层:高层模块定义(依赖)一个接口,供底层模块来实现。
高层被重用:高层被重用的可能更大
面向抽象编程,关键的问题在于抽闲属于谁
好莱坞规则:Don’t Call me, I will Call you.
Tomcat 基于规范或者 Servlet 接口开发。高层模块不依赖低层模块,框架不依赖与实现代码,而是依赖于接口,框架可以调用代码。
Button 的本质:检测用户的按键指令,传递给目标对象。
用什么机制检测用户的按键,目标对象是什么,都不重要。
2.5 里氏替换原则
1988,Barbara Liskov
若对每个类型 T1 的对象 o1,都存在一个类型 T2 的对象 o2,使得在所有针对 T2 编写的程序 P 中,用 o1 替换 o2 后,程序 P 的行为功能不变,则 T1 是 T2 的子类型。
子类型 subtype 必须能够替换掉他们的基类型 base type
LSP 指导软件框架开发。
关于正方形不能继承长方形的例子,我觉的可能也需要放到场景中,可能只是在 testArea 这个测试中不合适。
按照正方形不能继承长方形的思路,那么直角三角形也不能继承三角形?
对于同一个类,所创建的不同对象,标识和状态不同,但是行为相同;因此,设计和界定一个类,应该以其行为作为区分。
有一个小的疑问,如果不符合里氏替换原则,那么会带来什么恶果呢?
使用基类的地方,一定也适用于其子类。
子类一定得拥有基类的整个接口
子类的访问控制不能比基类更严格
可以提取共性到基类
继承比较容易,基类的大部分功能可以通过继承进入子类;
继承破坏了封装,继承将基类更多的细节暴露给子类,“白盒复用”;当基类发生改变,可能会层层影响其下的子类;继承是静态的,无法在运行时改变组合;类的数量有可能“爆炸”。
组合优于继承。
一个模型,只有通过客户程序或者使用场景才能体现出来是否有效,而设计的使用者很难预测,所以从设计的角度要避免“过于复杂”或“过度设计”。
可能违反 LSP 的征兆
派生类中的退化函数
派生类中抛出基类不会产生的异常(如果派生类抛出异常,那么一般是基类同名方法抛出异常的子类)
其实我挺想看到一个符合 LSP 原则的代码。
2.6 单一职责接口隔离
SRP, Single Responsibility Principle
一个类,只能有一个引起它变化的原因。
违反 SRP,把两个功能(变化原因)耦合在一起,会引起程序脆弱,修改其中一个功能时,另一个功能可能会议外受损;并且有可能造成代码不可移植(或者移植不需要的代码)
ISP, Interface Segregation Principle
不应该强迫客户程序依赖(看到)他们不需要的方法
实现类无法拆解的方法,可以通过不同的借口暴露给不同的客户程序。
2.7 案例:反应式编程框架 Flower 的设计
一般的并发用户请求场景,在客户端网络连接、服务器远程调用和数据库访问三个 IO 密集的位置都有可能产生阻塞,导致程序变慢,进而可能系统崩溃。
那个 Flower 反应式重构前后性能对比,不知道与 C# 和 Java 的语言特性或者部署平台有多少关系?
我觉得 Flower 的设计里面,对于消息机制的应用是最巧妙的。另外对于,异步数据库驱动有一点好奇,这个是否属于 Flower 的范畴?
2.8 第二周课后练习
作业一:
请描述什么是依赖倒置原则,为什么有时候依赖倒置原则又被称为好莱坞原则?
请用接口隔离原则优化 Cache 类的设计,画出优化后的类图。
提示:cache 实现类中有四个方法,其中 put get delete 方法是需要暴露给应用程序的,rebuild 方法是需要暴露给系统进行远程调用的。如果将 rebuild 暴露给应用程序,应用程序可能会错误调用 rebuild 方法,导致 cache 服务失效。按照接口隔离原则:不应该强迫客户程序依赖它们不需要的方法。也就是说,应该使 cache 类实现两个接口,一个接口包含 get put delete 暴露给应用程序,一个接口包含 rebuild 暴露给系统远程调用。从而实现接口隔离,使应用程序看不到 rebuild 方法。
作业二:根据当周学习情况,完成一篇学习总结
版权声明: 本文为 InfoQ 作者【escray】的原创文章。
原文链接:【http://xie.infoq.cn/article/6c4fec19d8ad6d8e050ef817e】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论