软件构造总结

软件构造总结

写在前面

  百感交集。

第一章 软件构造基础

  第一章主要介绍了软件构造中的视图以及衡量软件的指标。
  视图我理解的是对软件构造领域中常用技术的分类,通过分为三个维度:code和component维度,build-time和run-time维度,moment和period维度将常用的技术划入8个不同的领域下。Snapshot使用方法点我

view

  指标分为外部指标和内部指标,外部指标直接影响到用户无疑是非常重要的,内部指标影响软件本身和开发者。外部指标取决于内部指标。外部指标有:正确性(分层,防御式,形式化验证),健壮性,可扩展性,可复用性,兼容性,性能,可移植性,易用性,功能性,及时性。内部指标:LOC,圈复杂度,耦合度,内聚度。
  最后是5个重要的性能:易理解性(第四章),可复用性(第五章),ready for change(第6章),正确性和健壮性(第7章),高效性(第8章)。

第二章 软件构造过程

  第二章主要是软件构造的过程描述,SCM(git)和一些代码评审的方式。
  SDLC,软件生命周期。所有软件开发模型分为两种基本模型,线性开发和迭代开发。传统的开发模型主要有瀑布,增量,V字,原型,螺旋模型。敏捷开发是区别传统开发的一种新的软件构造过程的模型。Ailge=增量+迭代。每次迭代处理一个增量。
  然后是SCM(软件配置管理),其实就是记录软件构造的构成中的每个版本。SCM的记录变化的单位是SCI(软件配置项),例如文件。基线是稳定时刻的软件版本。CMDB,仓库,工作拷贝,Change(diff),Head是SCM中常用的术语。SCM使用VCS来具体控制版本之间的关系。VCS又分为Local VCS,集中式VCS,分布式VCS。
  熟悉git常用命令。注意git中SCI是文件,文件中只要有一行发生了变化,git会重新拷贝整个文件。并且在commit graph中,箭头指的方向是从后来的commit指向之前的commit的。

git

  主要的代码评审方式有走查,结对编程,正式的代码评审会议和自动化评审。同时又分为静态评审和动态评审。CI是持续集成工具。

第三章 ADT+OOP

3.1 数据类型和校验

  首先是区分基本数据类型和对象数据类型。注意区分基本类型的包装类和基本类型。

data type

  存在静态语言和动态语言,类型检查的时间不一样(编译阶段和运行阶段,即静态检查和动态检查)。然后引入了immutable和mutable(安全性,是否存在表示暴露,修改传入的值或者成员变量的值,别名问题)。mutable可能引发的问题有很多,例如遍历集合的时候删除一个元素,如果不是iterator的remove方法就会引起各种问题。最后集合的unmodifiable系列方法返回的集合如果被调用mutator会直接抛出异常

3.2 SPEC(设计规约)

  Java会通过编译来完成静态类型检查。代码本身蕴含这设计决策,是给编译器读的,但是远远不够,注释形式的“设计决策”,给自己和别人读。SPEC(规约)可以隔离变化,无需通知客户端,扮演“防火墙”的角色。写SPEC的原则是只讲“能做什么”,不讲“怎么实现”。行为等价性必须通过SPEC才能判断!如果SPEC的前置条件被满足,那么后置条件必须被满足,如果前置条件没有被满足则理论上程序可以发生任何行为。
  比较两个SPEC的强弱。关键应当是是否能让一个SPEC替换另一个,如果能替换,则替换的那个SPEC更强一些。更强的SPEC的前置条件更弱,后置条件更强(判断条件强弱可以用集合来判断,即通过是否存在一个例子使得在一个条件中满足而另一个条件中不满足,越小的集合越强)。
  SPEC分为确定的规约,欠定的规约(同一个输入,多个输出)和非确定的规约(多次运行得到不同的输出)。也可以分为操作式的规约(例如伪代码)和声明式的规约(不描述内部,只描述初-终状态)。声明式的规约更有价值。SPEC描述的功能应单一,简单,易理解。

3.3 ADT

  抽象数据类型和表示独立性的概念。ADT是由操作定义的,和内部如何实现无关。ADT分为mutable和immutable,方法分为四种:Creators,Producers,Observers和mutators。静态的构造器叫工厂方法。mutator通常都是void,但是有能返回对象的mutator(例如BufferedReader的readLine)

method

  设计ADT的三点原则:1.设计简洁一致的操作,2.要足以支持client对数据所作的所有操作需要,且满足client需要的难度要低,3.要么抽象,要么具体,不要混合————要么针对抽象设计,要么针对具体应用设计。
  表示独立性的概念(成员变量满足条件,不被外界改变)。防御式拷贝注意安全性和新能之间的平衡。AF的性质(可能不是满射也不是单射)。checkRep的使用。有益的变动。

AF,RI

  最后采用三个标准来判断是否保持了不变量(创立,保存和是否防止了表示暴露)。最后注意ADT中的不变量其实是取代了复杂的pre-condition,相当于将复杂的pre-condition封装到了ADT内部。

3.4 OOP

  主要技术有泛型,继承,多态,动态分配/绑定。然后是类,接口,继承,实现等常用概念。
  这里!一次性分清楚重载和重写!!!重载是只编码方法名和参数列表,而且参数列表不是协变的,重写看能够取代原有的方法,即符合被重写方法的SPEC。

override
overload

  多态分为特殊多态(功能重载,就是一个方法很多重载,又称静态多态,重写是动态多态),参数多态(即泛型,类型擦出,具体见实验),子类型多态(包含多态)。子类型的规约(SPEC)不能弱化超类型的规约(SPEC)。
  最后是Object的重要方法,equals,hashCode,toString。immutable的优点:简单,天生的线程安全,可以被自由地共享,不需要防御式拷贝,非常优秀的构造块。

3.5 ADT和OOP中的等价性

  利用AF定义的观察等价性。
  针对immutable有引用等价性和对象等价性。注意equals是否正确重写(传入参数为Object,若为其他的类型那equals就是重载了)。等价的对象必须有相同的hashCode。不相等的对象也可以有相同的hashCode,但性能会变差。
  mutable的等价性,分为观察等价性和行为等价性,观察等价性就是当前两个对象的所有状态(Observers)是否相同。行为等价性是所有行为都一样(多数时候是直接比较运行时内存地址)。Date的equals,List的equals都是观察等价性。StringBuilder的equals直接继承了Object的方法,是行为等价性。最后注意深克隆和浅克隆。

第五章 面向可复用性的构造

5.1 可复用性的度量,形态和外部表现

  复用的两种不同的视角:面向复用编程和基于复用编程。复用的优点在于降低了开发的成本和时间,并且经过了充分的测试,可靠,稳定,最后复用能保持标准化,在不同的应用中保持一致。复用开发的难度在于开发成本高于一般的软件,要有足够的适应性。复用的测量:搜索获取可复用代码的花费,适配扩展的花费,实例化的花费,与软件其他部分的互通的难度。贴一下理想的复用性高的代码应该有的特点。

reuse

  复用分四个层次:代码层面的复用(方法,语句等),模型层面的复用(类和接口等),库层面的复用(Library,API,包,.jar等),架构层面的复用(framework等)。同时复用的方式又有两种:白盒复用和黑盒复用。白盒复用源代码可见,可修改和扩展,但是修改增加了软件的复杂性并且需要对其内部有充分的了解。黑盒复用的源代码不可见,不能修改,只能通过API来使用,无法修改代码,但是简单,清晰。白盒框架多利用继承和重写,黑盒框架多利用委托。

framework

  类的复用的两种方式:继承和委托。了解控制反转。外部观察到的可复用性有:类型可变(泛型),实现可变(一个SPEC,多种实现),功能分组(Routing Group,提供完备的细粒度操作),表示独立(信息隐藏),抽取共性行为(将共同的行为抽象出来,形成可复用实体)。

5.2 面向复用的软件构造技术

  多态中的子类型多态是一种重要的复用技术。LSP原则(子类型无条件取代父类型)。子类型要有更强的不变量,更弱的前置条件和更强的后置条件。

LSP

  注意上图中Java不支持参数的逆变(Java中参数的逆变都是重载)。协变和逆变的定义,通俗一点说,协变是兼容子类,逆变是兼容父类。数组是协变的!泛型不是协变的,但是有通配符来帮助完成协变或者逆变。
  委托是复用的一种常见形式,通常有实现comparable接口来并委托到Collectinos的sort中来排序。委托分显示委托和隐式委托。如果子类只需要父类中的一小部分方法可以不需要继承而是通过委托实现。委托发生在object层面,继承发生在class层面。委托根据强弱又分为:临时的委托,永久性的委托,composition委托,聚合委托。composition和聚合的区别在于:聚合真正的对象在外面,传入ADT,一个ADT消失了另一个还在,composition中拥有者消失后被包含对象也不存在。

5.3 面向复用的设计模式

  分为三种模式:创建型模式,结构型模式,行为类模式。
  结构型模式有:Adapter(适配器)模式,Decorator(装饰)模式,Facade(外观)模式。

structural

  行为类模式有:Stratege(策略)模式,Template(模板)模式,Iterator(迭代)模式。

behavioral

第六章 面向可维护性的构造

6.1 可维护性的度量与构造原则

  软件的大部分成本来自维护阶段。软件维护不仅仅是运维工程师的工作,在软件设计和开发阶段就开始了,在设计和开发阶段就要考虑将来的可维护性,使得设计方案“easy to change”。
  可维护性的别名(可扩展性,灵活性,可适应性,可管理性,支持性)。Code View中经常问的关于可维护性的问题。圈复杂度和LOC的定义。可维护性指数(MI)由HV,CC,LOC,COM组成。其他的常用的可维护性的度量有继承的层次数,类之间的耦合度以及单元测试的覆盖度。模块化编程注重高内聚,低耦合,分离关注点,信息隐藏。
  评判模型的五个标准。评判设计的五个标准。设计类的五个标准:单一职责原则,开放-封闭原则,Liskov替换原则,依赖转置原则,接口聚合原则。GRASP,通用职责分配软件模式。

class design

6.2 面向可维护性的设计模式

  和前面一样分为创建型模式,结构型模式,行为类模式。
  创建型模式有:工厂方法模式,抽象工厂模式,Builder模式。
  结构型模式有:Bridge(桥接)模式,Proxy(代理)模式,Composite(组合)模式。
  行为类模式有:Observer(观察者)模式,Visitor模式,Mediator模式,*Command模式,*职责链模式。

6.3 面向可维护性的构造技术

  使用有限状态机来定义程序的行为,使用状态来控制程序的执行。
  基于自动机的编程。状态设计模式(注意下图中singleton模式的使用)。

singleton

  表驱动编程的核心思想:将代码中复杂的if-else语句和switch-case语句从代码中分离出来,通过“查表”的方式完成,从而提高可维护性。
  正则表达式的语法。解析正则语法使用的是正则语法树。

regular

6.4 设计模式的共性和差异

  这一节实际上是帮助总结的,在这一节可以大致看到所有常用的设计模式。提取了设计模式的三个共性样式,并将常用的设计模式划入这三个共性样式中。
  共性样式1下有:Adapter模式,Proxy模式,Template模式。
  共性样式2下有:Decorator模式,Composite模式。
  共性样式3下有:Strategy模式,Iterator模式,工厂方法模式,抽象工厂模式,Builder模式,Bridge模式,Observer模式,State模式,Memento模式

pattern

第七章 面向健壮性的构造

7.1 健壮性和正确性

  健壮性:系统在不正常输入或不正常外部环境下仍能够表现正常的程度。
  正确性:程序按照SPEC加以执行的能力,是最重要的质量指标。
  面向健壮性编程:处理未期望的行为和错误终止,即使终止执行,也要准确/无歧义的向用户展示全部错误信息。错误信息有助于debug。
  对自己的代码要保守,对用户的行为要开放。

correct

  几种错误的区别和因果关系。

error

  MTBF(平均失效间隔时间),残余缺陷率分别从外部和内部测量了健壮性和正确性。

7.2 错误与异常处理

  首先是Throwable的分类

Throwable

  内部错误的发生通常无能为力,只能让程序优雅地结束。异常可以捕获,处理。异常:程序执行中的非正常事件,程序无法再按预想的流程执行。是除return外的第二种退出途径。若找不到异常处理程序则整个系统完全退出。
  异常的分类,异常可分为运行时异常和其他异常,checked异常和unchecked异常。unchecked异常是从RuntimeException派生出的子类型,checked异常是从Exception派生出的子类型。checked异常必须指定处理方式,unchecked可以不指定。
  异常的处理方式:try,catch,finally,throws,throw。如果客户端可以通过其他的方法恢复异常,那么采用checked异常,如果客户端对出现的这种异常无能为力,那么采用unchecked异常。异常出现时要做一些试图恢复它的动作而不要仅仅的打印它的信息。另一个角度是尽量使用unchecked异常来处理编程错误。如果客户端对某种异常无能为力,可以把他转变为一个unchecked异常(rethrow)。如果错误可以预料但是无法预防,但可以有手段从中恢复,此时使用checked异常。注意unchecked异常不应该出现在SPEC,@throws或者方法的声明的throws中。

exception difference

  如果父类型中的方法没有抛出异常,那么子类型的方法必须捕获所有的checked异常。子类型的方法不能比父类型抛出更多的异常!不管程序是否碰到异常,finally都会被执行。TWR语句用于打开一些需要finally来关闭的资源。Stack Trace中越上面的条目离案发现场越近。当有异常抛出的时候,如果不想恢复它,那么要毫不犹豫的将其转换为unchecked异常,而不是用一个空的catch块或者什么也不做来忽略它,以至于从表面看像是什么也没有发生一样。

7.3 断言和防御式编程

  如果无法避免bug,尝试着将bug限制在最小的范围内。fail fast。断言避免bug扩散。检查前置条件是防御式编程的一种典型形式。使用断言的场景有:检查内部不变量,表示不变量,控制流不变量,方法的前置条件,方法的后置条件。断言主要用于开发阶段,避免引入和帮助发现bug,在实际运行阶段,不再使用断言,避免降低性能,使用断言的主要目的是为了在开发阶段调试程序,尽快避免错误。异常和断言:使用异常来处理你“预料到可以发生”的不正常情况,使用断言处理“绝不应该发生”的情况。Garbage in,garbage out。类的public方法接收到外部数据都应被认为是dirty的,需要处理干净再传递到private方法————隔离舱。

7.4 代码调试

  debug占用了大量的开发时间。debug是测试的后续步骤:测试发现问题,debug消除问题。而错误的定位占用了绝大部分调试的时间。
  诊断的方法有:测量(log,输出等),分治,切片,寻找差异(利用VCS等),Delta Debugging(基于差异的调试,两个测试用例之间的比较等),符号化调试(符号化执行树)
  有关于log的使用,下面为系统log的级别,系统log默认是输出到控制台,但可以通过配置handler来配置log输出信息的地方。

log

  调试的工具有:Memory dump,Stack Trace,输出调试,日志等。设定日志的格式可以通过SimpleFormatter,XMLFormatter。

7.5 软件测试与测试优先的编程

  测试:发现程序中的错误,提高程序正确性的信心。程序确认的基本方法:形式化推理,代码评审。用残留缺陷率来测量代码的正确性。好的测试的特点:能发现错误,不冗余,最佳特性,别太复杂也别太简单。测试有多种分类方法,分为:单元测试,集成测试,系统测试,或者分为:静态测试和动态测试,或者分为:白盒测试和黑盒测试。

test

  测试的特点有:软件行为在离散输入空间中差异巨大,大多数正确,少数点出错。bug的出现也往往不符合特点的概率分布。无统计规律可循。测试用例的定义:输入+执行条件+期望结果。
  提倡测试优先的编程:先写SPEC,然后利用SPEC写测试用例,然后写代码,执行测试,有问题再改,再执行测试用例,直到通过它。先写测试会节省大量的调试时间。针对软件的最小单元模型展开测试,隔离各个模块,容易定位错误和调试。

assert

  黑盒测试用于检查代码功能,不关心内部实现细节,检查程序是否符合规约,用尽可能少的测试用例,尽快运行,并尽可能大的发现程序的错误。
  基于等价类划分的测试:将被测函数的输入域划分为等价类,从等价类中导出测试用例。针对每个输入数据的需要满足的约束条件,划分等价类(对称,自反,传递)。基于的假设是:相似的输入,将会展示相似的行为,故可从每个等价类中选一个代表作为测试用例即可。

partition

  BVA,边界值分析,边界值分析方法是对等价类划分方法的补充,在等价类划分时,将边界作为等价类之一加入考虑。
  代码覆盖度:已有的测试用例有多大程度覆盖了被测程序。

cover

  测试效果:路径覆盖 > 分支覆盖 > 语句覆盖。回归测试:一旦程序被修改,重新执行之前的所有测试。一旦发现bug,要马上写一个可重现该bug的测试用例,并将其加入测试库。
  测试策略(根据什么来选择测试用例)非常重要,需要在程序中显式记录下来,目的在于,在代码评审过程中,其他人可以理解你的测试,并评判你的测试是否足够充分。

第八章 面向性能的构造技术

8.1 软件构造性能指标

  时间性能:每条指令,每个控制结构,整个程序的执行时间。空间性能:每个变量,每个复杂结构,整个程序的内存消耗。

8.2 内存性能与垃圾回收

  了解本机内存(物理内存/虚拟内存)。管理内存的基本动作:内存分配,垃圾回收。每个对象存储在内存中一段连续的空间中,包括header和fielders,如果是引用,则存储它所指向的对象内存地址。为新对象分配内存的基本操作:在内存里创建一个新对象,将其与某个引用关联起来,初始化其内部各域。
  三种对象管理的模型:静态内存模型,动态基于堆的内存模型,动态基于栈的内存模型。静态模型在将程序load进内存的时候或开始执行时,确定所有对象的分配,运行时无法改变。栈模型用于存储方法调用以及方法执行中的局部数据,后进先出,无法支持复杂的数据结构。堆模型是自由模式的内存管理,动态分配,可管理复杂的动态数据结构。使用动态分配内存的原因:某些对象延续的时间比创建它的方法所延续的时间更长(所以stack不行),递归的数据结构,长度可变的数据结构(所以静态方法和stack不行),经常使用不限定长度的数据结构。
  JVM内存管理模式:在heap上创建新对象,即使是局部变量的object,也是在堆上创建。当某个对象不再有reference指向它,删除对象释放内存。每个线程都有自己的栈,管理其局部空间,各栈之间彼此不可见。所有局部的基本数据类型都在栈上创建,多线程之间传递数据,是通过复制而非引用。如果两个线程调用同一个对象上的某个方法,它们分别保留该方法的局部变量拷贝。

JVM

  三种模式下的内存回收:静态内存分配模式下,无需进行内存回收,所有都是已确定的,在栈上进行内存空间回收,按block(某个方法)整体进行,在heap上进行内存空间回收,最复杂(无法提前预知某个object是否已经变得无用)。对象的活性:可达/不可达(从根对象)。而内存回收的首要问题:如何把可达对象与不可达对象分离开来。根对象的确定:静态区域的数据,寄存器,目前执行的栈中的数据所指向的内存对象。大致想法是:从root对象开始进行有向图的搜索,将图分为root可达部分和root不可达部分,后者将被进行内存回收。回收的指标有:执行时间,延迟时间,所占用的内存/对程序所使用内存的影响,其他指标。防御式拷贝对GC带来的影响。人工GC可能造成内存泄露或者悬空指针。
  四种GC的方法:引用计数(Reference counting),标记-清除(Mark-Sweep),标记-整理(Mark-Compact),复制(Copying)。注意四个算法的执行步骤,优点和缺点。JVM的GC将堆分为不同的区域,各区域采用不同的GC策略,以提高GC的效率。

heap

  Yong代采用copy策略,old代采用Mark-Sweep或者Mark-Compact策略。只有当某个区域不能再为对象分配内存时(满),才启动GC。Yong代使用minor GC,Old代使用full GC,当perm代(Metaspace)满了之后,无法存储更多的元数据,也启动full GC。

heap GC

  下图是配置heap参数。System.gc()可在代码中手动请求GC。

configure
command

8.3 I/O算法与性能

  注意几种IO的方式。

8.4 动态性能分析方法与工具

  分为静态分析(使用抽象的输入值)和动态分析(使用具体的输入值)。Profiling的技术有:代码注入/代码插入,采样,借助虚拟机获取程序性能数据。下列是各种动态分析性能的工具。除此之外还有JMC,MAT等等。

tools

8.5 面向性能的代码调优

  代码调优的优先级最低。不要边写程序边调优(放到最后!),代码调优不是优化性能的第一选择(在架构和ADT设计方面的性能改进余地更大)。代码调优的优化方法有:Singleton(单例)模式,Flyweight(轻量)模式,Prototype(原型)模式(注意深拷贝和浅拷贝),Object Pool(对象池)模式,规范化。

interface