软构随笔复习1

多维度视图
Build-time:
Code-level view(代码的逻辑组织):源码、类结构;代码变化
Component-level view(代码的物理组织):文件、目录、包、库、静态链接、测试项;配置项、版本
Moment view(源码和组件在特定时刻的软件形态):源码、类结构;文件、目录、包、库、静态链接、测试项
Period view(软件形态随时间的变化):代码变化;配置项、版本

Run-time:
Code-level view(逻辑实体在内存中如何呈现):代码快照、内存转储;堆栈轨迹、并发线程
Component-level view(物理实体在物理硬件环境中如何呈现):包、库、动态链接、数据库、网络、硬件;事件日志、多进程、分布式程序
Moment view(逻辑/物理实体在内存/硬件环境中特定时刻的形态):代码快照、内存转储;包、库、动态链接、数据库、网络、硬件
Period view(逻辑/物理实体在内存/硬件环境中的形态随时间的变化):堆栈轨迹、并发线程;事件日志、多进程、分布式程序

源码组织在文件中,进而组织在目录中;文件存档在包中,并逻辑上归属于组件和子系统;可重用的模块以库的形式存在,库存储在磁盘文件中。
静态链接:库被拷贝进代码形成整体,执行的时候无需提供库文件。
动态链接:库文件不会在构建阶段被加入可执行软件,仅仅做出标记;程序运行时,根据标记装载库至内存;发布软件时,将程序所依赖的所有动态库都复制给用户。
分布式程序:需要多个运行程序,分别部署于多个计算机物理环境
代码快照:描述程序运行时内存里变量层面的状态
内存转储(Memory dump):一个包含进程内存拷贝的磁盘文件,包含程序异常退出时的寄存器、调用栈、程序数据等,调试器可以加载转储文件并显示信息
执行跟踪(Execution tracing):软件层面,用日志方式记录程序执行的调用次序
事件日志:系统层面的日志

软件的质量因素
外部质量因素:
正确性:最重要的质量指标
健壮性:针对异常情况的处理,关键在于出现异常时不要崩溃,是正确性的补充
可扩展性
可复用性
兼容性
性能:过度的优化会导致软件不再适应变化和复用
可移植性
易用性
功能性
及时性
内部质量因素:
源码方面:行数(LoC)、逻辑复杂度
结构方面:耦合和内聚
此外还有可读性等

五个核心质量因素:
specification规约
reusability可复用性
maintainability可维护性
robustness and correctness健壮性和正确性
ADT and OOP

软件测试
测试的目的是发现错误,再好的测试也无法证明系统里不存在错误。

测试:发现是否存在错误
调试:识别错误根源,消除错误

测试的分类
从范围上看
单元测试:针对软件的最小单元模型开展测试(一般来说是在单个方法/类的级别),隔离各个模块,容易定位错误和调试
集成测试:将多个程序员/团队编写的类/包/组件/子系统联合起来测试
系统测试:对整个系统进行测试,将硬件、软件、配置信息等看作一个整体
验收测试:产品发布之前所进行的软件测试活动,是技术测试的最后一个阶段,目的是确保软件准备就绪,并且可以让最终用户将其用于执行软件的既定功能和任务
回归测试:一旦程序被修改,重新执行之前的所有测试以确认修改没有引入新的错误或导致其他代码产生错误
从静态/动态上看
静态测试:在编写代码的阶段由程序员或是代码编辑器、编译器等工具进行检查(如语法检查、代码评审)
动态测试:通过测试用例实际执行了编写的代码
动态测试可能在程序完全编写完成前就用于测试代码的特定节
从结构上看
白盒测试:对程序内部代码结构的测试
黑盒测试:对程序外部表现出来的行为的测试(例如输入输出)
编写测试的过程:先写规约,再写符合规约的测试用例,写代码、执行测试、有问题再改、再执行测试用例,直到通过它。
测试驱动开发(TDD):将需求转化为具体的测试用例,然后软件经过改进,通过新的测试。

黑盒测试:
基于等价类划分的测试,边界值分析
两种覆盖方式
笛卡尔积:全覆盖
将多个划分维度上的多个取值组合起来,每个组合都要有一个用例。不过并非所有组合情况都可能。
测试完备,但用例数量多,测试代价高。
覆盖每个取值:最少 1 次即可
每个维度的每个取值至少被 1 个测试用例覆盖一次即可。
测试用例少,代价低,但测试覆盖度未必高。

白盒测试:
独立/基本路径测试:对程序所有执行路径进行等价类划分,找出有代表性的最简单的路径(例如循环只需执行 1 次),设计测试用例使每一条基本路径被至少覆盖 1 次。
覆盖度测试
代码覆盖度:已有的测试用例有多大程度覆盖了被测程序,通常用百分比衡量
代码覆盖度越低,测试越不充分;但要做到很高的代码覆盖度,需要更多的测试用例,测试代价高。
语句覆盖:每条语句至少执行一次
分支覆盖:判定中每个条件的所有可能结果至少出现一次,并且每个判定本身的所有可能结果也至少出现一次
路径覆盖:每条可能执行到的路径至少执行一次
测试效果:路径覆盖 > 分支覆盖 > 语句覆盖
测试难度:路径覆盖 > 分支覆盖 > 语句覆盖

测试策略——非常重要,需要在程序中显式记录下来

Junit、EclEmma、VisualVM、AppPerfect:动态检查,需要执行代码
CheckStyle、SpotBugs、PMD:静态检查,无需执行代码

传统开发模型
两种基本类型:线性过程、迭代过程
瀑布过程
特点:线性推进、整体推进、非迭代
优点:管理简单
缺点:无法适应需求增加/变化
增量过程
特点:线性推进、增量式(多个瀑布的串行)、非迭代
优点:比较容易适应需求的增加
V-Model
V 模型可以看作瀑布模型的优化,它仍然是线性推进的,瀑布模型存在的问题大多在 V-model 中也存在。
每个开发阶段都有相应的测试对齐进行验证,但是测试与开发是串行而非并行进行的,也就是测试需要等开发完成后再开始。
原型过程
开发出来之后由用户试用/评审,发现问题反馈给开发者,开发者修改原有的实现,继续交给用户评审,循环往复这个过程,直到用户满意为止。重点在于在原型上持续不断地迭代发现用户变化的需求。
特点:迭代推进
优点:开发质量高
缺点:时间代价高
螺旋过程
多轮迭代基本遵循瀑布模式
每轮迭代有明确的目标 ,遵循“原型”过程
进行严格的风险分析, 方可进入下一轮迭代

敏捷宣言四个维度:
个体和互动高于流程和工具
工作的软件高于详尽的文档
客户合作高于合同谈判
响应变化高于遵循计划
敏捷 = 增量 + 迭代,每次迭代处理一个小规模增量

软件配置管理(SCM)
软件配置管理:追踪和控制软件的变化,包括版本控制和软件配置项
软件配置项(SCI):软件中发生变化的基本单元(例如:文件)
版本控制分类
本地版本控制系统
仓库存储于开发者本地机器,无法共享和协作。
集中式版本控制系统
仓库存储于独立的服务器, 支持多开发者之间的协作。
分布式版本控制系统
仓库存储于独立的服务器 + 每个开发者的本地机器。

Git
Git是一个分布式版本控制系统
一个 Git 仓库分为三个部分:
.git 目录:本地的 CMDB
工作目录:本地文件系统
暂存区:.git 目录中的一个文件,隔离工作目录和 Git 仓库
文件有三种状态:已修改;已暂存;已提交

一般:每个 commit 指向一个父亲
分支:多个 commit 可指向同一个父亲
合并:一个 commit 指向两个父亲
一个 “分支”只是一个指向 commit 的别名
HEAD 指向当前工作的 commit
一条边A→B表示在版本 B 的基础上作出变化,形成了版本 A。

结点
一个 commit 存储一个树形结点。tree 中包含了数个 blob;每个 blob 是一个压缩了的仓库文件,不保存文件名信息。
对于每个文件的每个版本(或是不同文件名但相同内容的文件),Git 只会存储一个 blob,而允许多个 commit tree 指向同一个 blob。
一个 commit 中与之前相比未发生变化的文件,无需重复存储。文件未发生变化,则后续多个版本始终指向同一个 blob;文件发生变化了,存储两份不同的 blob,两个版本指向不同的 blob。
传统 VCS 和 Git 对比
传统 VCS:存储版本之间的变化(行)
Git:存储发生变化的文件(而非代码行), 不变化的文件不重复存储

软件构造的一般过程
软件构造的一般过程包括编码、重构、调试、测试、性能分析、代码评审、构建。
编码
从用途上划分:编程语言、建模语言、配置语言、构建语言
从形态上划分:基于语言学的构造语言、基于数学的形式化构造语言、基于图形的可视化构造语言
建模语言:用于可视化、推理、验证和传达系统设计
配置语言:用于配置程序的参数和初始设置
代码评审
结对编程
走查
正式评审会议
自动化评审
性能分析
动态分析:执行程序并观察现象、收集数据、分析不足
Profiling:对代码的运行时状态和性能进行度量,发现代码中的潜在问题

数据类型

类型检查
静态类型语言:在编译阶段进行类型检查,所有变量的类型在编译期就已经确定
动态类型语言:在运行阶段进行类型检查,变量的类型在运行期才能确定
静态类型检查:可发现语法错误、类名/函数名错误、参数数目错误、参数类型错误、返回值类型错误
静态类型检查可在编译阶段发现错误,避免了将错误带入到运行阶段,可提高程序正确性/健壮性
动态类型检查:可发现非法的参数值、非法的返回值、越界、空指针
静态类型检查是关于“类型”的检查,不考虑值;动态检查是关于“值”的检查。
Java 是一种静态类型语言,它同时具有静态类型检查和动态类型检查。

可变性和不可变性
改变变量和值的区别
改变一个变量:将该变量指向另一个值的存储空间
改变一个变量的值:将该变量当前指向的值的存储空间中写入一个新的值
不变性
不变数据类型(immutable type):一旦被创建,其值不能改变
引用不变:一旦确定其指向的对象,不能再被改变(但其值是可能变化的)
Java 中的 final 关键字
final 类无法派生子类
final 方法无法被子类重写
final 变量无法改变其引用(而非值)
不可变类型(immutable):一旦被创建,就始终代表(对外表现出)同样的值。

可变类型(mutable):拥有方法可以修改自己的值。

对于不可变类型,频繁对其运算会产生大量的临时拷贝,可变类型能最少化拷贝以提高效率。因此,使用可变数据类型,可获得更好的性能,也适合于在多个模块之间共享数据。
而不可变类型更“安全”, 在其他质量指标上表现更好。例如,向任何方法传入不可变类型的值,不用担心其值被意外修改;多线程可以安全地持有同一个不可变类型的引用,而不用担心竞争。
保护可变类型
防御式拷贝:给客户端返回一个全新的对象
大部分时候该拷贝不会被客户端修改, 可能造成大量的内存浪费
单引用局部变量:把对 mutable 对象的引用限制在类/方法内,不对外暴露
如果存在多个引用,使用可变类型就非常不安全

快照图
快照图用于描述程序运行时的内部状态。使用快照图便于程序员之间的交流、刻画各类变量随时间变化、解释设计思路。
基本类型:在箭头末端填写值。
对象类型:在箭头末端画一个椭圆,在其中写上对象的类名,更详细的话再包含内部属性。可变类型对象的椭圆使用单线,而不可变类型的椭圆使用双线。

引用:可变引用使用单线,不可变引用使用双线。

迭代器
迭代器是一个可变数据类型,它拥有两个方法:mutator 方法 next() 返回容器的下一个数据;observer 方法 hasNext() 返回容器是否还有下一个数据。

基本类型及其封装对象类型都是不可变的,高精度数 BigInteger 和 BigDecimal 也是不可变的。
不要使用 Date,因为它是可变类型。相反,使用 java.time 包下的类,如 LocalDateTime。
Java 中的常用容器如 ArrayList、HashMap 等都是 mutable 的,如果希望容器不可变,可使用包装器 Collections.unmodifiableList 等获取一个不可变的包装对象——当调用 mutator 方法时会抛出一个异常。
然而,这种“不可变”是在运行阶段获得的,编译阶段无法据此进行静态检查。同时,wrapper 仅仅保证了无法通过 wrapper 引用修改对象,但如果通过被包装对象的引用修改了对象的值,wrapper 引用指向的对象仍然会被修改。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值