软件构造总结

软件构造总结

第一章 软件构造的多维度视图和质量目标

本章将重点从软件的三个维度、八个可观察视角分析什么是软件构造、软件系统是如何构成的以及什么才是好的软件系统等

一、多维软件视图

三维度八视图
三个维度
按阶段划分:构造时/运行时视图
按动态性划分:时刻/阶段视图
按构造对象的层次划分:代码/构件视图

八个视角
(1)构建阶段—某时刻—代码层级视图
在该视图中,我们关注于如何按基本程序块(如函数、类、方法、接口等)逻辑组织源代码,以及它们之间的依赖关系。简单来说就是我们的软件系统中的具体的代码实现,小到某个类、某个算法的实现代码;大到软件整体的组织结构,类与类之间的委派继承关系等都属于这个视图,是软件能够正常运行的基础。

(2)构建阶段—某时间段—代码层级视图
在该视图中,我们关注于软件开发中代码发生的变化,这个变化既包括我们的软件的代码从无到有的变化,也包括我们某一时间段(可能是一天、也可能是一个月)内对代码的修改,在软件开发过程中,通过该视图我们可以清晰的看到整体的软件开发进度,同时也能了解到不同的开发成员在某段时间内对代码进行了哪些修改。
代码增减示例

(3)构建阶段—某时刻—构件层级视图
在该视图中,我们是对代码层级的视图进行进一步的封装。在开发过程中,我们会将几个功能近似或紧密度较高的类进行打包,在包之上又可以形成组件或子系统,最终通过这些封装的整体间的组合形成我们最终的软件系统。此外,针对开发过程中某些较为通用的抽象数据结构或实现函数,我们可以以库的方式进行封装,以便后续开发进行代码复用,提升开发效率

(4)构建阶段—某时间段—构件层级视图
在该视图中,我们关注于各项软件实体随时间如何变化,在开发过程中会对哪些文件进行修改,发布的软件的版本又会用到文件的哪个版本都属于该视图的讨论范畴。软件与文件版本对应关系

(5)运行阶段—某时刻—代码层级视图
在该视图中,我们关注于分析软件在运行时特定瞬间的内存变量状态。类似于在编程过程中设置断点,以便检查在某时刻下变量的确切地址和值。通过这种分析,我们可以深入理解软件在特定时间点的内存使用情况,从而进行有效的调试和性能优化。(在软件运行时,我们常使用转储文件来查看程序在计算机中所占的资源)

(6)运行阶段—某时间段—代码层级视图
相较于上一个视图,该视图更像是对上一个视图在时间上的累积。该视图常通过日志(代码层面)的方式展现,日志中会记录程序在运行时段上的不同模块的调用情况,从而动态展现程序中的运行时序关系,以此帮助程序员快速了解程序的运行状态并在发生问题时迅速找到问题的关键。

(7)运行阶段—某时刻—构件层面视图
对该视图的理解可以参考UML中的部署图,该视图中展现了运行时软件系统的结构、构成应用程序的硬件和软件元素的配置和部署方式。

(8)运行阶段—某时间段—构件层面视图
该视图类似于(6)同样通过日志(系统层面)的方式展现,在日志中会记录程序运行时产生的不同的事件,并为其分配专属的序号,以便当程序发生问题时可以快速定位到哪些事件引起了此次问题,从而快速的修复系统中存在的bug

** 软件构造: 视图间的转换**
请添加图片描述
任何一款软件系统都需要通过这八个视角的反复打磨才能最终实现。在构建阶段,软件经历了从底层代码编写到逐层封装,从空无一物的项目到不同文件的版本迭代,最终才能形成一个可以运行的程序并进入运行阶段;在运行阶段,通过对软件的测试运行,不断的通过快照、日志等方式发现软件在构建时出现的问题,并重新退回构建阶段来对现有的问题进行修复与重构。如此循环往复这八个视图之间,最终才能够交付出一个合格软件。而软件构造便是在此基础上深入这八个视图当中,教会我们如何从零开始完成软件开发的任务。

二、软件系统的质量特性

一款软件系统的质量可以由外部因素和内部因素来决定。外部因素是指系统表现给用户的实时反馈,而内部因素是指系统内部的实现水平,对于软件开发的目标来说,外部因素是该软件质量的决定性指标,但内部因素的高低又同样隐含着决定了外部因素的高低

外部因素
外部因素中包括正确性、健壮性、可扩展性、可复用性、兼容性、性能、可移植性、易用性、功能性、及时性、可验证性、完整性、可修复性、经济性等,其中最重要的是正确性,正确是软件开发的前提与保障,而其他属性是对软件质量要求上的进一步提高

内部因素
内部因素中最重要的是复杂性,如何降低软件系统开发的复杂程度对外部因素的提升有至关重要的帮助

五项重要质量考虑因素
①易于理解 ②开发成本低廉 ③易于变化 ④减少错误 ⑤运行高效

第二章 软件测试与测试优先的编程

本章将重点介绍测试对于软件开发的意义,以及测试优先编程在软件开发中的重要作用与地位

一、软件测试

软件测试是提高软件质量的重要手段,同时也是代码是否满足用户需求的最好证明,即程序员是有责任“自证“自己所写的代码是符合用户需求的。因此要求程序员在编写代码时需要尽早发现代码中的问题,然而即使是最好的测试,也无法达到100%的无错误。

测试层级
(1)单元测试:指验证特定代码段功能的测试,通常在函数级进行
(2)集成测试:由多个程序员或编程团队创建的两个或多个类、包、组件、子系统的合并执行
(3)系统测试:测试一个完全集成的系统,以验证该系统是否满足其要求,该系统在最终配置中执行软件。
(4)验收测试:验证系统是否达到了用户需求
(5)回归测试:验证最近对软件的更改或更新是否无意中引入了新错误或对以前的功能方面产生了负面影响
请添加图片描述
** 静态测试与动态测试**
静态测试:在不实际执行程序的情况下进行的测试,就像校对一样,再加上编程工具/文本编辑器检查源代码结构或编译器(预编译器)检查语法和数据流时的静态程序分析。
动态测试:指对代码的动态行为进行测试,即用一组给定的测试用例实际执行已编程的代码。

二、测试优先的编程

测试用例
测试用例:输入+执行条件+期望结果
① 能发现错误 ②不冗余 ③最有效 ④既不过于复杂也不过于简单

测试优先的编程
1.步骤(类似于黑盒测试思想)
①跟据某个用户提出的需求设计对应的方法及其spec(具体方法将在第五章展开讨论)
②针对spec中的功能及其特殊之处设计对应的测试用例
③对方法进行实现,当不能通过测试时,反复进行修改,直至通过测试

2.优点
①尽早的发现程序中的错误
②使编写代码的过程更有成就感
③帮助理解、修正、完善spec设计

3.测试驱动开发
与测试优先的编程对应的开发流程为测试驱动开发(TDD),它依赖于在极短的开发周期内重复进行:将需求转化为非常具体的测试用例,然后改进软件以通过新的测试。
在这里插入图片描述
4.单元测试
单元测试将验证工作集中在软件设计的最小单元–软件组件或模块上。 针对软件的最小单元模型开展测试,隔离各个模块,容易定位错误和调试

5.单元测试框架 JUnit
JUnit 在开发测试驱动开发过程中发挥了重要作用,是单元测试框架系列之一
JUnit 单元测试以方法的形式编写,前面加上注解 @Test,在测试方法中对被测模块的一次或多次调用,使用 assertEquals、assertTrue 和 assertFalse 等断言方法检查结果。
详细细节参见:https://junit.org/junit5/

三、测试用例编写方法

1.等价类划分方法
基于等价类划分的测试:将被测函数的输入域划分为等价类,从等价类中导出测试用例。
划分依据:针对每个输入数据需要满足的约束条件,划分等价类,其中每个等价类代表着对输入约束加以满足/违反的、有效/无效数据的集合

例子1 输入的学号no需满足的条件:
• 长度为10位:10、>10、<10
• 以118开头:以此开头、以其他开头
• 之后两位数应为03/36/37:03、36、37、其他
例子2:Math类中的 max() 函数
根据规范,可以将该函数划分为a < b、a = b、a > b三个等价类
在这里插入图片描述
2.边界值分析方法
由于大量的错误发生在输入域的“边界”而非中央,因此边界值分析(BVA)是作为一种测试技术而开发的,它针对边界值情况进行测试,是对等价类划分方法的补充
例子:Math类中的 max() 函数
Value of a
• a = 0
• a < 0
• a > 0
• a = minimum integer
• a = maximum integer(b情况同理)

3.分区覆盖方式
(1)笛卡尔积:全覆盖
多个划分维度上的多个取值,要组合起来,每个组合都要有一个用例
(2)覆盖每个取值:最少1次即可
每个维度的每个取值至少被1个测试用例覆盖一次即可
(3)对比:
前者:测试完备,但用例数量多,测试代价高
后者:测试用例少,代价低,但测试覆盖度未必高。

4.黑盒测试与白盒测试
黑盒测试:将软件视为一个 “黑盒子”,在不了解内部实现、不查看源代码的情况下检查其功能,对程序外部表现出来的行为的测试,即全从函数spec导出测试用例,不考虑函数内部实现。
白盒测试:通过查看源代码来测试程序的内部结构或工作原理,即针对程序内部代码结构的测试。独立/基本路径测试:对程序所有执行路径进行等价类划分,找出有代表性的最简单的路径(例如循环只需执行1次),设计测试用例使每一条基本路径被至少覆盖1次。

代码覆盖度
代码覆盖度:已有的测试用例有多大程度覆盖了被测程序代码
覆盖度越低,测试越不充分,但要做到很高的代码覆盖度,需要更多的测试用例,测试代价高
测试效果:路径覆盖>分支覆盖>语句覆盖
测试难度:路径覆盖>分支覆盖>语句覆盖

5.测试策略
测试策略(根据什么来选择测试用例,即等价类的划分依据)非常重要,需要在程序中显式记录下来
目的:在代码评审过程中,其他人可以理解你的测试,并评判你的测试是否足够充分

第三章 软件构造过程与配置管理

本章将重点介绍软件配置管理与版本控制系统以及将简单介绍Git作为版本控制工具的使用

一、软件开发生命周期(SDLC)

软件开发生命周期(Software Development Life Cycle,SDLC)包含了软件从开始到发布的不同阶段。它定义了一种用于提高待开发软件质量和效率的过程。因此,SDLC旨在通过最少的资源,交付出高质量的软件。为了避免产生严重项目失败后果,软件开发的生命周期通常可以被划分为如下六个阶段:计划、分析、设计、实现、测试与集成、维护请添加图片描述

二、传统软件过程模型(详情参见软件过程与项目管理课程)

现有软件过程模型:瀑布模型、增量模型、V字模型、原型模型、螺旋模型

三、敏捷开发(详情参见软件过程与项目管理课程)

敏捷开发方法:XP、Scrum等

四、软件配置管理(SCM)和版本控制系统 (VCS)

1.概念
软件配置管理(SCM):追踪和控制软件的变化
软件配置项(SCI):软件中发生变化的基本单元(例如:文件)
基线:软件持续变化过程中的“稳定时刻”(例如:对外发布的版本)
配置管理数据库(CMDB):存储软件的各配置项随时间发生变化的信息+基线

2.版本控制系统
本地版本控制系统:仓库存储于开发者本地机器无法共享和协作
集中式版本控制系统:仓库存储于独立的服务器,支持多开发者之间的协作
分布式版本控制系统:仓库存储于独立的服务器+每个开发者的本地机器

3.Git
Git常用指令及状态如下图所示
在这里插入图片描述

第四章 数据类型与类型检验

本章介绍了Java语言中的数据类型以及Java语言中进行的数据类型检验,其中本章将重点关注于Java语言中数据类型的可变性与不可变性

一、数据类型

基本数据类型
byte,short,int,long,boolean,float,double,char
对象数据类型
例如:String,BigInteger,…
请添加图片描述
基本数据类型的包装
Java中同样中基本数据类型同样有其对应的对象数据类型,如:
Boolean, Integer, Short, Long, Character, Float, Double
他们之间可由自动封箱/装箱功能实现基本数据类型和对象数据类型间的自由转换

类型转换
可参考这篇文章:https://blog.csdn.net/qq_47897078/article/details/120038031

二 静态/动态数据类型匹配检查

静态类型检查:在编译阶段进行类型检查
动态类型检查:在运行阶段进行类型检查
无检查:编程语言不提供类型检查。
静态类型检查 >> 动态 >> 无检查

Java语言属于静态类型语言
静态类型检查可在编译阶段发现错误,避免了将错误带入到运行阶段,可提高程序正确性/健壮性
在静态检查中检查内容包括语法错误、类名/函数名错误、参数数目错误、参数类型错误、返回值类型错误
在动态检查中检查内容包括非法的参数值、非法的返回值、 越界、空指针
静态检查往往与类型有关,与变量的具体值无关,而动态检查相反

三 可变性和不可变性

赋值操作
“赋值”的本质:在内存特定区域开辟一段空间,写入特定值,将该空间与变量(引用)关联到一起。
改变一个变量:将该变量指向另一个值的存储空间
改变一个变量的值:将该变量当前指向的存储空间中写入一个新的值。

不可变性
数据类型不变性:即指那些一旦被创建,其值不能改变的数据类型,当对其进行赋值操作时,实质是将该变量指向一个新的内存空间,而原有内存空间在没有被其他变量指向的条件下将会无法再次访问,最终会由Java垃圾回收机制进行处理(Java中所有基本数据类型均为不可变数据类型)

例如String是不变数据类型,意味着该类型的“值”一旦在内存里被创建,永远不可更改`

String a = "abc";
a = "def";

在执行完上一条语句之后,存储“abc”字符串的内存空间中的值不变化,仍然是“abc”。而a将从指向“abc”字符串的内存空间转而指向“def”字符串的内存空间,“abc”内存空间将无法再次访问

引用的不变性:一旦确定其指向的内存对象,该“指向关系”就不能再被改变,对应Java中的final关键字,当变量被final修饰,该变量的指向将不会再被改变,如果编译器无法确定final变量不会改变,就提示错误,这也是静态类型检查的一部分。
(final其他用法:final类无法派生子类、final方法无法被子类重写)

可变性
数据类型可变性:即可以直接修改其指向的值
引用的可变性:变量和内存区域之间的关联关系可以修改

可变性与不可变性对比
String是不可变类型的一个例子,StringBuilder 是一个可变类型的例子。

String s = "a";
s = s.concat("b");

跟据String的不可变性,我们可以画出如下的快照图,其中双框椭圆代表该内存空间不可改变请添加图片描述

StringBuilder sb = new StringBuilder("a");
sb.append("b");

跟据StringBuilder的可变性,同样可以画出其对应的快照图如下请添加图片描述
此时看来,他们最终的值都是一样的,但当有多个引用的时候,差异就出现了

String t = s;
t = t + c;
        
StringBuilder tb = sb;
tb.append("c");

此时我们引入了新的变量t与tb,并再次跟据他们的可变性与不可变性画出如下快照图
请添加图片描述
跟据快照图,我们可以清晰的看到对t的改变不会影响到s的结果,而对tb的修改却改变了sb的结果(这显然不是我们所期望的)。

在实际应用时,在客户端传入了一个可变数据类型变量时,而在开发方的函数中对该变量内部的值进行了修改,当用户再次使用该变量时,其内部已经变成了新的值;对于开发方,当编写的函数返回了一个可变数据类型变量给客户端,而在程序内部又将重复利用该变量,此时客户端便可通过改变该变量而使程序内部出现不可预料的错误。由此我们可以总结出可变数据类型与不可变数据类型的缺点:
使用可变类型,容易产生非预期的错误修改,对客户端与开发方双方均是一个潜在的威胁
使用不可变类型,对其频繁修改会产生大量的临时拷贝,而可变类型最少化拷贝以提高效率
不可变类型的优点:更加安全
可变数据类型的优点:①可获得更好的性能 ②适合于在多个模块之间共享数据(类似全局变量)

防御式拷贝
针对上述可变数据类型的使用缺陷,主要的应对策略就是防御式拷贝,防御式拷贝的主要的思路就是创建一个跟原来的对象一模一样的对象,并且这个新的对象不会受到原来对象的影响,他们之间彼此独立,从而保证了客户端与开发端的数据安全

第五章 设计规约

本章将重点介绍spec在软件开发中的重要作用以及如何编写合格的spec

一、Spec

Spec概念
Spec是Specifications的简称,没有规约,软件开发是无从下手的;即使写出来程序,也不知道对错,而Spec就是作为一个严格的评价标准提出的,Spec的具体内容是程序与客户端之间达成的一致,Spec给“供需双方”都确定了责任,在调用的时候双方都要遵守
Spec内容
①输入/输出的数据类型 ②功能和正确性 ③性能
参数由 @param 描述,结果由 @return 描述,异常由@throws描述
Spec的作用
Spec作为客户端使用的标准,用户仅需要跟据Spec内容来使用方法,而对于开发方是如何实现的方法是无需关心的,因此作为开发方可以任意的修改方法的内部实现,提升使用的性能(但始终要求满足Spec中的限制,即满足“行为等价性”)可以说,Spec是一堵客户端与开发方的“防火墙”,保证了客户端与开发方的有序运行
在这里插入图片描述
使用Spec的优点
①精确的Spec,有助于客户端与开发方区分责任 ②客户端无需阅读调用函数的代码,只需理解spec即可

前置条件:对客户端的约束,在使用方法时必须满足的条件
后置条件:对开发者的约束,方法结束时必须满足的条件
契约:如果前置条件满足了,后置条件必须满足,反之前置条件不满足,则方法可做任何事情。

要求:
除非在后置条件里声明过,否则方法内部不应该改变输入参数
应尽量遵循此规则,尽量不设计mutating的spec(产生的后果参见第四章)

二、设计Spec

Spec比较
规约的确定性:规范是否只定义了给定输入的单一可能输出,还是允许实现者从一组合法输出中进行选择?
规约的声明性: 规范只是描述了输出应该是什么,还是明确说明了如何计算输出?
规约的强度:该规范是有一小部分合法实施,还是有一大部分合法实施?

Spec替换原则
可以使用更强的Spec替换较弱的Spec (不是所有Spec都是可比的)

spec变强:更放松的前置条件+更严格的后置条件
即对客户的要求更少,实现更多或更优的功能,具体示例如下:
请添加图片描述
越强的规约,意味着开发方的自由度和责任越重,而客户端的责任越轻。

声明式规约
操作式规约,例如:伪代码
声明式规约:没有内部实现的描述,只有“初-终”状态
声明式规约更有价值,因为它们通常更简短、更容易理解,最重要的是,不会无意中暴露客户可能依赖的实施细节。

Spec质量标准
①Spec描述的功能应单一、简单、易理解
②Spec描述的功能信息丰富的,但不能让客户端产生理解的歧义
③Spec应足够强大(太弱的spec,client不放心、不敢用 )
④Spec也应足够薄弱(太强的spec,给开发者增加了实现的难度)
⑤Spec应使用抽象类型
(在规约里使用抽象类型,可以给方法的实现体与客户端更大的自由度)
⑥注意添加前置条件与后置条件的限度
是否使用前置条件取决于(1) 检查用户输入的代价;(2) 方法的使用范围
如果只在类的内部使用该方法(private),那么可以不使用前置条件,在使用该方法的各个位置进行check——责任交给内部client;
如果在其他地方使用该方法(public),那么必须要使用前置条件,若client端
不满足则方法抛出异常

第六章 抽象数据类型 (ADT)

本章将重点介绍如何编写设计自己的ADT以及如何使自己设计的ADT更加健壮

一、抽象数据类型

传统的类型定义:关注数据的具体表示
抽象类型:强调“作用于数据上的操作”,程序员和客户无需关心数据如何具体存储的,只需设计/使用操作即可。
ADT是由操作定义的,与其内部如何实现无关

构造ADT的步骤
(1)决定设计的ADT是可变的还是不可变的
可变类型的对象:提供了可改变其内部数据的值的操作
不变数据类型: 其操作不改变内部值,而是构造新的对象

(2)设计ADT中的操作
ADT中的操作可以分为四类:
①构造器(creator):创建该类型的新对象(可能实现为构造函数或静态函数)
②生产器(producer):从该类型的旧对象创建新对象(例如String.concat())
③观察器(observer):获取抽象类型的对象并返回不同类型的对象(例如List.size() )
④变值器(mutator):改变对象属性的方法(仅出现在可变数据类型中)

设计要求:
①设计简洁、一致的操作
②要足以支持client对数据所做的所有操作需要,且用操作满足client需要的难度要低
③要么抽象、要么具体,不要混合 — 要么针对抽象设计,要么针对具体应用的设计

(3)为每个方法设计Spec (参见第五章)
(4)针对Spec设计测试用例(参见第二章)
针对creator:构造对象之后,用observer去观察是否正确
针对producer:produce新对象之后,用observer判断结果是否正确
针对observer:用其他三类方法构造对象,然后调用被测observer,判断观察结果是否正确
(5)设计AF、RI、Safety from rep exposure(在第三部分展开论述)
(6)设计ADT内表示(属性)
表示独立性:客户使用ADT无需考虑其内部如何实现,ADT内部表示的变化不应影响外部Spec和客户端。
表示独立性的优点:在选择表示时,可以选择对实现更有利属性而不会影响客户端对方法的使用

二、保持不变量

不变量是程序的一种属性,对于程序的每一种可能的运行时状态都始终为真。好的抽象数据类型最重要的属性是它保留自己的不变量。(例如:不可变性就是一个典型的“不变量”)
为什么需要不变量:保持程序的“正确性”,容易发现错误
例子:

public class People {
    public String name;
    public Date birth;
}

在上方的例子中,我们声明了一个People类,并设定People类是一个不可变的数据类型(在类的内部不存在mutator方法),在这个类中我们使用public关键字修饰了name、birth两个表示,看上去People类是可以正常工作的,但当客户端在使用People时是可以通过People.name或People.birth来对对象内部的表示进行修改,从而使得People的不变性被破坏,我们称该问题为表示泄露

那么我们将public改为private关键字并加上final关键字就万事大吉了么? 我们来看下面的例子

public class People {
    private final String name;
    private final Date birth;

	public Date getBirth(){
		return this.birth;
	}
}

在这个例子中name、birth两个表示均被private与final修饰,但该ADT存在一个方法获取People类的birth表示,这看上去固然没有任何问题,但请回想一下第四章所述,由于Date是一个可变数据类型,getBirth()方法将直接获得birth所在的内存空间,尽管birth被final修饰,但客户端同样可以更改该内存空间内的值,最终造成程序出错,这同样是表示泄露的典型方式
特别注意,表示泄露的地方不仅是在与方法的返回值,在对象的实例化上也有可能发生

List<People> list = new ArrayList<>();
Date birth = new Date();
for(int i = 0; i < 5; i++){
    birth.setTime(i);
    list.add(new People("ZhangSan", birth));
}

在这个例子中,客户端目标是构建出具有不同birth的对象,但由于使用的是同一内存空间上的birth,所以这5个实例化的对象对应的birth表示都指向该内存空间,即表示的值最后均为最后一次修改的值。尽管这是由于客户端的使用不当出现的问题,但除非迫不得已,否则不要把希望寄托于客户端上,ADT有责任保证自己的不变量,并避免“表示泄露”。

如何避免表示泄露?①对于所有表示尽可能使用private和final关键词修饰 ②使用防御式拷贝 ③尽可能使用不可变数据类型作为表示

三、表示不变量与抽象函数

R空间:表示值(rep值)的空间由实际实现实体的值组成。
A空间:抽象值构成的空间,即客户看到和使用的值
ADT开发者关注表示空间R,client关注抽象空间A

性质:
① 每个抽象值都映射到某个代表值(满射)。
②一些抽象值被多个代表值映射到(未必单射)。
③不是所有的rep值都被映射(未必双射)。
即使是同样的R、同样的RI,也可能有不同的AF,即“解释不同”。

抽象函数(AF):R和A之间映射关系的函数,即如何去解释R中的每一个值为A中的每一个值。
表示不变量(RI):将rep值映射为布尔值的rep不变量,即描述了什么是“合法”的表示值

步骤选择某种特定的表示方式R,进而指定某个子集是“合法”的(RI),并为该子集中的每个值做出“解释”(AF)——即如何映射到抽象空间中的值

checkRep() 在所有可能改变rep的方法内都要检查是否仍保持RI(rep)

回想一下,当且仅当一个类型的值在创建后永不改变时,该类型才是不可变的。根据我们对抽象空间 A 和 rep 空间 R 的新理解,我们可以完善这一定义:抽象值应永不改变。但是,只要 rep 值继续映射到相同的抽象值,实现就可以自由地更改 rep 值,这样客户端就看不到变化。
这种变化被称为
有益的可变性
,即可以通过牺牲immutability的部分原则来换取“效率”和“性能”
(但这并不代表在immutable的类中就可以随意出现mutator)

有益的可变性方式:①通过cache暂存某些频繁计算的结果
②数据结构再平衡,例如对tree数据结构进行插入或删除节点之后
③懒惰计算,在有需要的时候再进行运算

在代码中用注释形式记录AF、RI与表示泄漏的安全声明

第七章 面向对象的编程(OOP)

本章将重点介绍Java编程中重要的OOP特性以及Java语言中的OOP设计思想

一、基本概念

现实世界中的物体有两个共同特征:它们都有状态行为
物体——类;状态——属性;行为——方法
类成员变量(方法):与类相关联的变量(方法),通常被static关键词修饰
实例成员变量(方法):与实例化对象相关联的变量(方法)

二、接口

Java 中的接口是一个方法签名列表,但没有方法体。如果一个类在其 implements 子句中声明了一个接口,并为该接口的所有方法提供了方法体,则该类实现了该接口。
接口:确定ADT规约;类:实现ADT

接口特性:
接口之间可以继承与扩展
一个类可以实现多个接口(从而具备了多个接口中的方法)
一个接口可以有多种实现类
接口中的每个方法在所有类中都要实现
通过default关键字修饰方法,允许接口中统一实现某些功能,无需在各个类中重复实现它

静态工厂方法
由于接口定义中没有包含constructor,也无法保证所有实现类中都包含了同样名字的constructor。故而,客户端需要知道该接口的某个具体实现类的名字(依旧需要用户了解内部实现)
为解决该问题,这里提出了静态工厂方法,如下例所示:(此处<L>为泛型,将在稍后展开)

public interface Graph<L> {
    public static <L> Graph<L> empty() {
        return new ConcreteEdgesGraph<>();
    }
}

该此例中,我们定义了一个Graph接口,为了让用户不用了解具体实现类的名字,所以在此处设置了静态empty()方法,默认实现类为ConcreteEdgesGraph。客户端在使用该接口时,仅需调用empty()方法即可实例化Graph
客户端代码:

public static void main(String[] args) {
	Graph<String> graph = Graph.empty();
}

优点
①可以自由选择实现方式,增加系统的独立性与灵活性
②当客户端使用接口类型时,静态检查可确保他们只使用接口定义的方法。
③易于理解,客户和维护者都知道在哪里可以找到 ADT 的规范。
请添加图片描述

三、继承与重写

(1)重写(Override)
可重写方法: 允许重新实现的方法。
严格继承:子类只能添加新方法,无法重写超类中的方法(使用final关键词)

重写的函数:完全同样的方法名、参数列表、返回值类型
子类对父类进行重写时,在子类方法前需添加“@Override”来标注该方法为重写方法
父类型中的被重写函数体不为空:意味着对其大多数子类型来说,该方法是可以被直接复用的。对某些子类型来说,有特殊性,故重写父类型中的函数,实现自己的特殊要求。
如果父类型中的某个函数实现体为空,意味着其所有子类型都需要这个功能,但各有差异,没有共性,在每个子类中均需要重写。
当子类包含的方法重写了父类的方法时,它也可以通过使用关键字** super** 来调用超类的方法。重写之后,利用super()复用了父类型中函数的功能,并对其进行了扩展

(2)抽象类
抽象方法:有函数名但没有实现的方法(也称抽象操作)由关键字 abstract 定义
抽象类:至少包含一个抽象方法的类称为抽象类
接口: 仅有抽象方法的抽象类

四、多态、子类型、重载

三种多态性
特殊多态(Ad hoc polymorphism):当一个函数表示不同的、潜在的异构实现时,取决于有限范围内单独指定的类型和组合。许多语言都使用函数重载来支持特设多态性
参数化多态(Parametric polymorphism):在编写代码时不提及任何特定类型,因此可以透明地用于任何数量的新类型。在面向对象编程界,这通常被称为泛型或通用编程
子类型多态(又称子类型多态或包含多态):当一个名称表示许多不同类的实例时,它们之间存在着某种共同的超类。(将在第九章展开叙述)

重载(Overload)
重载的函数:具有同样的函数名,但有不同的参数列表,相同/不同返回值类型
重载的价值:方便客户端调用,客户端可用不同的参数列表,调用同样的函数

重载根据参数列表进行最佳匹配,使用静态类型检查策略
与之相反,重写则是在运行时进行动态检查
重载也可以发生在父类和子类之间

重载与重写比较
请添加图片描述
泛型
泛型:可以根据运行时传递的参数进行工作,即允许静态类型检查,而无需完全指定类型
通用编程是一种编程风格,在这种风格中,数据类型和函数都是以待定类型的形式编写的,然后在需要时对作为参数提供的特定类型进行实例化。

类型变量:非限定标识符,使用菱形运算符 <> 来帮助声明类型变量。例如:
List<Integer> ints = new ArrayList<Integer>();
public interface List<E>
public class Entry<KeyType, ValueType>
泛型类/接口/方法:其定义中包含了类型变量,即使用某一标识符代表所有可能出现的类

Set 是泛型类型的一个例子:这种类型的规范是以后填充的占位符类型。我们不需要为 Set<String> 、Set<Integer> 等分别编写规范和实现,而是设计和实现一个 Set<E> 。

泛型特性:
①可有多个类型参数
例如:Map<E, F>, Map<String, Integer>
②类型擦除
编译器在完成代码编译后会丢弃类型参数的类型信息,因此在运行时无法获得这些类型信息,这一过程叫做类型擦除
③通配符,只在使用泛型的时候出现,不能在定义中出现
使用场景:假设现在有一个Animal类,一个Dog类和一个Cat类,Dog类和Cat类继承自Animal类

public static void test1(List<Animal> list){
   	for(var animal : list){
        System.out.println(animal);
   	}
}
public static void main(String[] args) {
   	List<Dog> list = new ArrayList<>();
   	test1(list);
}

在上面这个例子中我们可以看到,我们定义了test1方法,其中的参数为List<Animal> list,但在实际调用方法时,我们向该方法中传入了List<Dog>。当我们在编译器中编写这段代码时,我们会发现编译会在 test1(list);中报错。
尽管Dog为Animal的子类,但编译器并不允许这种行为(这是因为该行为会导致List等泛型容器内部的类型紊乱,因此编译器将其视作一种不合法的行为)
为了能够匹配多种类型的参数,我们引入了通配符"?",将List<Animal> list中的Animal换做?,编译器会在运行时进行泛型擦除,从而保证泛型容器内部类型的统一。此时我们可以将List<(任意)>作为参数传入test1中
此外,为了限制传入的类型,我们可以使用通配符**“? extends Animal”,其效果是可以允许Animal及其子类作为类型参数传入方法,而其他类型的参数一律不接受。
同理第三种通配符
"? super Dog",他可以向泛型容器中添加Dog及其超类**作为类型参数传入方法,而其他类型的参数一律不接受。
④无法创建通用数组
例如:Pair[] foo = new Pair[42]; // 报错

五、Object类

在Java中所有类都继承自Object(无extends时默认extends Object)
Object类中包含:equals() 、hashCode()、toString()三个通用方法。但这三个方法并不能适配于每个类
在Object类中equals()与hashCode()验证的是两个类的地址信息,而toString()打印出的信息并不符合人们的直观感受,因此在编写设计自己的ADT时,常常需要重写这些方法来达到开发者想要的效果
在IDE中常常带有自动重写他们的功能

六 设计高质量类

不可变类型类的优点:简单 ▪ 本身线程安全 ▪ 可自由共享 ▪ 无需防御副本 ▪ 出色的构建模块
不可变类编写建议:
①不提供任何mutator
②确保任何方法都不能被重写
③使所有表示都是被final修饰的
④使所有表示都是被private修饰的
⑤确保任何可变组件的安全性(避免表示暴露)
⑥实现 toString()、hashCode()、clone()、equals() 等功能。
**可变类编写建议:**最小化变化内容

第八章 ADT和OOP中的“等价性”

本章将重点介绍不可变对象之间的引用等价性和对象等价性和可变数据类型的观察等价性和行为等价性

一、不可变类型的等价性

基于抽象函数AF定义ADT的等价操作: AF映射到同样的结果,则等价
即站在外部观察者角度:对两个对象调用任何相同的操作,都会得到相同的结果,则认为这两个对象是等价的。

== 和 equals()
引用等价性: == 运算符比较引用。它测试引用是否相等。如果两个引用指向内存中的同一个存储空间。
对象等价性: equals() 操作比较对象的内容,换句话说,就是比较对象的平等性。
对基本数据类型,使用引用等价性;对对象类型,使用equals()

重写equals()
注:在重写equals()方法前请注意添加@Override,否则将变成重载且较难发现问题
下面是一个重写Name类的equals()方法的例子,Name类中包含String first和String last两个表示

@Override public boolean equals(Object o) {
	if (!(o instanceof Name))	return false;
	Name n = (Name) o;
	return n.first.equals(first) && n.last.equals(last);
} 

istanceof运算符用于测试对象是否是特定类型的实例。一般来说,在面向对象编程中使用 instanceof 是不好的。(因为违反了开放封闭原则,在添加一个新子类时,可能会需要对父类代码进行修改,详情将在第十章展开)
除了实现equals外,任何地方都不允许使用 instanceof。(对于getclass()同理)
“n.first.equals(first)”这里使用了equals()是由于first是String类型,在String类型中重写了equals()

重写hashCode()
Object中的hashCode()返回值为该对象的内存空间地址
要求:
①相等的对象必须有相等的hashCode(),如果重写 equals,则必须重写hashCode
②不相等的对象应有不同的hashCode(非必须,有可能发生哈希冲突,在构建时尽可能使用类中的表示)
③除非对象发生变化,否则hashCode不得更改
如何重写
标准的做法是,为对象的每个组件计算用于确定相等性的哈希代码(通常是调用每个组件的哈希代码方法),然后将这些代码组合起来,再进行一些算术运算。

二、可变数据类型的等价性

观察等价性:在不改变状态的情况下,两个可变对象是否看起来一致
例:Date类的equals():"当且仅当getTime方法为两个Date对象返回相同的长值时,两个Date对象才相等。
行为等价性:调用对象的任何方法都展示出一致的结果
例:StringBuilder类的equals继承自Object类(即指向同样内存空间的objects,才是相等的

对可变类型来说,倾向于实现严格的观察等价性,但在有些时候,观察等价性可能导致bug,甚至可能破坏RI
例子:

List<String> list = new ArrayList<>();
list.add("a");
Set<List<String>> set = new HashSet<>();
set.add(list);
list.add("b");
System.out.println(set.contains(list));
for(List<String> l : set){
    System.out.println(set.contains(l));
}

在上面的代码中,两次打印的结果均为false,这是因为当列表第一次被放入哈希集合时,它被存储在与当时的哈希编码()结果相对应的哈希桶/散列桶中。当列表随后发生变化时,它的 hashCode() 也会发生变化,但 HashSet 并没有意识到它应该被移动到另一个桶中。因此,再也找不到它了。当 equals() 和 hashCode() 会受到突变的影响时,我们就会破坏使用该对象作为键的哈希表的 rep 不变性。
如果某个mutable的对象包含在HashSet集合类中,当其发生改变后,集合类的行为不确定
在JDK中,不同的mutable类使用不同的等价性标准

对可变类型,实现行为等价性即可,无需重写equals()与hashCode(),直接继承Object的两个方法即可。
如果一定要判断两个可变对象看起来是否一致,最好定义一个新的方法。

第九章 面向复用的软件构造技术

本章将重点介绍复用在软件开发中的重要性,如何设计可复用的类以及Liskov替换原则(LSP)

一、软件复用

软件复用是利用现有软件组件实施或更新软件系统的过程
两个视角:
面向复用编程:开发出可复用的软件
基于复用编程:利用已有的可复用软件搭建应用系统
请添加图片描述
复用优点:
① 降低成本和开发时间 ②经过充分测试,可靠、稳定 ③标准化,在不同应用中保持一致
复用代价:
面向复用编程开发成本高于一般软件的成本:要有足够高的适应性,但面对具体场景缺少足够的针对性
基于复用编程往往无法拿来就用,需要适配
因此不仅面向复用编程代价高,基于复用编程代价也高
请添加图片描述
复用性评价标准
①小、简单 ②与标准兼容 ③灵活可变 ④可扩展 ⑤泛型、参数化 ⑥模块化 ⑦变化的局部性 ⑧稳定 ⑨丰富的文档和帮助

复用层级
源代码级:方法、语句等
模块级:类和接口
库级: API
架构级别:框架

复用种类
白盒复用:源代码可见,复制已有代码到正在开发的系统,进行修改
黑盒复用:源代码不可见,只能通过API接口来使用,无法修改代码

二、设计可复用类

子类型多态:客户端可用统一的方式处理不同类型的对象
LSP原则: Let q(x) be a property provable about objects x of type T, then q(y) should be provable for objects y of type S where S is a subtype of T. ——Barbara Liskov
即在可以使用a的场景,都可以用c代替而不会有任何问题,其中c为a的子类

行为子类型:
①子类型可以增加方法,但不可删
②子类型需要实现抽象类型中的所有未实现方法
③子类型中重写的方法必须有相同类型的返回值或者符合协变的返回值
④子类型中重写的方法必须使用同样类型的参数或者符合逆变的参数
⑤子类型中重写的方法不能抛出额外的异常,抛出相同或者符合协变的异常

可简化为:
①更强的不变量 ②更弱的前置条件 ③更强的后置条件

协变
父类型→子类型:Spec越来越具体
返回值类型:不变或变得更具体
异常的类型:不变或变得更具体。
逆变
父类型→子类型:越来越具体specific
参数类型:要相反的变化,不变或越来越抽象(目前Java中遇到这种情况,当作overload看待)

三、委派与组合

委派/委托:一个对象请求另一个对象的功能
委派模式:通过运行时动态绑定,实现对其他类中代码的动态复用
例如:
请添加图片描述
委派三部曲:①保存委派关系 ②建立委派关系 ③进行功能委派

委派优点
一个类不需要继承另一个类的全部方法,通过委托机制调用部分方法,从而避免大量无用的方法

组合复用原则(CRP)
委托可以看作是对象级的重用机制,而继承则是类级的重用机制。
CRP原则更倾向于使用委派而不是继承来实现复用。
请添加图片描述

通用设计结构
请添加图片描述
遵循CRP原则,尽量避免通过继承机制进行面向复用的设计,尽量通过CRP设计两棵继承树,通过delegation实现“事物”和“行为”的动态绑定,支撑灵活可变的复用

委派关系
(1)依赖:临时性的委托,委派关系通过方法的参数传递建立起来
(2)关联:永久性的委托,委派关系通过固有表示建立起来
(3)组合:更强的关联,但难以变化,其中委派关系通过类内部表示初始化建立起来,无法修改
(4)聚合:更弱的关联,可动态变化,其中关联关系通过客户端调用构造函数或专门方法建立起来
在组合中,当拥有对象被销毁时,包含的对象也会被销毁,而聚合并没有这层关系
都支持1对多的委派

四、框架

框架:一组具体类、抽象类、及其之间的连接关系
开发者根据框架的规约,填充自己的代码进去,形成完整系统
请添加图片描述
白盒框架,通过代码层面的继承进行框架扩展
黑盒框架,通过实现特定接口/委派进行框架扩展

第十章 面向可维护性的构造技术

本章将重点介绍什么是软件维护以及实现高可维护性的设计原则

一、软件维护与演化

软件维护是指在软件产品交付后对其进行修改,以纠正错误、提高性能或其他属性。
软件维护类型
①纠错性 25% ②适应性 21% ③完善性 50% ④预防性 4%

软件演化:对软件进行持续的更新

可维护性指标
圈复杂度、代码行数、可维护性指数、继承的层次数、类之间的耦合度、单元测试的覆盖度

二、模块化设计

模块化编程是一种设计技术,强调将程序的功能分离成独立、可互换的模块,使每个模块只包含执行所需功能的一个方面所需的一切。

评估模块化的五个标准
可分解性、可组合性、可理解性、可持续性、出现异常之后的保护

模块化设计的五条规则
直接映射、尽可能少的接口、尽可能小的接口、显式接口、信息隐藏

耦合与内聚
耦合是衡量模块之间依赖性的标准。如果一个模块的变化可能需要另一个模块的变化,那么两个模块之间就存在依赖关系。
内聚是衡量一个模块的功能或职责之间关联程度的标准。如果一个模块的所有元素都朝着同一个目标努力,那么该模块的内聚力就很高。
最好的设计是模块内部具有高内聚性,模块之间具有低耦合性

三、面向对象设计原则 SOLID

SOLID:
(SRP) The Single Responsibility Principle 单一责任原则
(OCP) The Open-Closed Principle 开放-封闭原则
(LSP) The Liskov Substitution Principle Liskov替换原则
(ISP) The Interface Segregation Principle 接口隔离原则
(DIP) The Dependency Inversion Principle 依赖转置原则

(1)单一责任原则
不应该有多于1个原因让你的ADT发生变化,否则就拆分开
责任是变化的原因,不应有多于1个的原因使得一个类发生变化

(2)开放/封闭原则
对扩展性的开放:模块的行为应是可扩展的,从而该模块可表现出新的行为以满足需求的变化
对修改的封闭:模块自身的代码是不应被修改的

关键的解决方案:抽象技术
通过构造一个抽象类,该抽象类中包含针对所有类型的子类都通用的代码,从而实现了对修改的封闭;
当出现新的子类型时,只需从该抽象类中派生出具体的子类即可,从而支持了对扩展的开放。

(3)Liskov替换原则 (参见第九章)

(4)接口隔离原则
只提供必需的接口
胖接口可分解为多个小的接口,不同的接口向不同的客户端提供服务,客户端只访问自己所需要的端口)

(5)依赖转置原则
抽象的模块不应依赖于具体的模块,具体应依赖于抽象
即上层客户端的代码面向抽象接口编程,隔离对下层具体实现机制的直接接触

四、语法驱动的构造

由于有一类应用,从外部读取文本数据,在应用中做进一步处理。例如:文件特定格式、网络上传输过来的消息、命令行输入的指令等
解决方法:正则表达式, 根据语法,开发一个它的解析器,用于后续的解析

常用正则表达式语法:
. - 除换行符以外的所有字符
[abc] - 匹配 a、b 或 c 中的一个字母。
[a-z] - 匹配 a 到 z 中的一个字母。
[^abc] - 匹配除了 a、b 或 c 中的其他字母。
aa|bb - 匹配 aa 或 bb。
? - 0 次或 1 次匹配。
* - 匹配 0 次或多次。
+ - 匹配 1 次或多次。
{n} - 匹配 n次。
{n,} - 匹配 n次以上。
{m,n} - 最少 m 次,最多 n 次匹配。

第十一章 面向可复用性和可维护性的设计模式

本章将重点介绍几个可复用性和可维护性重要的设计模式

一、工厂方法模式

适用情况
当client不知道要创建哪个具体类的实例,或者不想在client代码中指明要具体创建的实例时,用工厂方法。
定义一个用于创建对象的接口,让其子类来决定实例化哪一个类,从而使一个类的实例化延迟到其子类。
设计样例
请添加图片描述
例子

public interface Animal {
    public void name();
}


public class NormalAnimal implements Animal{
    @Override
    public void name(){
        System.out.println("No name");
    }
}


public class NamedAnimal implements Animal{
    String name;
    public NamedAnimal(String name){
        this.name = name;
    }

    @Override
    public void name() {
        System.out.println(name);
    }
}

在这个例子中定义了Animal有两个实现类NormalAnimal和NamedAnimal

public interface Factory {
    public Animal create();
    public Animal create(String name);
}

public class FactoryImplements implements Factory{
    @Override
    public Animal create() {
        return new NormalAnimal();
    }

    @Override
    public Animal create(String name) {
        if(!name.isBlank()) return new NamedAnimal(name);
        else return new NormalAnimal();
    }
}

在这里定义了Factory的接口和其实现类,在客户端中仅需要使用工厂方法即可分别创建Animal的实例而无需了解NormalAnimal和NamedAnimal的具体实现与方法名称,客户端代码如下:

public static void main(String[] argc){
   Animal animal1 = new FactoryImplements().create();
   Animal animal2 = new FactoryImplements().create("Dog");
   animal1.name();
   animal2.name();
}
//输出结果:
//No name
//Dog

优点:
无需将特定于应用程序的类绑定到代码中
代码只与产品接口打交道,因此可与任何用户定义的具体产品配合使用
潜在的缺点
客户可能不得不为创建某个实体类而创建一个工厂的子类。
如果客户必须对工厂类进行子类化,那么这种情况是可以接受的,但如果不是这样,客户就必须创建另一个工厂类。

二、适配器模式

适用情况
将某个类/接口转换为client期望的其他形式
设计样式
在这里插入图片描述

三、装饰器模式

适用情况
为对象增加不同侧面的特性

构造样式
请添加图片描述
例子

interface Stack {
	void push(Item e);
	Item pop();
}

//基础实现
public class ArrayStack implements Stack {
	public void push(Item e) {}
	public Item pop() {}
}

//装饰器
public abstract class StackDecorator implements Stack {
	protected final Stack stack; //委派动态链接
	public StackDecorator(Stack stack) {
		this.stack = stack;
	}
	public void push(Item e) {
		stack.push(e);
	}
	public Item pop() {
		return stack.pop();
	}
}

//Undo特性
public class UndoStack	extends StackDecorator implements Stack {
	private final UndoLog log = new UndoLog();
	public UndoStack(Stack stack) { 
		super(stack); 
	}
	public void push(Item e) {
		log.append(UndoLog.PUSH, e); //特性实现
		super.push(e);
	}
	public void undo() {}
}
//其他特性...

构建一个secure synchronized undo stack
Stack t = new SecureStack(new SynchronizedStack(new UndoStack(s)) (像一层一层的穿衣服)

四、策略模式

适用情况
有多种不同的算法来实现同一个任务,但客户端需要通过委派动态切换算法,而不是写死在代码里
设计样式
在这里插入图片描述

五、模板模式

适用情况
做事情的步骤一样,但具体方法不同
设计样式
在这里插入图片描述
使用继承和重写实现模板模式(白盒框架)

六、迭代器

适用情况
客户端希望遍历被放入容器/集合类的一组ADT对象,无需关心容器的具体类型
设计样式
在这里插入图片描述
让自己的集合类实现Iterable接口,并实现自己的独特Iterator迭代器(hasNext, next, remove),允许客户端利用这个迭代器进行显式或隐式的迭代遍历:
例子

public class Pair<E> implements Iterable<E> {
	private final E first, second;
	public Pair(E f, E s) { first = f; second = s; }
	public Iterator<E> iterator() {
	return new PairIterator();
}

private class PairIterator implements Iterator<E> {
	private boolean seenFirst = false, seenSecond = false;
	public boolean hasNext() { return !seenSecond; }
	public E next() {
		if (!seenFirst) { seenFirst = true; return first; }
		if (!seenSecond) { seenSecond = true; return second; }
		throw new NoSuchElementException();
	}
	public void remove() {
		throw new UnsupportedOperationException();
	}
}

七 Visitor设计模式

适用情况
为ADT预留一个将来可扩展功能的“接入点”,外部实现的功能代码可以在不改变ADT本身的情况下通过delegation接入ADT
设计样式
在这里插入图片描述
对比策略模式
二者都是通过delegation建立两个对象的动态联系
但是Visitor强调是的外部定义某种对ADT的操作,该操作于ADT自身关系不大(只是访问ADT),故ADT内部只需要开放accept(visitor)即可,客户端通过它设定visitor操作并在外部调用。
而Strategy则强调是对ADT内部某些要实现的功能的相应算法的灵活替换。这些算法是ADT功能的重要组成部分,只不过是delegate到外部strategy类而已。

第十二章 面向正确性与健壮性的软件构造

本章将重点介绍软件构造最关键的质量特性——健壮性和正确性以及错误处理方式

一、健壮性与正确性

健壮性:系统在不正常输入或不正常外部环境下仍能够表现正常的程度
正确性:程序按照spec加以执行的能力,是最重要的质量指标

面向健壮性的编程
处理未期望的行为和错误终止
即使终止执行,也要准确/无歧义的向用户展示全面的错误信息(错误信息有助于进行debug)

**Postel’s Law:**对自己的代码要保守,对用户的行为要开放,即总是假定用户恶意、假定自己的代码可能失败, 把用户想象成白痴,可能输入任何东西

正确性与健壮性的比较
健壮性:让用户变得更容易:出错也可以容忍,程序内部已有容错机制
正确性:让开发者变得更容易:用户输入错误,直接结束。(不满足precondition的调用)
对内,追求正确性;对外,追求稳健性。

可靠性 = 正确性 + 健壮性

提高稳健性和正确性的步骤
第 0 步:使用断言、防御性编程、代码审查、正式验证等方法,按照稳健性和正确性目标编 写代码
第 1 步:观察故障症状(内存转储、堆栈跟踪、执行日志、测试)
第 2 步:识别潜在故障(错误定位、调试)
第 3 步:修复错误(代码修订)

度量健壮性和正确性
外部观察角度:平均失效间隔时间(MTBF)、平均故障时间(MTTF)
内部观察角度:残余缺陷率

二、错误与异常

内部错误:程序员通常无能为力,一旦发生,想办法让程序优雅的结束,例如用户输入错误、设备错误、物理限制
异常:你自己程序导致的问题,可以捕获、可以处理
在这里插入图片描述
异常可以分为:运行时异常和其他异常
运行时异常:由程序员在代码里处理不当造成
其他异常:是程序员无法完全控制的外在问题所导致的

Checked and unchecked 异常
Checked异常:编译器可帮助检查你的程序是否已抛出或处理了可能的异常(需要从Exception派生出子类型)
Unchecked 异常:错误和运行时异常(可以不处理,编译没问题,但执行时出现就导致程序失败,代表程序中的潜在bug)

异常处理方法
throws: 声明“本方法可能会发生XX异常”
throw: 抛出XX异常
try, catch, finally:捕获并处理XX异常

Checked异常适用场景:错误可预料,但无法预防,但可以有手段从中恢复,此时使用checked 异常。
在这里插入图片描述异常声明
你所调用的其他函数抛出了一个checked异常——从其他函数传来的异常
当前方法检测到错误并使用throws抛出了一个checked异常——你自己造出的异常
不要抛出Unchecked异常

如果子类型中override了父类型中的函数,那么子类型中方法抛出的异常不能比父类型抛出的异常类型更宽泛(参见第九章)

捕获异常
异常发生后,如果找不到处理器,就终止执行程序,在控制台打印出stack trace。
尽量在自己这里进行try/catch处理,实在不行就往上传,由客户端自己处理

异常连锁
允许在catch里抛出异常,目的:更改exception的类型,更方便client端获取错误信息并处理

final关键字
final被用于try/catch异常捕获块后,用于异常发生后这些资源要被恰当的清理
不管程序是否碰到异常,finally都会被执行

三、断言

断言:在开发阶段的代码中嵌入,检验某些“假设”是否成立。若成立,表明程序运行正常,否则表明存在错误。
**目的:**断言主要用于开发阶段,避免引入和帮助发现bug
断言一般涵盖程序的正确性问题、异常一般涉及程序的稳健性问题。

适用场景
如果参数来自于外部(不受自己控制),使用异常处理
如果来自于自己所写的其他代码,可以使用断言来帮助发现错误(例如postcondition就需要)

四、防御式编程

(1)保护程序免受无效输入的影响
对来自外部的数据源要仔细检查,例如:文件、网络数据、用户输入等
对每个函数的输入参数合法性要做仔细检查,并决定如何处理非法输入
(2)设置路障
类的public方法接收到的外部数据都应被认为是dirty的,需要处理干净再传递到private方法——隔离舱

  • 22
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值