【软件构造】总复习

第一、三讲

多维度

  • 构建-时刻-代码:源码,AST,类 ,接口,属性...
  • 构建-时刻-组件:包,库,测试用例
  • 构建-时段-代码:代码变化
  • 构建-时段-组件:版本,SCI
  • 运行-时刻-代码:代码快照,内存信息转储
  • 运行-时刻-组件:部署结构...
  • 运行-时段-代码:时序图,栈跟踪,多线程...
  • 运行-时段-组件:时序图,系统层面的事件日志...

内外质量指标

  • 正确性
  • 健壮性
  • 拓展性
  • 高性能
  • 及时性

  • 可读性
  • 易理解
  • 成本低
  • 尺寸合适

五个关键质量

cFree

  • free from bug
  • cheap to develop
  • ready for change
  • easy to understand
  • effective to run

软件构造各个阶段

  • 构建
  • 编码
  • 重构
  • 调试
  • 测试
  • 性能分析
  • 代码评审

SCM和VCS

  • SCI:软件配置项
  • baseline:基线
  • SCDB:软件配置数据库
  • VCS的三种模式
    • 只保存在本地
    • 只保存在远程仓库
    • 远程和本地仓库一起用(Git)

Git的结构,基本工作原理,基本指令

  • Git分为三个区域:工作区、暂存区、git仓库
  • Git的SCI分为三种状态:已修改、已暂存、已提交
  • Git的SCI是文件
git commit -m "";
git branch
git push
git clone 
git checkout -b name
git merge (保留在原分支)

第四-八讲

基本数据类型、对象数据类型

基本数据类型是int、float等类型,不可变,内存在栈里
对象数据类型(引用数据类型)储存的其实是对象的地址,是否可变取决于其方法,有ID,从堆中分配内存

静态/动态类型检查

静态关注类型,动态关注值

可变/不可变类型

类:值类型都不可变,引用类型是否可变取决于其方法是否有Mutator
final:类不可继承,方法不可重写,成员不可再赋值

防御式拷贝

输入、输出都要按照需求进行一次拷贝,防止外部进行修改

方法规约

方法规约是更详细的函数签名。包含含函数名的那一行代码以及上方的注释;下方花括号中的内容叫实现体。
方法规约的注释部分应该包含:

  • 方法描述
  • 前置条件(输入)
  • 后置条件(输出)
  • 意外行为(抛出)

如果客户端违反了前置条件,方法可以给出任意形式的反馈:抛出或者任意输出

规约的强度:前置条件越弱,后置条件越强,则规约强度越强(客户端用的越舒服则规约越强)

欠定和非确定的规约。
欠(Under-deterministic):规约有问题,输出很可能是确定的
非(Nondeterministic):规约没问题,输出不确定

规约分类:
  • 操作式规约
  • 声明式规约

什么是优质规约

  • 功能单一(内聚)
  • 没有歧义
  • 不能太弱也不能太强
  • 使用抽象类型
  • 综合检查数据合法性的代价,选择是否将precondition放入函数内

行为等价性

在客户端使用看来没有区别就是具有行为等价性
or 适用于相同的规约

ADT的四种操作

  • Mutator 变化器
  • Constructor 构造器
  • Producer 生产器
  • Observer 观察器

其中某类(T)构造器和生产器得到的都是T对象,如果得到的不是T对象,则属于观察器或变化器
构造器和生产器的主要区别是,构造器的参数一定不含T对象,而生产器一定含。

四种操作的测试策略

  • 构造器、生产器、修改器:通过观察器函数和输入的构成参数比较
  • 观察器:通过另外三种产生或改变对象,进行观察验证

表示独立性 表示泄露

表示:用来储存和操作数据的数据结构与算法

Rep指ADT的内部表示

表示独立性是指:ADT内部的“表示(实现)”是独立的。ADT内部具体如何实现与使用ADT的客户端没有任何关系,没有任何影响。除非规约明确规定,外部不能够修改ADT的具体表示

表示泄露是指Rep暴露给ADT外。这不但破坏不变性,还破坏了独立性(不应与外部有关,外部也不需要)

不变量 表示不变量

  • 不变量:自从对象被创建之后就不能被修改的内容,需要在checkRep中验证。为了保证ADT的正确性
    保护方式:
  1. private
  2. final
  3. 防御性拷贝

表示不变量RI
是指ADT内部表示完全合法的所有条件
如:

RI:name.lengh>0&&name.length<10

表示空间和抽象空间

  • 表示空间指具体的值
  • 抽象空间可以理解为集合的表示。当然,一个集合可能涵盖多个具体值,即与表示空间一对多。
RI用来描述表示空间的范围 而AF则用抽象的集合涵盖在表示空间具体的值,再将抽象的集合对应到具体的意义

如:AF(author,password)=用户名为author,密码为password的twitter账号

注释形式的AF、RI、Rep exposure

  • Rep exposure:泄露了哪些Rep。一般来说不能泄露,这里主要是一个“没有泄露”的声明,让使用者放心。但在一些特殊情况不得不泄露,也要在这里表述
//RI: 
//author是非空字符串,其中的字符只能是数字、下划线、英文字母
//author.length<250
//AF: 
//AF(author,password)=用户名为author,密码为password的twitter账号
//Rep exposure:
//所有的成员变量都是私有的
//author和password都是String类型,是不可变的
//成员变量timestamp的类型是XXX,是可变类型,但是使用final修饰,且其getter返回的是对象的防御性拷贝

override和overload

  • override:重写-子类同名同参(数量和类型)方法,需要用@Override标签标注。其中输出类型可以是旧类型的子类,输入类型可以是旧类型的父类(这是LSP的要求,但Java并不能实现后者)
  • overload:重载-同类同名不同参方法

几种多态

特殊多态:方法重写
参数化多态:泛型
子类型多态:继承、组合、聚合

泛型

public <E> void Test(E waitWhat)
{//...}
public <E extends String> void Test2(E waitWhat)
{//...}
List<?>  list;
List<? super String> list2;
//......
  • 用<E>表示泛型,其中的E可以在后文使用。这里是声明处,只有这里可以出现从未出现的泛型符号“E”。此处不能用?

  • 用<?>表示泛型应该出现在对于泛型方法/类的引用时,表示泛型的类还不确定,用?所代表的一个范围表示。
    ? extends String
    ? super String

  • 不能创建泛型的数组

  • 子类型的规约不能弱化超类的规约

动态绑定

变量类型,静态看声明(左),动态看对象(右,由创建时new右侧内容和后续的强转决定)

等价性

  • ==:对于值类型是比较值,对于引用类型是比较两个对象的地址是否相等。
  • equals:编写方法决定。默认是比较两个对象的地址,即==;重写时,应当注意保持等价性(自反性、传递性、对称性)
    equals方法的第一部分基本都需要null判断和instanceof判断,随后才是具体的数据比较

hashCode()

  • 在容器中,决定储存位置
  • 进行contains方法比较时,只有两个对象的equals和hashCode都为true才证明两个对象是“同一”的。所以如果要重写equals,那么也必须同时重写hashCode
  • 两个储存数据不同的对象可以有相同的hashCode。只是这应当尽量避免,因为这是哈希冲突,会降低容器存取这类对象的效率

不可变对象的引用等价性、对象等价性

  • 引用等价性即是否有相同的地址,真正的“同一”
  • 对象等价性指是否有相同的数据。具体关注哪些数据,要在equals的重写里写明

可变对象的观察等价性、行为等价性

观察等价性:observer方法得到的结果是否都相同
行为等价性:调用对象任何方法的结果是否相同
一般来说采用观察等价性

注意-所有类的父类Object的默认equals方法是比较对象的地址。其中有一些子类依照观察等价性重写了equals,比如

  • 各种容器类(List等,比较内部元素)
  • 各种包装类(Integer等,比较被包装额值)
  • String(比较储存的字符串)

但也有一些比较特殊的,比如StringBuilder,StringBuffer并没有重写equals方法,比较的还是地址

第九讲

程序复用

复用分级

  • 源码复用
  • 模块复用:类/接口/抽象类
  • 库复用:API/包
  • 系统复用:框架

优良复用特点

  • 通用性
    • 和标准兼容
    • 灵活可变
    • 泛型、参数化
  • 好修改
    • 可拓展
    • 变化的局部性
  • 易使用
    • 小、简单
    • 稳定
    • 丰富的文档和帮助
    • 模块化

复用方法

  • 继承:需要使用的方法占复用类的多数时使用,创建新类
  • 委派:需要使用的方法数较少时使用,不需要创建新类,只需要复用类的对象

对于白盒的复用件,可以使用继承;对于黑盒的复用件,往往使用委托
同时应用委派和继承,实现灵活的复用

协变、逆变、不变

泛型容器中,取出的元素只能作为限定类范围的最高父类,接受时,只能接收限定范围内最底层的子类。这一特性是由继承的树结构决定的,因为从父类伸出的范围外的继承分支不受控制
协变当我们要找“A类以及A的所有子类”即<? extends A>时,最高父类是A,而最底层子类不能确定(随时可能有新类继承),所以容器<? extends A>只能给出A类对象,不能接收对象
逆变当我们要找“A类以及A的所有父类”即<? super A>时,最高父类是所有类的父类Object,而最底层的子类是确定的A,所以容器<? super A>只能给出Object类对象,只能接收A对象

CRP( Composite Reuse Principle)原则

组合优于继承。多用委派,少用继承。
委派发生在对象层面,继承发生在类层面

  • 依赖(Dependency):临时委托-在方法中传入被委托对象,但不在对象内保存
  • 关联(Association-has):永久委托-用成员变量储存被委托对象,可以反复使用
  • 组合(Composition-part of):更强的关联,不易变化。与被委托对象同生同死,不可修改
  • 聚合(Aggregation-has):更弱的关联,外部可以调用方法来更换对象内储存的被委托对象

白盒/黑盒框架的原理与实现

白盒框架的使用以继承为主。使用者需要理解超类的实现。
黑盒框架主要使用委派/组合,开发者只需要了解接口,不用关注具体类的具体实现

十-十一讲

可维护性指标

  • 可维护性:方便为了各种原因修改
  • 可拓展性
  • 灵活性:灵活相应需求
  • 可适应性:适应不同用户的操作环境
  • 可管理性
  • 支持性:部署后的运行效率

SOLID法则

  • SRP:单一责任
  • OCP:开闭原则-尽可能拓展而不修改
  • LSP:里氏转换-子类总能代替父类发挥与父类相同的作用
  • ISP:接口聚合-接口应当小而简单,只提供必须的
  • DIP:依赖转置-抽象模块不应依赖具体模块,应当为具体模块再包装一层

设计模式

设计模式分为三类

  • 构建型模式
  • 结构型模式
  • 行为型模式

工厂模式

使用工厂类的工厂方法创建对象,而不是直接通过对象的构造方法。用户只需与接口打交道,不用考虑具体类;添加新的子类时,工厂模式能更好的帮助我们遵守OCP

适配器模式

将某个类/接口转换为客户端期望的接口。“套一个特定接口的壳”,将预期接口和实际接口的输入输出做适配转换

装饰器模式

装饰器模式本质是将一/几个对一类对象可以从外部进行的操作(方法),抽离出来做成一个类。这个类的成员包含被操作的类,就像被操作类的外包装。这个类就是装饰器类,其中的方法就是装饰,就是赋予给被装饰类的新的行为。

策略模式

策略模式是让一个可能会有多种实现,且可能还会拓展新实现方式的方法,能够再不破坏OCP的前提下进行拓展。具体讲就是将一个方法中可能改变的部分抽离出来封装成类,使用时根据需要填入对应算法的类的对象。
其效果类似易于拓展的分支语句

访问者模式

访问者模式和装饰器模式的作用有一些相似之处,都是给一类对象提供一些对于这类对象可以从外部进行的操作。其中最大的区别是装饰器模式可以再更长时段里保持提供的新方法,而访问者模式只是传入访问者对象进行暂时委托。这一区别区分了两种模式的应用场景

模板模式

一些类做事的步骤一样,只是方法的具体实现不同。模板模式作为这些类的父类,提供所有的这些方法;这些方法可能是抽象方法,也可能是提供了默认实现的方法,要重写哪些取决于继承的子类;而在按照这个通用步骤使用这些类时,只需要把他们都看作模板类。
接口本身也是一种模板,模板模式的作用和接口非常相似,只是可以提供的共性内容比接口更多。

迭代器模式

不管元素被储存在什么容器里,可以用迭代器按指定的次序输出这些元素。这一模式有两个关键的接口。

  • Iterable接口:实现该接口的集合对象是可迭代遍历的
  • Iterator接口:迭代器(hasNext,next,remove)
posted @ 2023-05-26 23:47  NoSLoofah  阅读(26)  评论(0)    收藏  举报