软件构造笔记

软件构造整理的笔记(未上传图片)

第一章 软件构造的视图与质量目标
第一节 软件构造中的多维视图
一、内容大纲
1.软件多维视图
(1)按阶段(phases)划分:构造时视图,运行时视图(build-time & run time)
(2)按动态性(dynamics)划分:时刻视图,阶段视图(moment & period)
(3)按构造对象的层次(level)划分:代码,构件视图(code & component)
(4)每个视图的元素、关系和模型
2.软件构造就是视图之间的转换
(1)∅ → Code
(2)Code → Component
(3)Build-time → Run-time
(4)Moment → Period
二、要求
1.能够从三个维度看软件系统的构成
2.用什么样的模型/视图描述软件系统
3.将“软件构造”看作“不同视图之间的转换”
三、软件构造的多维视图
1.软件的定义
(1)图灵:二进制代码构成的可执行程序
(2)代码程序 → 算法+代码+数据 → 程序+数据+文件 → 模块(构件)+数据/控制流
软件的质量需要看外部的环境
2.软件构造的环节有很多
3.软件多维视图

(1)Build-time构造阶段
Code-level:代码的逻辑组织:functions, classes, methods, interfaces
Component-level:代码的物理组织:files, directories, package, libraries
Moment:特定时刻软件状态
Period:软件形态随实践的变化
1)Build-time, Moment, Code-level
主要观察源代码的组织和逻辑关系
三个层面:
词汇层面:程序中使用的语句、字符串、注释、变量等等
语法角度:语法树AST,对于程序的操作画成树

语义层面:源代码的程序的组成部分的逻辑关系(UML)

2)Build-time, Period, Code-level
编码时期代码的改变情况,编码时长需要用数月数年,需要记录改变
Code churn:代码变化:一个文件中语句的增删改

代码版本控制工具

3)Build-time, Moment, Component-level
开发程序时有很多类和文件,需要有规则的组织起来,代码分割为文件,文件组织成包,模块组织成库Library。
使用库文件:
寻找库:配置资源目录
静态链接(构造阶段),动态链接(运行阶段)
4)Build-time, Period, Component-level
各项软件实体随时间的变化,配置项,版本

版本控制工具可以也支持协同开发

(2)Run-time运行阶段
运行时程序被载入目标机器开始执行
Code-level:逻辑实体在内存中的呈现
Component-level:物理实体在物理硬件环境中的呈现
Moment:逻辑/物理实体在内存/硬件环境中特定时刻的形态
Period:逻辑/物理实体在内存/硬件环境中随时间的变化
关注点:可执行程序(原生机器码)、库文件(动态链接)、配置文件(对程序的执行进行限制,比如程序需要读入音频、视频、图片,不同类型的读入需要不同的配置)、分布式程序(需要运行多个运行程序,分别部署于多个计算机物理环境)
1)Run-time, Moment, Code-level
Snapshot代码快照图:描述程序运行时内存里变量层面的状态
Memory dump内存信息转储:

2)Run-time, Period, Code-level
UML图
执行追踪:日志记录

3)Run-time, Moment, Component-level
UML:不同机器上,及其的不同位置上

4)Run-time, Period, Component-level
系统层面日志

四、软件构造不同视图的转化
1.

2.类型转化
(1)∅ → Code
Programming, Coding
Review,静态分析,检查
(2)Code → Component
Design
编译:静态链接,包,安装
(3)Build-time → Run-time
Install,deploy(部署,调动)
Debug,测试
(4)Moment → Period
版本控制
加载,动态链接,执行(转储,分析,日志)
多线程
第二节 软件构造的质量目标
一、内容大纲
1.软件系统的质量属性
(1)内部质量因素(internal quality factors)和外部质量因素(external quality factors)
(2)重要外部质量因素
(3)质量因素的折中(tradeoff)
2.软件构造的五个关键的质量目标
Easy to Understand:便于理解:漂亮的代码,可理解性
Ready for change:可变:可维护性和适应性
Cheap to develop:开发花费低:可复用设计,可复用性
Safe from bugs:鲁棒性
Efficient to run:表现
二、要求
1.软件构造中需要关心的质量因素
2.质量因素的重要性
3.了解本课程中每个质量因素需要学习的构造技术。
三、软件系统的质量因素
1.外部因素和内部因素
(1)外部质量因素是用户能感受到的,影响用户的使用。类似运行速度,界面整洁
(2)内部质量影响使用代码的相关人员,影响软件本身和开发者。类似可读性,模块化
(3)最后,只有外部因素才重要。但是内部因素是决定外部因素的,为了让用户享受可见的质量,设计者和实现者必须应用内部技术来确保隐藏的质量。
2.外部因素的类型:Correctness(正确性),Robustness(鲁棒性),Extendibility(易扩展性),Reusability(复用性),Compatibility(兼容性),Efficiency(效率),Portability(可移植性),Ease of use(易用性),Functionality(功能性),Timeliness(时效性),Other qualities(其他性质)
(1)Correctness(正确性)
正确性是软件质量的首要指标,正确性是指符合规约。想要保证正确性,需要同时保证底层和本层的正确性。

测试和调试可以用来保证正确性。静态类型检查,保护性编程,断言等等来保证。
(2)Robustness(鲁棒性)
健壮性是软件系统对异常情况做出适当反应的能力。健壮性是正确性的补充。健壮性可以保证程序不发生灾难性后果。
健壮性同异常情况相关,异常或非异常取决于程序的规约。
(3)Extendibility(易扩展性)
可扩展性是指软件易于调整以适应变化的能力。可扩展性和规模密切相关,软件越大越难以扩展。软件是易变的(fickleness),需要可扩展性。
简化设计:简单的体系结构总是比复杂的体系结构更适合变化
离散化:模块自治性强,变化对其余模块影响小
(4)Reusability(复用性)
软件经常遇到相似的模式,利用共性,避免重复实现,这样可以降低成本
(5)Compatibility(兼容性)
兼容性是将软件元素与其他元素结合起来的容易程度。软件开发时需要相互融合。兼容性难在不同的软件有不同的设定和规定。
兼容性依赖于设计一致性(homogeneity),标准化是解决兼容性的关键:相同的文件格式(相同的字符串等等),相同的数据类型,相同的外观(windows窗口)。
可以通过协议实现更通用的兼容性。
(6)Efficiency(效率)
软件系统对硬件资源尽可能少的需求。
一般最后考虑效率,需要与其他因素折中,必须保证正确性。
(7)Portability(可移植性)
将软件产品转移到各种硬件和软件环境。
(8)Ease of use(易用性)
用户可以轻松掌握软件的使用,运行和监控的容易度。
需要结构简洁。需要理解用户,换位思考,站在用户的角度进行设计。
(9)Functionality(功能性)
系统提供的可能性的范围
蠕变特征(“creeping featurism”),程序设计中一种不适宜的趋势,软件开发者增加越来越多的功能,企图跟上竞争,结果是程序及其复杂,不灵活,占用过多磁盘空间。
一致性(consistency)可能会消失,影响易用性。
过分追求功能,质量也会下降。正确的做法是在提升技术的帮助下,可以在整个项目中保持质量水平不变,而不仅仅是功能性。可以先追求主要功能,质量要高。然后再追求更多功能,保证质量。

(10)Timeliness(时效性)
及时性是指软件系统在用户需要时或之前发布的能力。
一个伟大的软件产品如果出现得太晚,可能会完全失去目标。
(11)Other qualities(其他性质)
Verifiability(可验证性):是否易于检验代码的正确性
Integrity(完整性):软件系统保护其各种组件(程序和数据)免受未经授权的访问和修改的能力。
Repairability(可修复性):是指有助于修复缺陷的能力。
Economy(经济性):同时效性(及时性)相伴,是系统能够按照其分配的预算或低于预算完成的能力。
3.内部质量因素
圈复杂度,用来很难过梁一个模块判定结构的复杂程度。设计中追求高内聚度,低耦合度。可读性、可理解性和清晰性。复杂度。大小
内部质量因素通常用作外部质量因素的部分度量。
4.质量属性的折中(Tradeoff)
有些因素相互之间互相影响,或者矛盾或者相关。
真正的软件工程方法意味着努力清晰地陈述标准,并有意识地做出选择。
正确性是首要的。
正确性和健壮性是相关的。需要系统化的软件构造,格式化的规约,自动检查,好的语言机制,一致性检测工具。
可复用性和易扩展性是相关的,有冲突也有一致。需要模块化得更好。
5.OOP提高质量的方式
正确性:封装,分权制。
鲁棒性:封装,错误处理。
易扩展性:封装,信息隐藏
可复用性:模件性,组件、模型、模式。
兼容性:标准模块及接口。
可移植性:信息隐藏,抽象。
易用性:GUI组件,框架。
效率:复用组件。
时效性:模块化,复用。
经济性:复用。
功能性:易扩展性。
四、五个软件构造中重要的质量目标
1.代码要容易理解,精美的代码

2.可复用性,开发便宜

3.低复杂度,易于改变,易于扩展

4.鲁棒性和正确性

5.性能和效率

第二章 软件构造的过程与工具
第一节 软件生命周期与配置管理
一、内容大纲
1.软件开发的生命周期(SDLC)
2.传统的软件开发的模型
3.敏捷软件开发模型
4.软件配置管理(SCM)
5.Git
二、要求
1.软件开发的基本过程
2.传统软件开发过程模型
3.敏捷开发
4.软件配置管理
5.Git作为配置管理工具
三、软件开发的生命周期
1.软件开发:从无到有
计划→分析→设计→实现→测试和集成→维护→计划
2.

四、传统的软件开发模型
1.两个基本类型:线性过程,迭代过程

2.现有模型
(1)Waterfall (Linear, non-iterative),瀑布模型
(2)Incremental (non-iterative),增量过程
(3)V-model (for verification and validation),V字模型
(4)Prototyping (iterative),原型过程
(5)Spiral (iterative),螺旋模型
3.选择合适的过程模型的依据
(1)用户参与程度——适应变化
(2)开发效率/管理复杂度
(3)开发出的软件的质量
4.Waterfall(瀑布模型):线性推进,阶段划分清楚,整体推进,无迭代,管理简单,无法适应需求的增加和变化。
5. Incremental(增量过程):线性推进,增量式(多个瀑布的串行),无迭代,比较容易适应需求的增加

6.V-Model(V字模型),可以被认为是瀑布模型的扩展,测试的依据。
编码阶段结束后,加工步骤向上弯曲,形成典型的V形。
演示开发生命周期的每个阶段与其相关的测试阶段之间的关系。
水平轴和垂直轴分别表示时间和项目的完整性(从左到右)和抽象级别。

7.Prototyping(原型过程):在原型上持续不断的迭代发现用户变化的需求。
迭代:开发出来之后由用户试用/评审,发现问题反馈给开发者,开发者修改原有的实现,继续交给用户评审。
循环往复这个过程,直到用户满意为止,时间代价高,但开发质量也高。

8.Spiral(螺旋模型):多轮迭代基本遵循瀑布模型,每轮迭代有明确的目标,遵循原型过程,进行严格的风险分析(重要),方可进行下一轮迭代。

五、敏捷开发
1.敏捷开发:通过快速迭代和小规模的持续改进,以快速适应变化。需要设计一个较短的发布周期。需要用户的全程参与,极限的用户参与,极限的小步骤迭代,极限的确认/验证。
2.极限编程:用户来写需求,可以使用CRC card,用小纸条写需求,讲需求,以精炼需求。

燃尽图

第二节 软件构造的过程、系统和工具
一、内容大纲
1.软件构造的一般过程:Design→Programming→Debug→Testing→Build→Release
(1)编程/重构
(2)复查和静态代码分析
(3)调试(转储和日志记录)和测试
(4)动态代码分析/评测
2.狭义的软件构造过程:Build(构建):Validate→Compile→Link→Test→Package→Install→Deploy
(1)构建系统:组件和进程
(2)构建变量和语言。
(3)构建工具。
二、要求
1.广义的软件构造过程,Design→Programming→Debug→Testing→Build→Release
2.Eclipse作为Java构造工具
3.软件构造各阶段常用工具:静态分析,调试(dumping,logging),测试,动态分析
4.狭义的软件构造过程:Build,Validate→Compile→Link→Test→Package→Install→Deploy,验证→编译→链接→测试→包→安装→部署
5.Build工具:make,Ant,Maven,Gradle,Eclipse
三、软件构造的一般过程

1.Programming
(1)从用途上划分
编程语言:C,C++,Java,Python
建模语言:UML
配置语言:XML
构建语言:XML
(2)从形态上划分
基于语言学的构造语言
基于数学形式的构造语言
基于图形的可视化的构造语言
(3)编程语言
1)编程工具:集成开发环境,包括:源代码编辑器,智能代码补全工具,代码重构工具,文件管理,库管理,软件逻辑实体可视化,图形化用户界面构造器,编译器,解释器,自动化构建工具,版本控制系统,外部的第三方工具。
2)Eclipse IDE
(4)建模语言:建模语言是任何可以用来表达信息、知识或系统的人工语言,其结构由一组一致的规则定义,其目标是可视化、推理、验证和交流系统的设计。

UML Class Diagram

(3)配置语言:在运行时更改软件的行为,分离稳定和不稳定部分
配置文件配置程序的参数和初始设置
1)应用程序应该提供工具来创建、修改和验证其配置文件的语法
2)有些计算机程序只在启动时读取它们的配置文件,有些程序则定期检查配置文件的更改
目的:
1)部署环境设置
2)应用程序功能的变量
3)部件之间连接的变量
配置语言示例:Key-Value texts (.ini, .properties, .rc),XML, YAML, JSON
1)适合人类编写:ini > toml> yaml > json > xml > plist
2)可以存储的数据复杂度:xml > yaml > toml ~ json ~ plist > ini
2.审查和静态代码分析:结对编程,走查,正式评审会议,自动化评审
(1)静态代码分析工具:CheckStyle,SpotBugs,PMD
(2)代码审查的目的:改善代码,改善编程技术,
代码评审在诸如Apache和Mozilla这样的开放源码项目中得到了广泛的实践。
代码审查在工业中也被广泛应用。
3.动态代码分析/评测
(1)动态分析:要执行程序并观察现象、收集数据、分析不足。
(2)评测:对代码的运行时状态和性能进行度量,发现代码中的潜在问题。
4.调试和测试
测试:发现程序是否有错误

调试:定位错误,发现错误根源,目的是解决错误。
5.重构:在不改变功能的前提下优化代码
四、狭义的软件构造过程(Build)
粗略的理解:build:build-time → run-time:借助于工具,将软件构造各阶段的活动“自动化”(编译、打包、静态分析、测试、生成文档、部署、…)尽可能脱离“手工作业”,提高构造效率
1.使用Build的典型场景
(1)编写的传统编译语言,如C、C++、java和C语言等。
(2)解释型语言Perl和Python等的软件的打包和测试。
(3)基于web的应用程序的编译和打包。其中包括静态HTML页面、用Java或C编写的源代码、使用JSP(JavaServer页面)、ASP(活动服务器页面)或PHP(超文本预处理器)语法编写的混合文件,以及各种类型的配置文件。
(4)独立于代码的其余部分来执行单元测试以验证软件的一小部分。
(5)执行静态分析工具来识别程序源代码中的错误。这个构建系统的输出是一个bug报告文档,而不是一个可执行程序。
(6)生成PDF或HTML文档。这种类型的构建系统使用各种不同格式的输入文件,但会生成可读的文档作为输出。
2.Build系统的组件
1)版本控制工具
2)源代码树:一个程序的源代码存储为许多磁盘文件。这种不同的文件排列方式称为源代码树。源代码树的结构通常反映软件的体系结构。
3)对象树:一个单独的树层次结构,存储由构建过程构造的任何对象文件或可执行程序。
4)编译工具:将人类可读的源文件转换成机器可读的可执行程序文件的程序。
编译器:源文件→目标文件
链接器:多个可执行对象→链接器映像
基于UML的代码生成器:模型→源代码文件
文档生成器:脚本→文档
发布打包和目标机器:生成一些可以实际安装到用户Machine上的东西。
-从源和对象树中提取相关文件并将其存储在发布包中。
-发布包应该是一个单一的磁盘文件,并且应该被压缩以减少下载所需的时间。
-任何不重要的调试信息都应该删除,这样就不会干扰软件的安装。
包装类型:
-归档文件:压缩和解压缩
-包管理工具:UNIX风格,如.rpm和.deb
-定制的GUI安装工具:Windows风格
(7)构建过程:构建工具调用每个编译工具来完成任务,这是一个端到端的事件序列
(8)如何构建:开发人员构建,发行的构建,健全的构建
3.Build工具

第三章 抽象数据类型和面向对象编程
第一节 数据类型与类型检验
一、内容大纲
1.编程语言的数据类型(Data type)
2.静态数据类型检测和动态数据类型检查
3.可变/不可变的数据类型(Mutability & Immutability)
4.Snapshot
5.复杂的数据类型:Arrays,Collections
6.不可变类型的优点与使用
7.软件构造的理论基础——ADT,软件构造的技术基础——OOP
二、要求
1.静态/动态类型检查
2.可变/不可变的数据类型
3.可变数据类型的危险性:了解别名使用
4.不变数据类型的优越性:可以维护正确性,清晰,可变性
5.用Snapshot理解数据类型
6.用集合类来表达复杂数据类型(Arrays,Collections,Enum)
7.了解空引用的危害并且避免
三、编程语言中的数据类型
1.数据类型:一类值的集合以及可以对这些值执行的操作:boolean、int、double、String等
2.变量:用特定数据类型定义,可以存储满足类型约束的值。
3.基本数据类型:int, long, boolean, double, char,小写开头
4.对象数据类型:String, BigInteger,大写开头

5.对象数据类型的层次结构:所有类都继承于Object类,extends可以省略,这样默认继承Object类
继承从其超类继承可见字段和方法,可以重写方法以更改其行为。
四、静态数据类型检查和动态数据类型检查
1.类型转换:

2.静态类型语言:可以在编译时进行一些检查,避免一些错误
3.动态类型语言:在运行时进行类型检查
4.静态类型检查 > 动态类型检查 > 无检查
5.静态类型检查范围:关于类型的检查
(1)语法错误:变量名,关键词拼写错误
(2)类名/函数名错误
(3)参数数目错误:Math.sin(0,0);
(4)参数类型错误:Math.sin(“sym”);
(5)返回值类型错误:return后面跟了错误类型或者没有返回值,return后面有内容。
6.动态类型检查范围:关于值的检查
(1)非法的参数值:除0
(2)非法的返回值
(3)越界
(4)空指针异常
7.改变一个变量和改变一个变量的值
(1)改变一个变量:将变量指向另一个值的存储空间
(2)改变一个变量的值:该变量当前指向的值的存储空间中写入一个新的值
8.不变性:重要设计原则,不变数据类型:一旦被创建,其值不能改变。如果是引用类型也可以不变:一旦确定指向的对象,就不能再改变,final关键字也属于静态检查。
尽量使用final作为输入参数或局部变量。
final类无法派生子类,final变量无法改变值/引用,final方法无法被子类重写
9.不变对象和可变对象
使用可变数据类型可以获得更好的性能,也适合于在多个模块之间共享数据。
不可变类型更“安全”在其他质量指标上表现更好。
10.对于可变数据类型尽量不要使用多个引用,否则会不安全
五、快照图
用于描述程序运行啥时候的内部状态
2.便于程序员之间的交流,便于刻画各类变量随时间的变化,便于解释设计思路。
3.使用
(1)对于基本类型:用箭头指向即可,不需要画圈

(2)对于对象类型:如果是可变对象,使用单椭圆圈住,如果是不可变对象,用双椭圆

(3)final引用使用双箭头

六、复杂数据类型
1.List
(String应该用双椭圆)
2.Set

3.Map

4.迭代器

第二节 设计规约
一、内容大纲
1.编程语言中的方法/函数
2.规约(Specification):编程中的交流
(1)撰写规约的原因
(2)行为等价
(3)规约结构:前置条件和后置条件
(4)测试和辨别规约
3.设计规约
(1)分类规约
(2)图解规约
(3)规约的质量
二、要求
1.方法的规约
2.前置/后置条件
3.欠定规约,非确定规约
4.陈述式、操作式规约
5.规约的强度及其比较
6.如何写出好的规约
三、编程语言中的方法/函数
1.参数:参数类型匹配在静态检查阶段完成
2.返回值:返回值类型匹配也在静态检查阶段完成
3.“方法”是程序的“积木”,可以被独立开发、测试、复用。使用“方法”的客户端,无需了解方法内部具体如何工作——抽象
4.一个完整的方法包含规约和实现两部分。
四、方法的规约
1.记录假设
(1)变量的数据类型定义
(2)final关键字定义了设计决策:“不可改变”
(3)代码本身蕴含着设计决策,但是远远不够
2.代码中的交流
(1)为什么写出“假设”:自己记不住,别人不懂
(2)代码中蕴含的设计决策给编译器读,注释形式的设计决策给自己和别人读
3.规约的原因
(1)没有规约无法写程序,即使写出来也不知道对错
(2)程序与客户端之间达成的一致
(3)规约给“供需双方”都确定了责任,在调用的时候双方都要遵守
4.规约的原因
(1)很多bug来自于双方之间的误解
(2)不写下来,不同开发者的理解不同
(3)没有规约难以定位错误
(4)需要有精确的规约来区分责任
5.规约
(1)规约可以隔离变化,无需通知客户端
(2)规约可以提高代码效率
(3)规约可以扮演防火墙角色
(4)规约可以内外分割,用户不需要了解具体实现——解耦
6.规约
(1)用户与开发者之间的协议,确定输入输出的类型,确定功能和正确性,性能
(2)只需要讲做了什么,不需要讲怎么实现

五、行为等价性
1.是否可以相互替换,站在客户端的角度看行为等价性
2.是否等价需要根据规约来进行判断,如果都符合相同的规约,那么两个方法就是等价的。
六、规约结构
1.一个方法的规约由几个条款组成:
(1)前提条件,由关键字requires表示
对客户端的约束,在使用方法是必须满足的条件
(2)后置条件,由关键字effects表示
对开发者的约束,方法结束时必须满足的条件
(3)异常行为:如果违反前提条件,它会做什么
2.契约:如果前置条件满足,后置条件必须满足,前置条件不满足,方法可以做任何事
3.方法中不能修改入口参数,除非后置条件声明了
4.尽量返回不可变变量,因为客户端拿回原来的东西,实现者是否不保留返回的对象都是难以规范的。
七、规约分类与强度
1.规约的确定性:一个方法可能对应多个输出,要避免这种情况
2.规约的陈述性:规约只说结果不说过程
3.规约的强度:开发者尽量使用强规约
(1)前置条件更弱,后置条件更强,规约更强
(2)如果强的规约满足了就一定满足弱的规约,此时可以用强的规约替代弱的规约
4.当一个规约更强,更少的实现可以满足,更多的用户可以使用,实现者的责任更重,客户的责任更轻。
八、图解规约
1.点代表方法实现
2.规范在所有可能实现的空间中定义一个区域。
3.如果满足规约,则落在其范围内,否则在其之外。
4.程序员可以在规约的范围内自由选择实现方式,客户端无需了解具体使用了哪个实现
5.更强的规约,表达更小的区域

九、规约质量
1.一个好的“方法”设计,并不是你的代码写的多么好,而是对spec的设计,一方面用户用着舒服,另一方面,开发者编的舒服
(1)内聚性:描述功能单一、简单、易理解,如果规约做了两件事,就需要分割成两个方法。
(2)规约的强度足够
(3)特殊情况下太强的规约会难以达到,此时才降低强度
(4)在规约里使用抽象类型,可以给方法实现体与客户端更大的自由度
(5)客户不喜欢太强的前置条件,一般做法是前置条件弱,然后throw expectation.
第三节 抽象数据类型(ADT)
一、内容大纲
1.Abstraction and User-Defined Types,抽象和用户定义类型
2.Classification of operations in ADT,ADT中的操作分类
3. Abstract Data Type Examples,抽象数据类型示例
4.Design principles of ADT,ADT的设计原则
5.Representation Independence (RI),表示不变性(RI)
6.Realizing ADT Concepts in Java,用Java实现ADT概念
7.Testing an ADT,测试ADT
8.Invariants,不变量
9.Rep Invariant and Abstraction Function,Rep不变量与抽象函数
10.Beneficent mutation (有益突变),有益突变
11.Documenting the AF, RI, and Safety from Rep Exposure,记录AF、RI和Rep泄漏的安全性
12.ADT invariants replace preconditions,ADT替换前置条件
二、要求
1.抽象数据类型与表示独立性:如何设计良好的抽象数据结构,通过封装来避免客户端获取数据的内部表示(即“表示泄露”),避免潜在的bug——在client和implementer之间建立“防火墙”
(1)抽象数据类型解决了一个特别危险的问题:用户假设类型的内部表示。
(2)我们要知道为什么要避免这件事。
(3)我们还将讨论操作的分类,以及一些设计抽象数据类型的原则。
2.不变量、表示泄漏、抽象函数(AF)和表示不变量(RI)
通过抽象函数和表示不变量的概念,对类实现ADT意味着什么的更正式的数学概念。
(1)这些数学概念在软件设计中非常实用。
(2)抽象函数将为我们提供一种在抽象数据类型上清晰定义相等操作的方法。
(3)rep不变量将使捕获由损坏的数据结构引起的错误变得更容易。
ADT的特性:不变量、表示泄漏、抽象函数AF、表示不变量RI
基于数学的形式对ADT的这些核心特征进行描述并应用于设计中。
三、抽象和用户定义类型
1.除了编程语言所提供的基本数据类型和对象数据类型,程序员可以定义自己的数据类型。
2.数据抽象:由一组操作所刻画的数据类型
3.传统类型定义:关注数据的具体表示
4.抽象类型:强调“作用于数上的操作”,程序员和用户无需关系数据是如何存储的,只需设计/使用操作即可。
5.注意:抽象数据类型是由它的操作定义的。与他内部实现无关

6.ADT的关键:
(1)抽象:用更简单,更高层次的想法来省略或隐藏低级细节
(2)模块化:将系统划分为组件或模块,每个组件或模块可以与系统的其余部分分开设计,实现,测试,推理和重用。
(3)封装:围绕模块构建墙壁,以便模块只负责其自身的内部行为,并且系统其他部分中的错误不会损害其完整性。
(4)信息隐藏:从系统的其余部分隐藏模块实现的详细信息,以便稍后可以更改这些详细信息,而无需更改系统的其余部分。
(5)关注点分离:模块具有单独的责任,不要将一个责任分散在不同的模块中
四、ADT中的操作分类
1.可变和不可变数据类型
2.分类
(1)构造器(Creator):生成一个新的对象:
1)构造函数或静态函数:Arrays.asList(),List.of()
2)工厂方法
(2)生产器(Producer):根据老对象生成新对象:String.trim()
(3)观察器(Observer):获得对象的信息:List.size()
(4)变值器(Mutator):改变对象属性:List.add()
1)通常返回void,也可能返回boolean

3.immutable类型没有Mutator
五、ADT的设计
设计好的ADT,靠“经验法则”,提供一组操作,设计其行为规约spec
1.设计简洁、一致的操作
2.要足以支持client对数据所做的所有操作需要,且用操作满足client需要的难度要低。如List需要size()操作,get()操作,否则就得用户自己遍历,很麻烦
六、表示独立性
1.表示独立性:client使用ADT时无需考虑其内部如何实现,ADT内部表示的变化不应影响外部spec和客户端。例如我们可以用LinkedList表示Array
2.除非ADT的操作指明了具体的pre-condition和post-condition,否则不能改变ADT的内部表示——Spec规定了client和implementer之间的契约。
七、测试ADT
1.测试creators, producers, and mutators:调用observers来观察这些operations的结果是否满足spec
2.测试observers:调用creators, producers, and mutators等方法产生或改变对象,来看结果是李正确。
3.风险:如果被依赖的其他方法有错误,可能导致被测试方法的测试结果失效。

八、不变量
1.保持不变量是好的ADT中非常重要的性质
2.不变量:在任何时候总是true
3.不变量需要ADT来负责,与用户的任何行为无关
4.需要不变量:保持程序的正确性,容易发现错误(总要假设用户恶意破坏不变量
5.Immutability是不变量的一种类型
6.使用public类型会表示泄漏,不仅影响不变性,也影响独立性:无法在不影响客户端的情况下改变其内部值。
7.需要把属性全部改为private,final也可以确保不变。
8.有时可以用复制来构建新对象来维护不变性,但复制代价很高,也有可能产生很多bug
9.除非万不得已,ADT有责任保证自己的不变量,放置泄漏
八、表示不变性(RI)和抽象函数
1.表示空间和抽象空间
(1)一般情况下ADT的表示比较简单,有些时候需要复杂表示
(2)®表示空间:所有可能值的集合,ADT开发者关注
(3)(A)抽象空间:抽象值的空间:client看到和使用的值,client关注

(4)抽象函数(AF):R→A,必须是满射
(5)RI:R→boolean另一种重要的ADT不变性
对于表示值r,RI®为true当且仅当r被AF映射。换句话说,RI告诉我们给定的rep值是否格式良好。或者,您可以将RI看作一个集合:它是定义AF的rep值的子集。
表示不变性RI:某个具体的“表示”是否是“合法的”
也可将RI看作:所有表示值的一个子集,包含了所有合法的表示值
也可将RI看作:一个条件,描述了什么是“合法”的表示值

(6)独立的抽象空间无法决定AF或RI,不同的内部表示需要设计不同的AF和RI
选择某种特定的表示方式R,进而指定某个子集是“合法”的(RI),并为该子集中的每个值做出“解释”(AF)——即如何映射到抽象空间中的值。
例:

即使同样的R同样的RI也可能有不同的AF即解释不同

2.RI和AF影响ADT的设计
设计ADT:
(1)选择R和A
(2)RI——合法的表示值
(3)如何解释合法的表示值——映射AF
做出具体的解释:每个rep value如何映射到abstract value
需要将以上内容注释在代码中
3.随时检查RI是否满足checkRep()需要使用assert。所有可能改变REP的方法都需要检查,Observer可以不检查,但是以防万一还是建议检查

第四节 面向对象的编程(OOP)
一、内容大纲
1.OOP的基本概念
2.OOP的独特特性
(1)封装与信息隐藏
(2)继承(Inheritance)和重写(Overiding)
(3)多态、子类型、重载
3.一些重要的Object方法
4.设计好的类
5.OOP的历史
二、要求
1.将抽象数据类型的接口与其实现分离,并使用Java接口类型来实现这种分离。
2.用接口定义ADT,并编写实现接口的类
3.用OOP/接口/类实现ADT
三、基本概念:object对象,class类,attribute属性,和method方法
1.静态方法和实例方法
(1)类成员变量:与类相关联的变量,而不是与类的实例关联的变量。您还可以将方法与类相关联——类方法。静态方法,堆中
(2)不是类方法或类变量的方法和变量称为实例方法和实例变量。动态方法,栈中
(右边是heap)
四、接口和枚举类型
1.接口
(1)Interface和Class:定义和实现ADT
(2)接口之间可以继承与扩展
(3)一个类可以实现多个接口(从而具备了多个接口中的方法) .
(4)一个接口可以有多种实现类
2.接口:确定ADT规约,类:实现ADT
(1)也可以不需要接口,直接使用类作为ADT,既有ADT定义也有ADT实现
(2)实际中更倾向于使用接口定义
(3)接口中要使用静态构造器
3.继承和重写:
(1)继承:子类可以重写父类中的方法(严格继承:子类只能添加新方法,无法重写超类中的方法(可以用final修饰方法,就无法重写))
(2)重写:重写的函数完全相同的signature签名(返回值和参数)
(3)重写时不要改变原方法的本意
五、抽象类和抽象方法
1.抽象方法:
(1)有签名但没有实现的方法(也称为抽象操作)
(2)由关键字abstract定义
2.抽象类:
至少包含一个抽象方法的类称为抽象类
3.接口:只有抽象方法的抽象类
接口主要用于系统或子系统的规范。实现由子类或其他机制提供。
4.具体类>抽象类>接口

六、多态(Polymorphism)、子类型(subtyping)、重载(overloading)
1.三类多态
多态指一个函数名实现多个功能。
(1)特殊多态:功能重载,不同的类型表现出不同的行为
(2)参数多态:参数不同
(3)子类多态

重载:多个方法具有相同的名字,但有不同的参数列表或返回值类型。是静态方法,接受静态检查。
价值:方便client调用,client可用不同的参数列表,调用同样的函数
静态多态:根据参数列表进行最佳匹配,受到静态类型检查,在编译阶段决定执行哪个方法。
相反Override是在run-time进行动态检查
规则:必须有不同的参数列表,可以有相同或不同的返回值类型,可以有相同/不同的public/private/protected关键字,可以生命新的或者原有的异常。可以在同一个类内重载,也可以在子类中重载。

七、参数多态和泛型
1.当一个函数在一系列类型上一致工作时,就会获得参数多态性;这些类型通常表现出一些共同的结构
它是一种以通用方式定义函数和类型的能力,以便它基于运行时传递的参数工作,即允许静态类型检查而不完全指定类型
这就是Java中所谓的“泛型”。
2.泛型编程是一种编程风格,其中数据类型和函数是按照稍后指定的类型编写的,然后在需要时为作为参数提供的特定类型实例化。
3.使用菱形运算符<>来帮助声明类型变量。
4.不能使用泛型数组,无法通过编译,运行时泛型会消失。
八、子类多态

1.B是A的子类,那么每个B都是A
2.子类型的规约不能比超类弱
3.子类型多态:不同类型的对象可以由用户统一的处理而无需区分
4.类型转换:避免向下类型转换

第五节 ADT和OOP之间的等价性
一、内容大纲
1.等价关系
2.不可变数据类型的等价
3.==和.equals()
4.Object合同
5.可变数据类型的等价性
6.自动封装和等价
二、要求
1.等价关系
2.站在观察者角度,利用AF,定义不可变对象之间的等价关系
3.引用等价性和对象等价性
4.可变数据类型的观察等价性和行为等价性
5.理解Object的契约,正确实现等价关系判定
三、等价关系
1.ADT是对数据的抽象,体现为一组对数据的操作
2.抽象反射弧AF:内部表示→抽象表示
3.基于抽象函数AF定义ADT的等价操作
4.现实中每个对象实体都是独特的
5.所以无法完全相等,但有“相似性”
6.在数学中,“绝对相等”是存在的
7.等价关系:自反对称传递
三、不可变类型的等价性
1.将数据类型的具体实例映射到其相应的抽象值,AF映射到相同的结果则等价,即用关系来判定是否等价。
2.等价关系产生抽象函数,抽象函数所诱导的关系是等价关系
3.站在外部观察者角度:对两个对象调用任何相同的操作,都会得到相同的结果,则认为这
两个对象是等价的。反之亦然。
四、==和.equals()
1.代表引用等价性,即指针指向同一地址
2…equals()代表对象等价性,在自定义ADT时,需要重写Object的equals()
3.对基本数据类型,使用
判定,对于对象类型,使用equals()
五、不可变类型的等价性
1.在Object中实现的缺省equals()是在判断引用等价性,因此需要重写(不能使用重载)
2.instanceof用于测试一个对象是否为一个类的实例,正能在equals中使用,否则容易出现bug。可以用多态避免使用
六、对象契约
1.等价关系:自反传递对称
2.除非对象被修改了,否则多次调用equals应得到同样的结果
3.“相等”的对象,hashCode()必须一致
七、可变数据类型的等价
1.观察等价性:在不改变状态的情况下,两个mutable对象是否看起来一致
2.行为等价性:调用对象的任何方法都展示出一致的结果
3.对可变类型:往往倾向于实现严格的观察等价性,但有些时候,观察等价性可能导致bug,是指破坏RI。如果某个mutable的对象包含在Set集合类中,当其发生改变后,集合类的行为不确定,务必小心
4.JDK提供的Mutable类的equals方法思路并不相同
5.实际上对于可变类型,实现行为等价性即可,也就是说,只有指向相同内存空间的对象,才是相等的。所以对于可变类型,无需重写hashCode和equals
6.如果一定要判断两个可变对象看起来是否一致,最好定义一个新的方法
八、自动转换和等价

第四章 面向可复用性的软件构建方法
第一节 可复用性的度量、形态与外部表现
一、内容大纲
1.什么是软件复用
2.如何构造“可复用性”
3.可复用组件的级别和形态
(1)源代码级别的复用
(2)模块级别的复用:类/抽象类/接口
(3)库级别的复用:API/包
(4)系统级别的复用:框架
4.可复用性的外部观察
(1)类型可变
(2)功能分组
(3)实施可变更
(4)表示独立
(5)共性抽取
二、要求
1.软件复用的优缺点
2.复用构造
3.可复用组件的特征
4.开发便携式应用系统的方法
三、软件复用
1.软件复用是使用现有软件组件实现或更新软件系统的过程。
2.软件复用的两个观点:
(1)面向复用编程:开发出可复用的软件
(2)基于复用编程:利用已有的可复用软件搭建应用系统
3.特点:
(1)很大的适应性
(2)降低成本和开发时间
(3)充分的测试→高可靠
(4)标准化、一致化
(5)针对性不强→性能差
4.为什么复用
(1)降低成本和开发时间
(2)经过充分的测试,可靠,稳定
(3)标准化,在不同的应用中保持一致
5.做到复用需要一些代价,面向复用或是基于复用代价都很高
6.开发可复用的软件
(1)开发成本高于一般软件的成本:需要足够高的适应性
(2)性能差些:针对更普适的场景,缺少足够的针对性
7.使用已有的软件开发
(1)可复用软件库,对其进行有效的管理。
(2)往往无法直接拿来就用,需要适配器。
三、度量可复用性
1.复用的机会有多频繁,复用的场合有多少
2.复用的代价有多大
(1)搜索、获取的代价
(2)适配、扩展的代价
(3)实例化的代价
(4)与软件其他部分互连的难度
3.复用性可以体现在,构建,打包,分配,安装,配置,部署,维护和升级阶段等等
4.高可复用性:小、简单,与标准兼容,灵活可变,可扩展,泛型、参数化,模块化,变化的局部性,稳定,丰富的文档和帮助
三、可复用组件的级别和形态
1.最主要的复用是在代码层面。但软件构造过程中的任何实体都可能被复用,包括需求,设计,规约,数据,测试用例,文档等等
2.
(1)源代码级别:方法,陈述等等
(2)模块级别:类,接口
(3)库级别:API
(4)系统级别:框架
3.白盒复用:源代码可见,可修改和扩展
(1)复制已有代码于正在开发的系统,进行修改
(2)可定制化程度高
(3)对其修改增加了软件的复杂度,且需要对其内部充分的了解
4.黑盒复用:源代码不可见,不能修改
(1)只能通过API接口来使用,无法修改代码
(2)简单清晰但是适应性差些
5.可复用组件分发格式
(1)类型
1)源代码
2)包,如.jar, .gem, .dll
(2)可复用软件组件的来源:
1)组织的内部代码库(Guava)
2)第三方提供的库(Apache)
3)语言自身提供的库(JDK)
4)代码示例
5)来自同事
6)已有系统内的代码
7)开源软件的代码
6.代码复用是一门技术,复制代码使用是很困难的。
相关研究:如何从互联网上快速找到需要的代码片段?
反向研究:如何从源代码中检测出克隆代码(clone code)?
7.模块级别的复用:直接用jar/zip,只需要包括classpath即可,可以使用javap工具
(1)文档非常重要(Java API)
(2)封装有助于复用
(3)管理的代码更少
(4)版本控制,向后兼容性仍然存在问题
(5)需要将相关类打包在一起——静态链接
8.类的复用可以使用继承,然后重写。也可以使用委托。
9.库层面的复用
(1)库是一类方法的集合
(2)框架可以让代码嵌入
(3)框架与库可以说是相反的,库是你调用它的代码,框架是它调用你的代码

10.好的API:易学,易于使用,即使没有文档,难以滥用,易于阅读和维护使用它的代码,足够强大以满足要求,易于进化,适合观众。
11.系统级别复用:框架
(1)框架:一组具体类,抽象类,及其之间的连接关系。开发者根据framework的规约,填充自己的代码进去,形成完整系统。
(2)领域知识的复用:将framework看作是更大规模的API复用,除了提供可复用的API,还将这些模块之间的关系都确定下来,形成了整体应用的领域复用。
(3)框架设计:黑盒框架和白盒框架
黑盒框架:通过实现特定的接口/委派进行框架扩展
白盒框架:通过代码层面的继承进行框架扩展
五、可复用性的外部观察特性
1.类型可变
(1)参数类型可以是泛型,其是可变的
2.功能分组
提供完备的细粒度操作,保证功能的完整性,不同场景下复用不同的操作。
3.实现可变
(1)ADT有多种不同的实现,提供不同的R和AF,但具有相同的规约,以适应不同的应用场景。
4.表示独立
内部实现可能经常变换,但客户端不受影响
5.共性抽取
将共同的行为(共性)抽象出来,形成可复用的实体:父类,抽象类
只要看到自己写了相同的代码,就想办法进行复用
第二节 面向复用的软件构造技术
一、内容大纲
1.设计可复用的类
(1)继承和重写
(2)重载
(3)参数多态和泛型编程
(4)行为子类型和里氏替换原则
(5)组合和委托
2.设计可复用库和框架
(1)API和库
(2)框架
二、要求

三、设计可复用的类
1.OOP中的复用类设计
(1)封装和信息隐藏
(2)继承和重写
(3)多态,子类型和冲载
(4)泛型编程
2.行为子类型
(1)子类型多态:客户端可用统一的方式处理不同类型的对象

3.里氏替换原则LSP
如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有发生变化,那么类型S是类型T的子类型。
所有引用基类的地方必须能透明地使用其子类。
(1)子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法;
(2)子类中可以增加自己特有的方法。
4.行为子类型的要求(静态检查内容)
(1)子类型可以增加方法,但不可删除
(2)子类型需要实现抽象类型中所有未实现方法
(3)子类型中重写的方法必须有相同或子类型返回值或者符合co-variance的参数
(4)子类型中重写的方法必须使用同样类型的参数或者符合contra-variance的参数
(5)子类型中重写的方法不能抛出额外的异常
规约方面
(1)更强或相等的不变量
(2)更弱或相等的前置条件
(3)更强或相等的后置条件
5.强行为子类型
(1)前置条件不能强化
(2)后置条件不能弱化
(3)不变量要保持
(4)子类型方法参数:逆变
(5)子类型方法的返回值:协变
(6)异常类型:协变
协变:父类型变成子类型
逆变:子类型变成父类型
Arrays是协变的

6.泛型中的LSP
(1)List并不是List的子类型。泛型不是协变的

(2)通配符(?)
想要编写一个方法打印List, List,或List的所有元素
不行,
彳亍!
下界通配符:,上界通配符

7.继承复用的优缺点
(1)通常类的复用分为继承复用和合成复用两种,继承复用虽
然有简单和易实现的优点。
(2)继承复用破坏了类的封装性。因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为“白箱”复用。
(3)子类与父类的耦合度高。父类的实现的任何改变都会导致子类的实现发生变化,这不利于类的扩展与维护。
(4)它限制了复用的灵活性。从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可能发生变化
8.继承复用有时会很复杂
汽车按“动力源”划分可分为汽油汽车、电动汽车等;按“颜色”划分可分为白色汽车、黑色汽车和红色汽车等。
如果同时考虑这两种分类,其组合就很多
四、委托和合成
1.委派/委托:一个对象请求另一个对象的某些功能
2.委派是复用的一种常见形式
3.明确授权和隐式委派两种
4.继承和委派
如果子类只需要复用父类中的一小部分方法,可以不需要继承,而是通过委派机制来实现
一个类不需要继承另一个类的全部方法,通过委托机制调用部分方法。
5.复合超继承原则
(1)合成复用原则(CRP)
合成/聚合复用原则是在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分;新的对象通过向这些对象的委派达到复用的目的。
委托发生在Object层面,继承发生在Class层面
6.普遍做法
使用接口定义系统必须对外展示的不同侧面的行为
接口之间通过extends实现行为的扩展(接口组合)
类implements组合接口
从而规避了复杂的继承关系

1.委派的类型:依赖,关联,聚合/合成关系

(1)依赖:临时性的delegation,对象之间动态的,临时的同心练习

(2)关联:对象之间的长期静态联系

(3)合成(Composition):更强的关系,但是难以变化,是一种将简单对象或数据类型合并为更复杂对象或数据类型的方法

(4)聚合:更弱的关系,可以动态变化

第三节 面向复用的设计模式
一、内容大纲
1.结构模式
(1)适配器模式Adapter:适配器允许具有不兼容接口的类一起工作,方法是将自己的接口包装在已存在类的接口周围。
(2)装饰模式Decorator:在对象的现有方法中动态添加/重写行为。
(3)Façade模式:为大量代码提供了一个简化的接口。
2.行为模式
(1)策略模式Strategy:允许在运行时动态选择一系列算法。
(2)模板方法Template Method:将算法的框架定义为抽象类,允许其子类提供具体的行为。
(3)迭代器模式Iterator:按顺序访问对象的元素,而不公开其底层表示。
二、设计模式
1.结构型模式
(1)代理(Proxy)模式:为某对象提供一种代理以控制对该对象的访问。即客户端通过代理间接地访问该对象,从而限制、增强或修改该对象的一些特性。
(2)适配器(Adapter)模式:将一个类的接口转换成客户希望的另外-一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。
(3)桥接(Bridge)模式:将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现的,从而降低了抽象和实现这两个可变维度的耦合度。
(4)装饰(Decorator)模式:动态地给对象增加一些职责,即增加其额外的功能。
(5)外观(Facade)模式:为多个复杂的子系统提供一个一致的接口, 使这些子系统更加容易被访问。
(6)享元(Flyweight)模式:运用共享技术来有效地支持大量细粒度对象的复用。
(7)组合(Composite)模式:将对象组合成树状层次结构,使用户对单个对象和组合对象具有一致的访问性。
2.行为型模式
(1)模板方法(Template Method)模式:定义一个操作中的算法骨架,将算法的一些步骤延迟到子类中,使得子类在可以不改变该算法结构的情况下重定义该算法的某些特定步骤。
(2)策略(Strategy)模式:定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的改变不会影响使用算法的客户。
(3)迭代器(Iterator)模式:提供一种方法来顺序访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示。
(4)访问者(Visitor)模式:在不改变集合元素的前提下,为一个集合中的每个元素提供多种访问方式,即每个元素有多个访问者对象访问。
(5)备忘录(Memento)模式:在不破坏封装性的前提下,获取并保存一个对象的内部状态,以便以后恢复它。
(6)解释器(Interpreter)模式:提供如何定义语言的文法,以及对语言句子的解释方法即解释器。
(7)命令(Command)模式:将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开。
(8)职责链(Chain of Responsibility)模式:把请求从链中的一个对象传到下个对象,直到请求被响应为止。通过这种方式去除对象之间的耦合。
(9)状态(State)模式:允许一个对象在其内部状态发生改变时改变其行为能力。
(10)观察者(Observer)模式:多个对象间存在一对多关系,当-一个对象发生改变时,把这种改变通知给其他多个对象,从而影响其他对象的行为。
(11)中介者(Mediator)模式:定义一个中介对象来简化原有对象之间的交互关系,降低系统中对象间的耦合度,使原有对象之间不必相互了解。
三、可复用的设计模式
1.使用原因:灵活的改变,易于修复,易于增加新功能
2.设计模式分类
(1)创建型模式:如何创建对象
(2)结构型模式:如何组合类和对象
(3)行为型模式:如何交互和分配责任
3.适配器模式Adapter
(1)将一个类的接口转换成客户希望的另外一个接口。Adapter模式使原本由于接口不兼容而不能一起工作的类可以一起工作。
(2)适配器模式分为类结构型模式和对象结构型模式两种,前者类之间的耦合度比后者高,且要求程序员了解现有组件库中的相关组件的内部结构,所以应用相对较少些。
(3)优缺点
1)该模式的主要优点如下:
客户端通过适配器可以透明地调用目标接口。
复用了现存的类,程序员不需要修改原有代码而重用现有的适配者类。
将目标类和适配者类解耦,解决了目标类和适配者类接口不一致的问题。
2)其缺点是:
对类适配器来说,更换适配器的实现过程比较复杂。
(4)意图:将类的借口转换为客户端期望的另一个接口。可以解决类之间接口不兼容的问题,为已有的类提供新的接口
(5)目标:对旧的不兼容组件进行包装,在新系统中使用旧的组件
(6)例:通过增加额外的间接层来解决不协调/不兼容的问题

可以使用适配器模式来使用同事已经编写过的代码。
(7)适配器可以用继承也可以用委托

4.装饰模式Decorator
(1)装饰(Decorator)模式的定义:
指在不改变现有对象结构的情况下,动态地给该对象增加一些职责(即增加其额外功能)的模式,它属于对象结构型模式。
(2)装饰(Decorator)模式的主要优点有:
采用装饰模式扩展对象的功能比采用继承方式更加灵活。
可以设计出多个不同的具体装饰类,创造出多个不同行为的组合。
(3)其主要缺点是:
装饰模式增加了许多子类,如果过度使用会使程序变得很复杂。
(4)通常情况下,扩展一个类的功能会使用继承方式来实现。但继承具有静态特征,耦合度高,并且随着扩展功能的增多,子类会很膨胀。如果使用组合关系来创建一个包装对象(即装饰对象)来包裹真实对象,并在保持真实对象的类结构不变的前提下,为其提供额外的功能,这就是装饰模式的目标。
(5)模式的结构:装饰模式主要包含以下角色。
1)抽象构件(Component)角色:定义一个抽象接口以规范准 备接收附加责任的对象。
2)具体构件(Concrete Component)角色:实现抽象构件,通过装饰角色为其添加一些职责。
3)抽象装饰(Decorator)角色:继承抽象构件,并包含具体构件的实例,可以通过其子类扩展具体构件的功能。
4)具体装饰(Concrete Decorator)角色:实现抽象装饰的相关方法,并给具体构件对象添加附加的责任。

(6)装饰模式的应用场景
当需要给一个现有类添加附加职责,而又不能采用生成子类的方法进行扩充时。例如,该类被隐藏或者该类是终极类或者采用继承方式会产生大量的子类。
当需要通过对现有的一组基本功能进行排列组合而产生非常多的功能时,采用继承关系很难实现,而采用装饰模式却很好实现。
当对象的功能要求可以动态地添加,也可以再动态地撤销时。
装饰模式在Java语言中的最著名的应用莫过于Java I/O标准库的设计了。例如,InputStream的子类FilterInputStream, OutputStream的子类FilterOutputStream, Reader的子类BufferedReader以及FilterReader,还有Writer的子类BufferedWriter、FilterWriter以及PrintWriter等,它们都是抽象装饰类。
(7)问题:需要对对象进行任意或者动态的扩展组合
(8)方案:实现一个通用接口作为要扩展的对象,将主要功能委托给基础对象,然后添加功能。以递归的方式实现。
接口:定义装饰物执行的公共操作
起始对象,在其基础上增加功能(装饰),将通用的方法放到此对象中。
Decorator抽象类是所有装饰类的基类,里面包含的成员变量component指向了被装饰的对象。

例子:

5.Façade外观模式
(1)问题:调用者需要一个简化的接口来调用复杂系统的整体功能
(2)方案:提供更高层次的接口来使子系统易于使用。
(3)外观(Facade)模式的定义:是一种通过为多个复杂的子系统提供一个一致的接口,而使这些子系统更加容易被访问的模式。该模式对外有一个统一接口,外部应用程序不用关心内部子系统的具体的细节,这样会大大降低应用程序的复杂度,提高了程序的可维护性。
(4)外观(Facade) 模式是“迪米特法则”的典型应用,它有以下主要优点。
1)降低了子系统与客户端之间的耦合度,使得子系统的变化不会影响调用它的客户类。
2)对客户屏蔽了子系统组件,减少了客户处理的对象数目,并使得子系统使用起来更加容易。
3)降低了大型软件系统中的编译依赖性,简化了系统在不同平台之间的移植过程,因为编译一个子系统不会影响其他的子系统,也不会影响外观对象。
(5)外观(Facade)模式的主要缺点如下。
1)不能很好地限制客户使用子系统类。
2)增加新的子系统可能需要修外观类或客户端的源代码,违背了“开闭原则”。
(6)迪米特法则的定义是:只与你的直接朋友交谈,不跟“陌生人”说话。其含义是:如果两个软件实体无须直接通信,那么就不应当生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。
(7)开闭原则:软件实体应当对扩展开放,对修改关闭。
(8)外观(Facade)模式的结构比较简单,主要是定义了一个高层接口。它包含了对各个子系统的引用,客户端可以通过它访问各个子系统的功能。现在来分析其基本结构和实现方法。模式的结构:外观(Facade)模式包含以下主要角色:
1)外观(Facade)角色:为多个子系统对外提供一个共同的接口。
2)子系统(Sub System)角色:实现系统的部分功能,客户可以通过外观角色访问它。
3)客户(Client)角色:通过一个外观角色访问各个子系统的功能。

(9)通常在以下情况下可以考虑使用外观模式。
1)对分层结构系统构建时,使用外观模式定义子系统中每层的入口点可以简化子系统之间的依赖关系。
2)当一个复杂系统的子系统很多时,外观模式可以为系统设计一个简单的接口供外界访问。(例如:银行系统)
3)当客户端与多个子系统之间存在很大的联系时,引入外观模式可将它们分离,从而提高子系统的独立性和可移植性。
(10)在外观模式中,当增加或移除子系统时需要修改外观类,这违背了“开闭原则”。如果引入抽象外观类,则在一定程度上解决了该问题。

6.策略模式strategy
(1)案例:跨国公司做生意时候,不同国家计算税收的方法不一样,之前一般的处理思路是复制粘贴,switch或if,变量制定,函数指针或者委托继承
1)对于switch和if如果需要满足的问题更多,比如加拿大可以有英语区,有法语区,就会组合爆炸
2)使用继承:重用SalesOrder对象,将新的缴税规则看成新种类的销售订单,只是缴税规则不一样。看上去很好但是如果有新的变化如日期格式,运费规则,就会导致很深的继承层,难以理解,冗余性高,难以测试。
(2)考虑:
设计中什么应该是可变的
对变化的概念进行封装
优先使用对象聚集,而不是类继承
改变思路:
寻找变化,将其封装在一个单独的类中
将此类包含在另一个类中
本例中,预先要分析出缴税规则是确定变化的,将其封装,创建一个抽象类定义:如何在概念上完成税额计算,然后为每一变化派生具体的类。

(3)问题:针对特定任务存在多种算法,条用着需要根据上下文环境动态的选择和切换
(4)例子:排序算法的接口都是一致的,因此定义一个算法的接口,每个算法用一个类来实现,客户端针对接口编写程序。这样想用哪个算法用哪个
(5)优点
1)易于扩展新算法实现
2)将算法与客户端上下文分离

7.模板方法Template Method
(1)问题:不同的客户端具有相同的算法步骤,但是每个步骤的具体实现不同
(2)例子:执行测试用例的测试套件。打开、读、写不同类型的文件
(3)方案:在父类中定义通用逻辑和各步骤的抽象方法声明,对于每个抽象步骤具体实现

比如,做两种不同的汽车,一种保时捷,另一种金龟车,做法是不一样的

抽象模板

具体实现

(4)应用方法:在父类声明一个通用逻辑,模板模式用继承+重写的方式实现算法的不同部分。策略模式用委托机制实现不同完整算法的调用(接口+多态)。模板策略在框架中应用广泛:框架实现了算法的不变性,客户端提供每步的具体实现。
8.迭代器模式Iterator
(1)现实问题
在现实生活以及程序设计中,经常要访问一个聚合对象中的各个元素如“数据结构”中的链表遍历,通常的做法是将链表的创建和遍历都放在同一个类中,但这种方式不利于程序的扩展,如果要更换遍历方法就必须修改程序源代码,这违背了“开闭原则”。
既然将遍历方法封装在聚合类中不可取,那么聚合类中不提供遍历方法,将遍历方法由用户自己实现是否可行呢?答案是同样不可取,因为这种方式会存在两个缺点:暴露了聚合类的内部表示,使其数据不安全。增加了客户的负担。
“迭代器模式”能较好地克服以上缺点,它在客户访问类与聚合类之间插入一个迭代器,这分离了聚合对象与其遍历行为,对客户也隐藏了其内部细节,且满足“单一职责原则”和“开闭原则”,如Java中的Collection、List、Set、Map等都包含了迭代器。
(2)迭代器(Iterator)模式的定义:提供一个对象来顺序访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示。迭代器模式是一种对象行为型模式。
(3)其主要优点如下。
1)访问一个聚合对象的内容而无须暴露它的内部表示。
2)遍历任务交由迭代器完成,这简化了聚合类。
3)它支持以不同方式遍历一个聚合,甚至可以自定义迭代器的子类以支持新的遍历。
4)增加新的聚合类和迭代器类都很方便,无须修改原有代码。
5)封装性良好,为遍历不同的聚合结构提供一个统一的接口。
(4)其主要缺点是:增加了类的个数,这在一定程度上增加了系统的复杂性。
(5)迭代器模式是通过将聚合对象的遍历行为分离出=来,抽象成迭代器类来实现的,其目的是在不暴露聚合对象的内部结构的情况下,让外部代码透明地访问聚合的内部数据。现在我们来分析其基本结构与实现方法。
1)模式的结构:迭代器模式主要包含以下角色。
抽象聚合(Aggregate)角色:定义存储、添加、删除聚合对象以及创建迭代器对象的接口。
具体聚合(Concrete Aggregate)角色:实现抽象聚合类,返回一个具体迭代器的实例。
抽象迭代器(Iterator)角色:定义访问和遍历聚合元素的接口,通常包含hasNext()、first()、next()等方法。
具体迭代器(Concrete Iterator)角色:实现抽象迭代器接口中所定义的方法,完成对聚合对象的遍历,记录遍历的当前位置。

(6)模式的应用场景:迭代器模式通常在以下几种情况使用。
1)当需要为聚合对象提供多种遍历方式时。
2)当需要为遍历不同的聚合结构提供一个统一的接口时。
3)当访问一个聚合对象的内容而无须暴露其内部细节的表示时。
(7)由于聚合与迭代器的关系非常密切,所以大多数语言在实现聚合类时都提供了迭代器类,因此大数情况下使用语言中已有的聚合类的迭代器就已经够了。
(8)问题:客户端需要以统的、与元素类型无关的方式访问容器中的所有元素
(9)方案:一种面向迭代的策略模式
(10)结果:隐藏了容器的内部实现。用统一的接口支持多种遍历策略。易于更换容器类型。促进程序各部分之间的通信
(11)模式结构:定义迭代器接口,实现迭代功能

第五章 软件构造的过程与工具
第一节 可维护性的度量与构造原则
一、内容大纲
1.软件维护与演化
2.可维护性指标
3.模块化设计与模块化原则
4.OO设计原则:SOLID
5.OO设计原则:GRASP
二、软件维护
1.软件维护:修复错误,改善性能(软件生命周期的第六层)
2.运维是软件开发中最困难的工作之一,维护工程师处理来自用户报告的故障/问题
3.修改之后:
(1)测试所做的修改,回归测试,记录变化
(2)除了修复问题,修改中不能引入新的故障
(3)最大的问题:修改后没有足够的文档记录和测试
4.软件维护的类型
(1)纠错性:25%
(2)适应性:21%
(3)完善性:50%
(4)预防性:4%
5.软件演化:对软件进行持续的更新。软件大部分成本来自于维护阶段

6.“变化”在软件生命周期中是不可避免的。如何在最初的设计中充分考虑到未来的变化,避免因为频繁变化导致软件复杂度的增加和质量的下降?
7.自我调节:维护组织稳定性。熟悉度守恒
8.软件维护不仅仅是运维工程师的工作,而是从设计和开发阶段就开始了。在设计与开发阶段就要考虑将来的可维护性。设计方案易于变化
9.维护需要考虑:模块化,OO设计原则,OO设计模式,基于状态的构造技术,表驱动的构造技术,基于语法的构造技术。
10.可维护性,可扩展性,灵活性,可适应性,可管理性,支持性
11.可维护性的问题:设计结构是否足够坚定,模块之间是否松散耦合,模块内部是否高度聚合,是否使用了非常深的继承数,是否使用了delegation替代继承,代码的权复杂度是否太高,是否存在重复代码。
12.圈复杂度,代码行数

还有继承的层次度,类之间的耦合度,单元测试的覆盖度
三、模块化设计与模块化原则
1.模块化编程
(1)设计的目标是将系统划分为模块,并以以下方式在组件之间分配责任:高内聚低耦合
(2)模块化降低了程序员在任何时候必须处理的总复杂性,假设:将功能分配给将相似功能组合在一起的模块(分离关注点)。信息隐藏
2.模块化评价的五个标准
(1)Decomposability可分解性:大模块是否可以分解为小模块
(2)Composability可组合性:大模块是否可以由小的模块组合而来
(3)Understandability可理解性:是否易于理解
(4)Continuity可持续性:发生变化时受影响范围最小
(5)Protection出现异常后的保护:出现异常后受影响范围最小
3.模块化设计的五个规则:直接映射,尽可能少的接口,尽可能小的接口,显示接口,信息隐藏。
4.内聚度和耦合度
(1)耦合:测量模块和模块之间的关系,一个模块改变,其他都会改变,则耦合度高(牵一发而动全身)。
(2)内聚:衡量一个模块的功能或职责之间有多强的相关性的一种度量。模块应该是为了一个单一的目标而建立
一般耦合度高,内聚度就低
四、OO设计原则:SOLID
1.SOLID是五个原则的统一:(SRP)单一责任原则,(OCP)开放-封闭原则,(LSP)里氏替换原则,(ISP)接口聚合原则,(DIP)依赖转置原则:首字母合在一起就是SOLID
2.单一责任原则:不应该有多于1个原因让你的ADT发生变化
(1)责任:变化的原因。也就是说,一个类只能有一个责任
(2)如果一个类包含多个责任:引入额外的包,占据资源。导致频繁的重新配置、部署等
(3)最简单而又最难做好的原则
比如画图的类Rectangle,不但画画又能计算面积,这样就违背了SRP,需要拆分责任。
还有猫

3.开闭原则:
(1)对扩展性的开放:模块的行为应是可扩展的,从而该模块可表现出新的行为以满足需求的变化
(2)对修改的封闭:但模块自身的代码是不应被修改的,扩展模块行为的一般途径是修改模块的内部实现,如果一个模块不能被修改,那么它通常被认为是具有固定的行为
(3)关键的解决方案:抽象技术,用好继承委派
4.里氏替换原则
5.接口隔离原则:不能强迫客户端依赖于它们不需要的接口:只提供必需的接口
(1)客户端不应该依赖于它们不需要的方法
(2)“胖”接口具有很多缺点:不够聚合
胖接口可以分解为小的接口,不同的接口向不同的客户端提供服务,客户端值访问自己所需要的接口。
6.依赖转换原则:抽象的模块不应依赖于具体的模块,具体应依赖于抽象。

也就是说,delegation时,要通过interface建立联系,而不是具体子类
四、OO设计原则:GRASP
1.General Responsibility Assignment Software Patterns (principles),通用责任分配软件模式。
2.GRASP为类和对象指派职责的一系列原则
第二节 面向可维护性的设计模式
一、内容大纲
1.构造模式
(1)工厂方法模式Factory Method:在不指定要创建的确切类的情况下创建对象。
(2)抽象工厂模式Abstract Factory:对具有共同主题的对象工厂进行分组。
2.结构模式
代理人模式Proxy:为另一个对象提供了一个占位符来控制访问、降低成本和降低复杂性。
3.行为模式
(1)观察者模式Observer:一种发布/订阅模式,允许多个观察者对象查看事件。
(2)访问者模式Visitor:Visitor通过将方法的层次结构移动到一个对象中,将算法从对象结构中分离出来。
4.设计模式的共性和差异
二、构造模式
1.工厂方法模式:又称“虚拟构造器”:定义一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到其子类
当client不知道要创建哪个具体类的实例,或者不想在client代码中指明要具体创建的实例时,用工厂方法。

2.模式的结构:工厂方法模式的主要角色如下。
(1)抽象工厂(Abstract Factory):提供了创建产品的接口,调用者通过它访问具体工厂的工厂方法new Product()来创建产品。
(2)具体工厂(Concrete Factory):主要是实现抽象工厂中的抽象方法,完成具体产品的创建。
(3)抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能。
(4)具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间一一对应。
3.例
(1)背景:小成有一间塑料加工厂(仅生产A类产品)。随着客户需求的变化,客户需要生产B类产品。
(2)冲突:改变原有塑料加工厂的配置和变化非常困难,假设下一次客户需要再发生变化,再次改变将增大非常大的成本
(3)解决方案:小成决定置办塑料分厂B来生产B类产品;

4.优缺点
(1)优点:
1)更符合开-闭原则:新增一种产品时,只需要增加相应的具体产品类和相应的工厂子类即可
2)-符合单一职责原则:每个具体工厂类只负责创建对应的产品
3)不使用静态工厂方法,可以形成基于继承的等级结构。
(2)缺点:
1)添加新产品时,除了增加新产品类外,还要提供与之对应的具体工厂类,系统类的个数将成对增加,在一定程度上增加了系统的复杂度;同时,有更多的类需要编译和运行,会给系统带来一些额外的开销
2)由于考虑到系统的可扩展性,需要引入抽象层,在客户端代码中均使用抽象层进行定义,增加了系统的抽象性和理解难度
3)虽然保证了工厂方法内的对修改关闭,但对于使用工厂方法的类,如果要更换另外一种产品,仍然需要修改实例化的具体工厂类
4)一个具体工厂只能创建一种具体产品
5.优势:无需将特定应用程序的类绑定到代码。代码只处理产品接口(Trace),因此它可以处理任何用户定义的具体产品(FileTrace,SystemTrace)
6.隐藏缺点:客户可能需要创建Creator的子类,来创建具体的产品。如果客户无论如何都必须对Creator进行的子类话分是可以接受的,但如果不是,客户必须处理另一个演化点。
耦合度大
彳亍

2.抽象工厂模式:
(1)例子:一个UI包含多个窗口控件,这些控件在不同的OS中实现不同。如何编写一个用户界面并使其可移植到这些不同外观标准的窗口管理器中?

(2)例子:一个仓库类,要控制多个设备,这些设备的制造商各有不同,控制接口有差异。如何编写独立于制造商的单一控制系统?
(3)抽象工厂模式:提供接口以创建一组相关/相互依赖的对象,但不需要指明其具体类。
背景;小成有两间塑料加工厂(A厂仅生产容器类产品,B厂仅生产模具类产品)
随着客户需求的变化,A厂所在地的客户需要也模具类产品,B厂所在地的客户也需要容器类产品
冲突:没有资源(资金+租位)在当地分别开设多一家注塑分厂
解决方案:在原有的两家塑料厂里增设生产需求的功能,即A厂能生产容器+模具产品; B厂间能生产模具+容器产品。

Abstract Factory创建的不是一个完整产品,而是“产品族”(遵循固定搭配规则的多类产品的实例),得到的结果是:多个不同产品的object,各产品创建过程对client可见,但“搭配”不能改变。本质上,Abstract Factory是把多类产品的factory method组合在一起
7.抽象工厂优点
(1)降低耦合
抽象工厂模式将具体产品的创建延迟到具体工厂的子类中,这样将对象的创建封装起来,可以减少客户端与具体产品类之间的依赖,从而使系统耦合度低,这样更有利于后期的维护和扩展
(2)更符合开-闭原则
新增一种产品类时,只需要增加相应的具体产品类和相应的工厂子类即可
简单工厂模式需要修改工厂类的判断逻辑
(3)符合单一职责原则
每个具体工厂类只负责创建对应的产品
简单工厂“中的工厂类存在复杂的switch逻辑判断
(4)不使用静态工厂方法,可以形成基于继承的等级结构。.
简单工厂模式的工厂类使用静态工厂方法
8.抽象工厂缺点:
(1)抽象工厂模式很难支持新种类产品的变化。
这是因为抽象工厂接口中已经确定了可以被创建的产品集合,如果需要添加新产品,此时就必须去修改抽象工厂的接口,这样就涉及到抽象工厂类的以及所有子类的改变,这样也就违背了开闭原则。
对于新的产品族符合开闭原则。对于新的产品种类不符合开-闭原则,这一特性称为开闭原则的倾斜性。
二、结构模式
代理模式Proxy
1.目标:允许通过充当传递实体或占位符对象进行对象级访问控制
某个对象比较“敏感”/“私密”/“贵重”,不希望被client直接访问到,故设置proxy,在二者之间建立防火墙。
Adapter:目的:消除不兼容,目的是B以客户端期望的统一的方式与A建立起联系。
Proxy:目的:隔离对复杂对象的访问,降低难度/代价,定位在“访问/使用行为”

三、行为模式
1.观察者模式
(1)问题:从属状态必须与主状态一致
“粉丝”对“偶像”感兴趣,希望随时得知偶像的一举一动
粉丝到偶像那里注册,偶像一旦有新闻发生,就推送给已注册的粉丝(回调callback粉丝的特定功能)

2.访问者模式
对特定类型的object的特定操作(visit),在运行时将二者动态绑定到一起,该操作可以灵活更改,无需更改被visit的类
本质上:将数据和作用于数据上的某种/些特定操作分离开来。
为ADT预留一个将来可扩展功能的“接入点”,外部实现的功能代码可以在不改变ADT本身的情况下通过delegation接入ADT
(1)访问者(Visitor)模式的定义:
1)将作用于某种数据结构中的各元素的操作分离出来封装成独立的类,使其在不改变数据结构的前提下可以添加作用于这些元素的新的操作为数据结构中的每个元素提供多种访问方式。
2)将对数据的操作与数据结构进行分离,是行为类模式中最复杂的一种模式。
(2)访问者(Visitor) 模式的优缺点
1)优点:
扩展性好。能够在不修改对象结构中的元素的情况下,为对象结构中的元素添加新的功能。
复用性好。可以通过访问者来定义整个对象结构通用的功能,从而提高系统的复用程度。
灵活性好。访问者模式将数据结构与作用于结构上的操作解耦,使得操作集合可相对自由地演化而不影响系统的数据结构。
符合单一职责原则。访问者模式把相关的行为封装在一起,构成一个访问者使每一个访问者的功能都比较单一。
2)缺点:
(1)增加新的元素类很困难。在访问者模式中,每增加一个新的元素类,都要在每一个具体访问者类中增加相应的具体操作,这违背了“开闭原则”
(2)破坏封装。访问者模式中具体元素对访问者公布细节,这破坏了对象的封装性。

(3)模式的结构:访问者模式包含以下主要角色:
1)抽象访问者(Visitor)角色:定义一个访问具体元素的接口,为每个具体元素类对应一个访问操作visit(),该操作中的参数类型标识了被访问的具体元
2)具体访问者(Concrete Visitor)角色:实现抽象访问者角色中声明的各个访问操作,确定访问者访问一个元素时该做什么。
3)抽象元素(Element)角色:声明一个包含接受操作accept()的接口,被接受的访问者对象作为accept()方法的参数。
4)具体元素(Concrete Element)角色:实现抽象元素角色提供的accept)操作,其方法体通常都是visitor.visit(this),另外具体元素中可能还包含本身业务逻辑的相关操作。
5)对象结构(Object Structure)角色:是一个包含元素角色的容器,提供让访问者对象遍历容器中的所有元素的方法,通常由List、Set、 Map等聚合类实现。

迭代器:以遍历的方式访问集合数据而无需暴露其内部表示,将“遍历”这项功能delegate到外部的iterator对象。
Visitor:在特定ADT上执行某种特定操作,但该操作不在ADT内部实现,而是delegate到独立的visitor对象,客户端可灵活扩展/改变visitor的操作算法,而不影响ADT

Strategy和Visitor
二者都是通过delegation建立两个对象的动态联系
但是Visitor强调是的外部定义某种对ADT的操作,该操作于ADT自身关系不大(只是访问ADT),故ADT内部只需要开放accept(visitor)即可,client通过它设定visitor操作并在外部调用。
而Strategy则强调是对ADT内部某些要实现的功能的相应算法的灵活替换。这些算法是ADT功能的重要组成部分,只不过是delegate到外部strategy类而已。
区别:visitor是站在外部client的角度,灵活增加对ADT的各种不同操作(哪怕ADT没实现该操作),strategy则是站在内部ADT的角度,灵活变化对其内部功能的不同配置。
四、模式对比

第三节 面向可维护性的构造技术
一、内容大纲
1.基于状态的构造
(1)基于自动机的程序设计
(2)设计模式:Memento提供了将对象恢复到其先前状态(撤消)的功能。
(3)设计模式:状态允许对象在其内部状态改变时改变其行为。
2.基于语法的构造
(1)语法与语法分析器
(2)正则表达式(regexp)
二、基于状态的构造技术
1.目标:使用有限状态机来定义程序的行为,使用状态来控制程序的执行
2.根据当前状态,决定下一步要执行什么操作、执行操作之后要转移到什么新的状态

3.核心思想:将程序看作是一个有限状态自动机,侧重于对“状态”及“状态转换”的抽象和编程
程序的执行被分解为一组自动执行的步骤
各步骤之间的通讯通过“状态变量”进行
4.
程序执行就可看作是各自动步骤的不断循环
使用枚举类型enum定义状态
使用二维数组定义状态转换表:行表示所有可能状态,列表示输入参数。

最好不要使用if/else结构在ADT内部实现状态转换(考虑将来的扩展和修改)
使用delegation,将状态转换的行为委派到独立的state对象去完成

2.备忘录模式
(1)不违反封装,通过外部控制对象内部的状态,可以回到某个状态
(2)记住对象的历史状态,以便于“回滚”
(3)设计:
需要“备忘”的类
添加originator的备忘记录和恢复
备忘录,记录originator对象的历史状态

共性对比

三、语法驱动的构造
1.字符串或流,IO
(1)有一类应用,从外部读取文本数据,在应用中做进一步处理。
(2)输入文件有特定格式,程序需读取文件并从中抽取正确的内容
(3)从网络上传输过来的消息,遵循特定的协议
(4)用户在命令行输入的指令,遵循特定的格式
(5)内存中存储的字符串,也有格式需要
2.语法:
(1)使用grammar判断字符串是否合法,并解析成程序里使用的数据结构
(2)通常是递归的数据结构
(3)Regular expression正则表达式
(4)根据语法,开发一个它的解析器,用于后续的解析
3.文字字符串:用语法定义一个字符串
这个字符串称为终止结点或者叶结点
语法解析树的叶子结点,无法再往下扩展,通常表示为字符串
(2)产生式结点又称非终止结点,遵循特定规则,利用操作符,终止结点或其他非终止结点,构造新的字符串
根节点也是一个非终止结点。
(3)三个重要操作

4.语法中的递归

Parse树

四、正则表达式
1.将右边替换每个非终结符,可以将其简化为一个产生式节点,而右边只剩下叶节点和操作符,则为正则语法。正则语法:简化之后可以表达为一个产生式而不包含任何非终止节点

终端和运算符的简化表达式可以用更紧凑的形式编写,称为正则表达式。
去除引号和空格,从而表达更简洁(更难懂)

2.Java中使用正则表达式

第一章 健壮性的软件构造
第一节 健壮性与正确性
一、内容大纲
1.认识健壮性和正确性
2.如何度量健壮性和正确性
3.目的
二、健壮性和正确性
鲁棒是Robust的音译,也就是健壮和强壮的意思。它也是在异常和危险情况下系统生存的能力。比如说,计算机软件在输入错误、磁盘故障、网络过载或有意攻击情况下,能否不死机、不崩溃,就是该软件的鲁棒性。
1.鲁棒性:健壮性:系统在不正常输入或不正常外部环境下仍能够表现正常的程度
面向健壮性的编程
(1)处理未期望的行为和错误终止
(2)即使终止执行,也要准确/无歧义的向用户展示全面的错误信息
(3)错误信息有助于进行debug
2.鲁棒性原理:(Postel’s Law)
(1)偏执狂:总是假定用户恶意、假定自己的代码可能失效
(2)白痴:把用户想象成白痴,可能输入任何东西,虚假和格式错误的输入
因此程序员向用户返回一个明确、直观的错误消息,不需要查找错误代码。
返回给用户的错误提示信息要详细、准确、无歧义
对别人宽容点,对自己狠一点
对自己的代码要保守,对用户的行为要开放
3.鲁棒性编程的原则
(1)封闭实现细节,限定用户的恶意行为
(2)考虑极端情况,没有“不可能”
4.正确性:程序按照spec加以执行的能力,是最重要的质量指标
(1)正确性:永不给用户错误的结果。
(2)健壮性:尽可能保持软件运行而不是总是退出。
正确性倾向于直接报错,鲁棒性则倾向于容错
健壮性:避免给用户太大压力,帮助用户承担一些麻烦。
5.
健壮性:让用户变得更容易:出错也可以容忍,程序内部已有容错机制
正确性:让开发者变得更容易,用户输入错误(不满足前置条件),直接结束
6.对外的接口倾向于健壮,对内的实现倾向于正确。内外部做好隔离,放置错误扩散
7.Reliability可靠性:系统在规定条件下执行其所需功能的能力,无论何时需要,平均无故障时间很长。Reliability = Robustness + Correctness
8.表示软件困境的术语
(1)error≈mistake:程序员犯的错误
(2)defect缺陷,bug的根源
(3)fault:defect≈fault、bug
(4)failure:失效,运行时的外在表现
因果关系:error→defect/fault/bug→failure
程序员犯错导致软件存在缺陷,导致软件运行时失效
9.维护鲁棒性和正确性的方法
(0)第0步:使用断言、防御性编程、代码评审、形式验证等,编写具有健壮性和正确性目标的代码
(1)步骤1:观察故障症状(内存转储、堆栈跟踪、执行日志、测试)
(2)步骤2:识别潜在故障(错误定位、调试)
(3)步骤3:修复错误(代码修订)
三、健壮性和正确性度量
1.外部观察角度:
(1)平均失效间隔时间(MTBF):指可修复产品两次相邻故障之间的平均时间。对于复杂的、可修复的系统,故障被认为是那些使系统停止工作并进入修复状态的超出设计条件的故障。在这个定义下,如果发生的故障可以保留或者保持在未修复的状态,并且不会是系统停止工作,则不视为故障。

(2)MTTF针对不可修复系统:MTBF描述可修复系统的两次故障之间的预期时间,而平
均故障时间(MTTF)表示不可修复系统的预期故障时间。

2.内部观察角度(间接)
(1)残余缺陷率:每千行代码中遗留的bug的数量
1-10 defect/kloc:典型的工业软件。
0.1-1 defect/kloc:高质量的软件,比如java库
0.01-0.1 defect/kloc:最好的,安全严格的验证。NASA和Praxis这样的公司能达到。
四、目的

第二节 错误和异常处理
一、内容大纲
1.Java中的错误和异常
2.异常处理
(1)什么是异常?
(2)异常情况的分类
(3)Checked和Unchecked异常
(4)如何抛出异常
(5)创建异常类
(6)捕捉异常
(7)重新引发和链接异常
(8)finally关键字
(9)Try with-Resources语句
(10)分析堆栈跟踪元素
二、Java中的异常

1.Error和Exception
Error一般指系统本身的错误,是内部错误,一旦发生,程序员无能为力,只能尝试让程序优雅的结束
Exception,是自己程序本身的错误,可以捕获大多数可以进行处理。
2.Error的类型
(1)用户输入错误:打字出错或者句法上出现问题
(2)设备错误:硬件出现问题,打印机可能已关闭,网页可能暂时不可用,设备通常会在任务执行过程中失败。
(3)物理限制:磁盘不够了,内存不够了
大多数时候程序员不需要实例化一个Error
3.一些Error类型

三、异常处理
1.异常:程序执行中的非正常事件,程序无法再按预想的流程执行
2.会将错误信息传递给上层调用者,并报告“案发现场”的信息
3.throw异常是return之外的第二种退出途径。在调用该方法的代码处不恢复执行。若找不到异常处理程序,系统退出。
4.Runtime Exception和其他异常
运行时异常是用程序员代码本身有问题造成的,需要自己改掉,如果代码找到就可以修改代码直接规避掉。
其他异常是外部原因造成的,可以使用异常处理机制。
Runtime异常包括不好的类型转换,数组越界,空指针异常等等
四、Checked和Unchecked异常
这是从异常处理机制的角度所做的分类。异常被谁check?——编译器、程序员

不加特殊说明一般把ERROR和RuntimeException当做Unchecked异常其他是Checked异常
Unchecked异常不需要处理,相当于动态检查
2.Checked异常
throws:声明“本方法可能会发生XX异常”
throw:抛出XX异常
try, catch, finally:捕获并处理XX异常
3.Unchecked异常也可以try-catch,但是由于没用所以一般不这么做
4.如果客户端可以处理,就可以用checked exception的方式处理
处理不了就用Unchecked exception
5.Unchecked exception一般可以用于类似动态检查,来发现程序的错误
6.不要创建没有意义的异常,应该能从checked异常中获得信息,才是目的。
7.例如:如果读文件的时候发现文件不存在了,可以让用户选择其他文件。但是如果调用某方法时传入了错误的参数,则无论如何都无法在不中止执行的前提下进行恢复。最好记录下来,等开发者以后处理。
五、利用throws声明Checked异常
1.异常也是spec的一部分,在post-condition中刻画
2.spec里用@throws注释
3.方法可以抛出多个异常
4.你的方法应该throws什么异常?
(1)你所调用的其他函数抛出了一个checked Exception——从其他函数传来的异常
(2)当前方法检测到错误并使用throws抛出了一个checked exception——你自己造出的异常。
此时需要告知你的client需要处理这些异常
如果没有handler来处理被抛出的checked exception,程序就终止执行
原则上不要抛出error或是Runtime Exception
5.如果子类型中override了父类型中的函数,那么子类型中方法抛出的异常不能比父类型抛出的异常类型更宽泛
子类型方法可以抛出更具体的异常,也可以不抛出任何异常
如果父类型的方法未抛出异常,那么子类型的方法也不能抛出异常。
六、throw抛出异常
1.

(1)找到一个能表达错误的Exception类/或者构造一个新的Exception类
(2)构造Exception类的实例,将错误信息写入
(3)抛出它
(4)一旦抛出异常,方法不会再将控制权返回给调用它的client,因此也无需考虑返回错误代码
3.如果JDK提供的exception类无法充分描述你的程序发生的错误,可以创建自己的异常类

六、catch捕获异常
异常发生后,如果找不到处理器,就终止执行程序,在控制台打印出stack trace.

尽量在自己这里处理,实在不行就往上传一要承担责任!但有些时候自己不知道如何处理,那么提醒上家,由client自己处理
如果父类型中的方法没有抛出异常,那么子类型中的方法必须捕获所有的checked exception
子类型方法中不能抛出比父类型方法更多的异常!
1.本来catch语句下面是用来做exception handling的,但也可以在catch里抛出异常
这么做的目的是:更改exception的类型,更方便client端获取错误信息并处理
但是这么做一般要保留住“根原因”

七、finally关键字
1.当异常抛出时方法中正常执行的代码被终止
如果异常发生前曾申请过某些资源,那么异常发生后这些资源要被恰当的清理
2.

(1)1和2之间没有异常:1,2,5,6
(2)1和2之间发生可以被接收的异常
如果3,4之间不抛出异常:1,3,4,5,6
如果3,4之间抛出异常:1,3,5
(3)1和2之间发生不能被接收的异常:1,5
3.finally一般用与关闭资源,不用返回:否则容易出现问题

八、TWR(Try-with-Resources)
1.try时自动调用资源,结束后自动执行.close()关闭

也可以打开多个资源,仍然可以自动关闭

无论如何退出,输入和输出都会自动关闭的。
九、堆栈跟踪分析
堆栈跟踪是在程序执行的特定点上所有挂起的方法调用的列表
几乎可以肯定的是,每当Java程序因意外异常而终止时,都会显示堆栈跟踪列表。
类有方法可以获得文件名及行序号,类似代码行号所执行到的类和方法名
方法生成一个包含所有这些信息的格式化字符串。
第三节 断言与防御式编程
一、内容大纲
1.回顾:ADT的设计
2.断言
(1)什么需要断言,什么不需要断言
(2)使用断言的准则
3.防御式编程
4.防御式编程技术
5.防御性编程检查表
6.SpotBugs工具
二、回顾:ADT的设计
1.第一步防御:最好的防御就是不要引入bug,具体做法:
(1)静态检查:通过在编译时捕捉错误来消除它们。
(2)动态检查:Java通过动态捕捉数组溢出错误,使其不可能发生。如果试图使用数组或列表,则Java会自动生成一个错误。—未检查的异常/运行时错误
(3)不变性:不可变类型是一种值一旦创建就永远不能更改的类型。
(4)不变量:按final,可以分配一次,但不能重新分配。
(5)不可变引用:按final,这使得引用不可赋值,但引用指向的对象可能是可变的或不可变的。
2.第二步防御:Bug局部化
(1)如果无法避免,尝试将bug限制在最小的范围内
(2)如果把bug限定在一个方法内,bug就不容易扩散
(3)尽可能早的指出client的bug,尽快fail,这样更容易发现bug,就能更早修复。
(4)断言:可以尽快bug,避免扩散
(5)检查前置条件是防御式编程的一种典型类型
真正的程序很少没有bug。
防御性编程提供了一种减轻错误影响的方法
三、Assertions断言
1.什么是断言:断言:在开发阶段的代码中嵌入,检验某些“假设”是否成立。若成立,表明程序运行正常,否则表明存在错误。

(1)assert如果false,JVM会抛出AssertionError,出现AssertionError说明内部某些假设被违反了。断言可以增强程序员对代码质量的信心:对代码所做的假设都保持正确。
(2)断言即是对代码中程序员所做假设的文档化,也不会影响运行时性能(在实际使用时,assertion都会被disabled)
(3)断言由布尔表达式和错误信息组成:

所构造的message在发生错误时显示给用户,便于快速发现错误所在。

2.使用断言的时机
(1)内部不变量
(2)表示不变量:checkRep()
(3)控制流不变量:switch-case
(4)前置条件
(5)后置条件
前后置条件:

控制流(不建议直接assert,因为会被屏蔽,建议throw):

3.更多场景:
(1)只输入变量的值不会被方法更改指针非空
(2)传递到方法中的数组或其他容器至少可以包含n个数据元素
(3)表已初始化为包含实际值
(4)当方法开始执行(或完成)时,容器为空(或已满)
(5)一个高度优化、复杂的方法的结果与一个速度较慢但编写清晰的例程的结果相匹配
(6)不允许出现异常的地方,用Assert检测一下。

4.断言主要用于开发阶段,避免引入和帮助发现bug
5.实际运行阶段,不再使用断言,避免降低性能。
6.使用断案的主要目的是为了在开发阶段调试程序、尽快避免错误。
7.assert在运行时将被屏蔽,不能将执行代码放在assert语句中。不要引起副作用。注意:assert在运行时是要被屏蔽的!
8.程序之外的事不受控制,不要乱用断言。Assert主要用于检测程序内部bug,外部错误不是程序本身的bug
9.加入/屏蔽assertion

默认关闭的
Eclipse:

断言通常实现程序的正确性问题。如果断言是针对异常情况触发的,则纠正操作不仅仅是为了优雅地处理错误,纠正操作是为了更改程序的源代码、重新编译并发布软件
的新版本。断言——Correctness
如果错误处理代码用于处理异常情况,则错误处理将使程序能够对错误作出正确响应。错误/异常处理——Robustness
使用异常来处理你“预料到可以发生”的不正常情况
错误处理代码检查异常情况,这种情况可能不经常发生,但编写代码的程序员已经预料到,并且需要由生产代码处理。
使用断言处理“绝不应该发生”的情况
11.比较好的断言前后置条件的思路
(1)如果参数来自于外部(不受自己控制)一般使用异常处理
(2)如果来自于自己所写的其他代码,可以使用断言来帮助发现错误。
12.结合使用异常和断言
(1)断言和异常处理都可以处理同样的错误
(2)开发阶段用断言尽可能消除bugs
(3)在发行版本里用异常处理机制处理漏掉的错误
四、防御性编程
1.防御性编程是一种防御性设计,旨在确保一个软件在不可预见的情况下的持续功能。
2.防御性编程的技术:对于无效的输入进行判断,断言,异常,规约错误处理,路障,debug。防御性编码的最佳形式首先是不插入错误。
3.对于输入错误,不能直接返回不正确的东西。垃圾进可以无输出,可以输出error信息,可以组织垃圾进入。垃圾进垃圾出是不安全的。
(1)对来自外部的数据源要仔细检查,例如:文件、网络数据、用户输入等
(2)设置路障:建立防火墙。隔离病房的隔离区等等。使用路障可以区分断言和错误处理。
五、SpotBugs
1.早期版本:FindBugs
2.是静态工具
第四节 代码调试
一、内容大纲
1.bug和debug
2.debug的过程
(1)再现bug
(2)诊断bug
(3)修复bug
(4)反思
3.debugging技术和工具
(1)Print debugging / stack tracing / memory dump
(2)日志
(3)编译器Warning信息
(4)Debugger:观察点,断点
二、bug和debug
1.bug:程序以一种意料之外的方式表现。严重干扰了程序的功能
2.bug出现的原因
(1)代码错误
(2)未完成要求或不够详细
(3)对用户需求的误解
(4)设计文件中的逻辑错误
(5)文档缺失
(6)测试不够充分
3.建议第一种

4.当防御式编程和测试都无法挡住bug,就不得不debug了。
5.debug的目的是需求错误的根源并消除它。Debug一般占用了大量的开发时间。Debug是最后的手段。
6.debug是测试的后续步骤:测试发现问题,debug消除问题
7.在不正常症状与原因之间建立联系。通过回归测试判断bug是否解决。
8.
(1)“症状”和“原因”可能相隔很远,高耦合导致的结果。当其他bug被修复后,该bug消失了。
(2)例如四舍五入造成的不准确

(3)因为人工错误导致bug的症状难以有效追踪。
(4)有些症状是由计时/定时等时间原因导致的,难以重现出错时的输入数据
(5)由于外部软硬件环境变化,导致间歇性的症状。
(6)分布式导致的问题。
三、debug过程
1.
(1)重现bug:找到一种可靠、方便地按需复制问题的方法。
(2)诊断/定位:构建假设,并通过执行实验进行测试,直到您确信自己已经确定了缺陷的根本原因。
(3)修复:设计和实现解决问题的变更,避免引入回归,维护或提高软件的整体质量。
(4)反思:从错误中吸取教训。哪里出了问题?同样的问题是否还有其他需要解决的例子?你能做些什么来确保同样的问题不再发生?

2.科学的debug方法
(1)假设检验法
(2)定位error的源头
收集bug相关数据
观察数据,做出bug原因的假设
决定如何验证
通过工具验证假设是否成立
(3)修复缺陷
(4)测试修复
(5)看一些相似的error
3.重现bug
(1)从小的测试用例集开始复现错误:编一点测试一点,不要编完再全测试
(2)多线程错误不好定位
(3)确定有哪些因素跟bug相关,将这些因素找出来,并变化它们的值
(4)确保你的bug复现环境跟用户发现bug的环境尽可能保持一致
(5)重现bug需要控制的东西:软件版本,软件运行的环境,输入数据。
4.诊断/定位bug:
(1)测量Instrumentation:指令插入是不影响软件行为的代码,但它提供了对软件行为的深入了解。如logging
测量并不局限于简单的输出语句,但是可以使用该语言的全部功能
可以收集和整理数据,计算任意代码,并测试相关条件。
(2)分治Divide and Conquer:它可以为你提供一个快速简便的方法,排除大量的候选人。

防狼围栏算法。
(3)切片:Slicing:切片意味着找到程序中有助于计算特定值的部分。
把不影响结果的部分去掉

(4)寻找差异:Focus on difference:
1)充分利用版本控制系统,找出在哪个commit之后出现了bug症状。git bisect
2)基于差异的调试:两个测试用例,分别通过/未通过。通过查找二者所覆盖的代码之间的差异,快速定位出可能造成bug的代码行。
3)其他差异:软硬件环境,JVM参数配置,输入文件……
4)符号化执行:不需输入特定的值,使用“符号值”(而非“实际值”)作为输入,解释器模拟程序执行,获得每个变量的“符号化表达式”,从而可判断是否执行正确。
(5)符号化执行树:

符号化执行程序的状态:各变量的符号化取值,路径条件PC,计数器
(6)调试器Debugger:单步执行、断点等等
(7)从别的人的错误中学习错误。比如上CSDN搜索
三、debugging工具
1.暴力破解Brute Force(穷举尝试):看内存导出文件,到处println(),自动化调试工具
2.工具:

3.Memory dump,内存转储:硬盘上的一种文件,包含某一特定时间进程内存内容的副本,当进程因某种内部错误或信号而中止时产生。
当程序中止时,可以进行内存转储,以便在崩溃时检查程序的状态。
4.事后调试:堆栈跟踪

5.打印信息,print函数输出动态信息,比静态分析有效。
6.日志
(1)通过设定日志级别来确定要log哪些信息,可以禁止/启动日志。
(2)结果可被多种渠道加以处理,可通过设定条件进行过滤,并输出为多种格式。
(3)可使用层次化的多个日志记录器。
除了,还可以设定其他的handlers,FileHandler,SocketHandler.

(4)其他日志:
通过拦截软件。与其他地方之间的通信量,可以从软件外部获取有用信息(外部日志记录)。
可以在客户端和服务器之间插入代理。如果您正在使用的协议不存在代理,或者您找不到配置代理以拦截流量的方法,则可以考虑使用网络分析器来捕获所有网络流量
7.编译器warning信息:把编译器的warning level调到最高级别,消除所有warning
学着把编译器当作自己的老师,搞清楚每一个warning,并在后续代码中避免
精益求精,把warning当error看待,统一设置
8.debug,断点,单步执行,恢复运行,临时断点
恢复执行
-调试器将暂停并继续运行下一个断点。
逐步调试
-Step Over将只执行该行并转到下一行。如果一个方法将在这一行中被调用,调试器将不会调试到该方法的代码中,并将该方法作为一个完整的步骤完全执行。
-Step Into将只执行行并转到下一行。如果一个方法将在这一行中被调用,下一步是进入该方法并继续一步一步地调试。
-Step Return如果调试器正在逐步调试方法,“Step returm”将允许调试器运行整个方法(直到该方法结束),直到它作为一个完整的步骤返回为止。
监视变量

第一章 并行与分布式程序设计
第一节 并发Concurrent
一、内容大纲
1.什么是并发编程
2.进程、线程和时间切片
(1)进程
(2)线程
(3)开启一个线程
3.交织和竞赛条件
(1)时间切片
(2)线程间共享内存
(3)竞争条件
(4)消息传递示例
(5)并发性难以测试和调试
(6)干扰线程自动交互的一些操作
二、并发编程
1.并发:多个计算同时发生。
2.在现代编程中并发无处不在。
(1)网络上的多台计算机
(2)一台计算机上的多个应用
(3)一个CPU上的多核处理器
3.并发在现代编程中是必不可少的:
(1)多用户并发请求服务器的计算资源(比如选课崩了)
(2)App在手机端和在云端都有计算
(3)GUI的前端用户操作和后台的计算
4.摩尔定律失效了,因为计算速度是有极限的。现在是加“核”的数量。为了充分利用多核和多处理器,需要将程序转化为并行执行。
5.并行程序设计的两个模型
(1)共享内存:在内存中读写共享数据

两个处理器,共享内存
同一台机器上的两个程序,共享文件系统
同一个Java程序内的两个线程,共享Java对象
(2)消息传递:通过channel交换信息

网络上的两台计算机,通过网络连接通讯
浏览器和Web服务器,A请求页面,B发送页面数据给A
即时通讯软件的客户端和服务器
同一台计算机上的两个程序,通过管道连接进行通讯
三、进程、线程、时间切片
1.进程和线程(以前编的程序都是串行的,可以说都是进程)
(1)进程:私有空间,彼此隔离,一个运行的程序都是一台虚拟机:有独立的内存、CPU、堆…
(2)线程:程序内部的控制机制。线程是运行程序中的控制点把它看作是正在运行的程序中的一个位置,加上导致该位置的方法调用堆栈
2.进程
(1)进程:拥有整台计算机的资源
(2)多进程之间不共享内存
(3)进程之间通过消息传递进行协作。相反,一个新的进程会自动为消息传递做好准备,因为它是用标准的输入和输出流创建的,这些流是在Java中使用的System.out和System.in流。
(4)一般来说:进程=程序=应用,但一个应用中可能包含多个进程
(5)OS支持的IPC机制(pipe/socket)等技术支持进程间通信。
(6)不仅是本机的多个进程之间,也可以是不同机器的多个进程之间。
3.线程
(1)进程=虚拟机,线程=CPU
(2)程序共享、资源共享都隶属于进程
(3)共享内存:很难获得线程私有的内存空间。通过创建消息队列在线程之间进行消息传递。

(4)使用多线程可以加快运行。
(5)充分利用多处理器,Java提供了很好的关于线程的库(如java.util.concurrent)
(6)如何使用?有效利用库,调试使用它们的程序
4.在java中启动一个线程
(1)每个应用至少有一个线程
(2)每个应用都有一个主线程,可以创建其他的线程。
(3)两种方法:Thread类派生子类。Runnable类接口构造Thread对象。
(使用较少)

前两种可以,第三种不行,没start
四、交错和竞争
1.时间切片:
(1)虽然有多线程,但只有一个核,每个时刻只能执行一个线程
(2)通过时间分片,在多个进程/线程之间共享处理器
(3)即使是多核CPU,进程/线程的数目也往往大于核的数目

两处理器三线程

时间分片是OS自动调度的
2.线程之间共享内存(容易有bug)

按理说余额永远是0,但是并行之后会出问题。+1和-1并不是原子操作

3.竞争条件
程序的正确性(后置条件和不变量的满足)取决于并发计算A和B中事件的相对时序。
一些事件的交错可能是可以的,因为它们与一个单独的、非当前的过程将产生的结果是一致的,但是其他交错产生错误的答案
是否是原子操作是由JVM确定的
不能通过观察一个表达式来判断它是否安全
4.信息传递示例
(1)传入的请求被放入队列中,一遍一次处理一个请求
(2)发信人在等待其请求的答复时不会停止工作。它处理来自自己队列的更多请求。对其请求的答复最终会作为另一条消自返回
(3)消息传递机制也无法解决竞争条件问题。
假设每个账户都支持存款和支取操作,并有相应的消息。
他们首先检查余额,以确保他们提取的钱款永远不会超过账户的存款额,因为透支会引发银行的巨额罚款。
问题再次是交错,但这次是交错发送到银行账户的消息,而不是A和B执行的指令。仍然存在消息传递时间上的交错
如果帐户有一美元在里面,然后是什么交错信息会欺骗A和B以为他们都能提取一美元,从而透支账户。
同时检测到账户上有一美元,则分别取出一美元,账户被透支

5.并发难以测试和调试很难测试和
(1)调试因为竞争条件导致的bug
跟相对时序有关
(2)因为interleaving的存在,导致很难复现bug
多次执行结果不一致。
依赖相对时序。
可以因为网络阻塞、OS计划决策、处理器的始终频率等因素出现延迟。
多次执行结果不一致。
6.利用某些方法调用影响时序关系
(1)可能增加语句就导致bug消失,因为改变了时序关系

本来想通过打印找原因,结果因为打印是慢操作,时序变化了。
(2)线程休眠
将某个线程休眠,意味着其他线程得到更多的执行机会。进入休眠的线程不会失去对现有monitor或锁的所有权

(3)线程中断
.interrupt()方法向线程发出中断信号
t.isInterrupted() 检查t是否已在中断状态中
当某个线程被中断后,一般来说应停止其run()中的执行,取决于程序员在run()中处理。一般来说,线程在收到中断信号时应该中断,直接终止

(4)保持执行:
让当前线程保持执行,直到其执行结束。一般不需要这种显式指定线程执行次序

第二节 线程安全
一、内容大纲
1.线程安全
2.策略1:不使用动态内存
3.策略2:Immutable类型
4.策略3:使用线程安全的数据类型
5.策略4:锁定和同步
6.如何进行安全论证
二、线程安全
1.多个线程共享同一可变变量,而不协调它们正在做什么
2.程序的正确性依赖于底层操作的时序。这就不可靠了
3.线程之间的“竞争条件”:作用于同一个mutable数据.上的多个线程,彼此之间存在对该数据的访问竞争并导致interleaving,导致post-condition可能被违反,这是不安全的。
4.底层时序:
x=x+1;
操作:在内存中读x。计算x+1,放入寄存器。将寄存器中的值写入内存
5.线程安全:ADT或方法在多线程中要执行正确,不需要额外的协调。
(1)不违反spec、保持RI与多少处理器、OS如何调度线程,均无关。不需要在spec中强制要求client满足某种“线程安全”的义务。
6.线程安全:迭代器的规范指出,不能再迭代集合的同事对其进行修改。作为这种非局部契约现象的一个症状,看一下Java集合类,这些类通常在客户机和实现者之间用非常清晰的契约进行记录。
7.四种线程安全的方式(虽然四种方式都不是太完善,使用时候要小心):
(1)限制数据共享(Confinement),对于mutable的类型,不要在共享内存中
(2)共享不可变数据。
(3)共享线程安全的可变数据。
(4)同步机制:通过锁的机制共享线程不安全的可变数据,变并行为串行。
三、策略一:限制数据共享(Confinement)
(1)将可变数据限制在单一线程内部,避免竞争
(2)不允许任何线程直接读写该数据
(3)核心思想:线程之间不共享mutable数据类型
(4)局部变量总是受限制的。(局部变量在栈中,每个线程都有自己的栈,所以不会共享,安全)。
(5)对象引用也必须是受限制的。
(6)避免全局变量
(7)如果一个ADT的rep中包含mutable的属性且多线程之间对其进行mutator操作,
那么就很难使用confinement策略来确保该ADT是线程安全的。
四、策略二:使用Immutable变量,确保不会改变。
(1)使用不可变数据类型和不可变引用,避免多线程之间的race condition
(2)Immutable数据通常是线程安全的。但是有时候有益突变也可能导致线程不安全。
(3)如果ADT中使用了beneficent mutation有益突变,必须要通过“加锁”机制来保证线程安全。
(4)具体做法:
1)没有变值器
2)所有属性private且final
3)没有表示泄漏
4)rep中没有任何可变物体的突变,甚至没有有益的突变

五、策略三:使用线程安全的数据类型。
(1)如果必须要用mutable的数据类型在多线程之间共享数据,要使用线程安全的数据类型。
(2)在JDK中的类,文档中明确指明了是否threadsafe
(3)一般来说,JDK同时提供两个相同功能的类,一个是threadsafe,另一个不是。原因:threadsafe的类一般性能上受影响
(4)集合类都是线程不安全的不能用于多线程
(5)Java API提供了进一步的decorator,经过包装后的方法,使得collection线程安全。
对它们的每一个操作调用,都以原子方式执行
线程在运行期间,不会与其他操作interleaving

包装器将其所有实际工作委托给指定的集合,但在该集合提供的功能的基础上添加额外的功能
这是一种装饰模式
这些实现是匿名的:库提供静态工厂方法,而不是提供公共类
同步包装器将自动同步(线程安全)添加到任意集合
(6)不要绕过包装
底层集合仍然是可变的,引用它的代码可以绕过不变性,尽量避免进行底层引用。
在使用synchronizedMap(hashMap)之后,不要再把参数hashMap共享给其他线程,也不要保留别名,一定要彻底销毁!
(7)即使在线程安全的集合类上,使用iterator也是不安全的(除非用locked)
(8)即使是线程安全的collection类,仍可能产生竞争。执行其上某个操作是threadsafe的,但如果多个操作放在一起,仍旧不安全。

五、如何进行安全论证
(1)在代码中以注释的形式增加说明:该ADT采取了什么设计决策来保证线程安全
(2)采取四种方法的哪一种,如果是后两种,还需要考虑对数据的访问都是原子的,不存在interleaving。
(3)除非你知道线程访问的所有数据,否则Confinement无法彻底保证线程安全
如果数据类型创建自己的线程集,那么您可以讨论与这些线程相关的限制。
否则,线程从外部传入,承载客户端调用,并且数据类型可能无法保证哪些线程引用了哪些。
除非是在ADT内部创建的线程,可以清楚得知访问数据有哪些
第三节 锁与同步Locks and Synchronization
一、内容大纲
1.同步
2.锁
3.原子操作
4.活力:死锁、饥饿和活锁
5.wait(), notify(), notifyAll()
二、要求
(1)了解如何使用锁来保护共享的可变数据
(2)能够识别死锁并知道防止死锁的策略
(3)了解监视器模式并能够将其应用于数据类型
三、同步
1.线程安全不应依赖于偶然
2.并发程序的正确性不应该依赖于时序的偶然
3.前三种策略的核心思想:
(1)避免共享→即使共享,也只能读/不可写(immutable)→即使可写(mutable),共享的可写数据应自己具备在多线程之间协调的能力,即“使用线程安全的mutable ADT”
(2)缺陷:不能用全局rep共享数据→只能“读”共享数据,不能写→可以共享“读写”,但只有单一方法是安全的,多个方法调用就不安全了
4.同步和锁:防止线程在同一时间共享数据。
(1)很多时候,无法满足上述三个条件→要读写共享数据,且线程中的读写操作复杂
(2)程序员来负责多线程之间对mutable数据的共享操作,通过“同步”策略,避免多线程同时访问数据。
(3)锁是一种同步技术。
使用锁机制,获得对数据的独家mutation权, 其他线程被阻塞,不得访问
拥有锁的线程,可以行使对数据的操作权利,其他线程只能等待。
(4)使用锁告诉编译器和处理器您正在并发地使用共享内存,这样寄存器和缓存将被刷新到共享存储中,从而确保锁的所有者总是在查看最新数据。
(5)阻塞意味着线程等待,直到一个事件打破这份等待。
四、锁定
1.Lock是Java语言提供的内嵌机制
2.每个object都有相关联的lock。
3.即使是网络对象也有一个锁,因此裸对象通常用显式锁定。

4.但是,不能在Java的内部锁上调用acquire和release。相反,您可以使用synchronized语句获取语句块期间的锁

拥有lock的线程可独占式的执行该部分代码,而其他线程只能等待,这叫“互斥”。
换句话说,您又回到了顺序编程的世界,一次只运行一个线程,至少对于引用同一对象的其他同步区域。
5.Lock保护共享数据的访问:数据变量被锁定后,对其存取就成为了原子型操作,不会被其他线程干扰。注意:要互斥,必须使用同一个lock进行保护
6.监视模式:
在编写类的方法时,最方便的锁是对象实例本身,即this.用ADT自己做lock

如果添加synchronized到方法的描述中,Java将按照整个方法体被锁定。

构造函数加上锁可能会报错,另外构造函数本来就是默认单线程的,不用加锁
(2)与synchronized方法不同,synchronized语句必须指定提供内在锁的对象。
7.锁函数和锁函数语句不一样:后者需要显式的给出lock,且不一定非要是this。后者可提供更细粒度的并发控制

Java内置锁三种使用方式

内置锁使用起来非常方便,不需要显式的获取和释放,任何一个对象都能作为一把内置锁。使用内置锁能够解决大部分的同步场景。“任何一个对象都能作为一把内置锁”也意味着出现synchronized关键字的地方,都有一个对象与之关联,具体说来:
(1)当synchronized作用于普通方法是,锁对象是this
(2)当synchronized作用于静态方法是,锁对象是当前类的Class对象
(3)当synchronized作用于代码块时,锁对象是synchronized(obj)中的这个obj
8.那么线程安全仅仅是将synchronized关键字放在程序中的每个方法上吗?
同步机制给性能带来极大影响
由于需要获取锁(刷新缓存并与其他处理器通信),同步方法调用可能需要更长的时间。
除非必要,否则不要用。Java中很多mutable的类型都不是threadsafe就是这个原因
多线程本来就是为了“快”设计的,你全加上锁做的就是单线程操作,反而因为加了很多东西变慢。
9.尽可能减小lock的范围
将synchronized添加到每个方法意味着您的锁就是对象本身,每个引用了您的对象的客户机都自动拥有对您的锁的引用,它可以随意获取和释放。
避免在方法spec中加synchronized,而是在方法代码内部更加精细的区分哪些代码行可能有threadsafe风险,为其加锁。
10.要先去思考清楚到底lock谁,然后再synchronized()。
对静态方法上锁:
意味着在class层面上锁!对性能带来极大损耗!

Synchronized不是灵丹妙药,你的程序需要严格遵守设计原则,先尝试其他办法,实在做不到再考虑lock。所有关于threadsafe的设计决策也都要在ADT中记录下来。
11.例子

需要在规约里面说明怎么线程安全的。
12.
任何共享的mutable变量/对象必须被lock所保护
涉及到多个mutable变量的时候,它们必须被同一个lock所保护
monitor pattern中,ADT所有方法都被同一个synchronized(this)所保护
五、原子操作
1.死锁:两个模块相互锁住。当并发模块被阻塞,等待对方做某事时就会发生死锁。

2.防止死锁:
(1)对需要同时获取的锁进行排序,并确保所有代码都按该顺序获取锁。
它在实践中有许多缺点。代码必须知道所有的锁。对于代码来说,在获得第一个锁之前,很难或者不可能确切地知道它需要哪些锁。它可能需要做一些计算来解决这个问题。
(2)粗粒锁:一次锁住很大的范围
严重的性能损失
六、wait(), notify()和notifyAll()
void notify():唤醒在此对象监视器.上等待的单个线程
void notifyAll():唤醒在此对象监视器上等待的所有线程
void wait():导致当前的线程等待,直到其他线程调用此对象的notify()方法或notifyAll()方法
1.保护块
保护块:这种块首先轮询一个必须为真的条件,然后才能继续。
某些条件未得到满足,所以一直在空循环检测,直到条件被满足。这是极大浪费。

wait()一旦被调用,则释放锁,将当前线程挂起来。
一个线程调用其中的任何一个方法,对象必须已经锁定

你可能感兴趣的:(软件构造笔记)