面向可复用性和可维护性、健壮性与正确性的软件构造

一、面向复用的软件构造技术

Programing for/with reuse

programming for reuse面向复用编程:开发出可复用的软件。
programming with reuse基于复用编程:利用已有的可复用软件搭建应用系统。

LSP

Liskov Substitution Principle:(在编译时静态检查)

  1. 子类型可以增加方法,但不可删。
  2. 子类型需要实现抽象类型中的所有未实现方法。
  3. 子类型中重写的方法必须有相同或子类型的返回值或者符合co-variance的参数,即更弱的前置条件。
  4. 子类型中重写的方法必须使用同样类型的参数或者符合contra-variance的参数,即更强的后置条件。
  5. 子类型中重写的方法不能抛出额外的异常,只能更详细或不抛出异常。

协变、反协变

协变Covariance: 子类方法比父类有更具体的规约、返回值、异常。
反协变Contravariance: 子类方法比父类有更抽象的参数值。
注:java无法实现反协变,因为如果@override重写一个父类的方法时改变了参数类型,编译器会认为这是重载而报错,而删掉@override标识,编译器就会把这个当作重载来进行,而不是对父类方法的重写。

数组的子类型化

Java 中的数组是协变的,也就是说,数组T[]中任意一个元素可以是T的子类。
用具体代码作例子:

Number[] numbers = new Number[2];
numbers[0] = new Integer(10);
numbers[1] = new Double(3.14);

泛型的子类型化

简而言之就是由于类型擦除,List不是List的子类,需要用到通配符实现期望的操作,详见笔者另一篇博客中的参数多态:https://blog.csdn.net/Mechanic_PK/article/details/125256079

Delegation

委托:一个对象请求使用另一个对象的功能,是复用的一种常见形式。
继承满足子类只需要复用父类中的一小部分方法时,更建议采用委托实现,可以避免大量无用的方法。委托是发生在object层面,继承是发生在class层面。
分类:

  1. 首先根据是否在field中有字段保存委托变量,没有则是dependency,是临时的委托关系(use);有则是association,是永久性委托关系(has)。
  2. 对于association,根据委托关系是由什么建立,由new初始化构造,则是composition,很强(不可修改);由客户端调用构造方法,从外界传入并保存到field,是aggregation,较弱(可修改)。

Comparator和Comparable

均是为了实现比较大小的功能,区别在于:
前者是使用继承委托,实现了Comparator接口并重写了compare方法,将比较过程委托给了自己实现的接口内去完成;
后者不使用继承委托,实现了Comparable接口并重写了compareTo方法,将比较过程代码放在ADT内部。

CRP原则

Composite Reuse Principle:

  1. 类应该实现通过组合的复用,而不是通过继承。
  2. 最好组合对象的功能而不是拓展对象的功能。

接口的组合

使用接口定义系统必须对外展示的不同侧面的行为,接口之间通过extends实现行为的扩展(接口组合),类implements组合接口,从而规避了复杂的继承关系。

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

白盒:代码可见,主要通过继承重写实现拓展的功能。
黑盒:接口可见,主要通过实现特定接口、委托来实现框架拓展。

二、面向可维护性的构造技术

可维护性的常见度量指标

圈复杂度Cyclomatic Complexity、代码行数LOC、可维护性指数Maintainability Index (MI)、继承的层次数Depth of Inheritance、类之间的耦合度Class Coupling、单元测试的覆盖度Unit test coverage。

聚合度与耦合度

聚合度:是一个模块的功能或职责之间的关联程度的度量。如果模块的所有元素都朝着相同的目标工作,那么模块就具有高度的内聚性。
耦合度:是模块之间依赖关系的度量。如果一个模块中的更改可能需要另一个模块中的更改,则两个模块之间存在依赖关系,依赖关系越强,模块就具有高度的耦合度。

SOLID

  1. The Single Responsibility Principle 单一责任原则:不应该有多于1个原因让你的ADT发生变化,否则就拆分,“责任”即变化的原因。
  2. The Open-Closed Principle 开放-封闭原则:对扩展性的开放——模块的行为应是可扩展的,从而该模块可表现出新的行为以满足需求的变化;对修改的封闭——但模块自身的代码是不应被修改的,扩展模块行为的一般途径是修改模块的内部实现,如果一个模块不能被修改,那么它通常被认为是具有固定的行为。
  3. The Liskov Substitution Principle Liskov替换原则:详见上文LSP。
  4. The Interface Segregation Principle 接口聚合原则:不能强迫客户端依赖于它们不需要的接口,只提供必需的接口,接口务必细分,避免混乱。
  5. The Dependency Inversion Principle 依赖转置原则:一个类依赖另一个类时,尽量用接口而不是具体实现,而抽象不可依赖具体。

正则表达式

  1. 操作符:*表示重复0次或多次,+表示重复至少一次,?表示重复0次或1次,ab表示a连接b,a|b表示a或b,[]表示中括号内的字符挑一个出来,[^]表示除了中括号内的字符的其他字符挑一个出来。
  2. 一些简写:.表示任意一个字符,\d表示任意一个阿拉伯数字,\s表示任意一个空白符,\w表示任意一个阿拉伯数字、大小写英文字母或_(下划线)。
  3. 以下特殊符号使用其为字符匹配时,要用\转义:<([{^-=$!|]})?*+.>

三、面向可复用性和可维护性的设计模式

factory method(工厂方法模式)

  1. 使用情景:也被称为虚拟构造器,即目的是充当实例话对象时的构造方法,当client不知道要创建哪个具体类的实例,或者不想在client代码中指明要具体创建的实例时。
  2. 实现方法:定义一个用于创建对象的接口,让其子类来决定实例化哪一个类,从而使一个类的实例化延迟到其子类。契合了OCP原则。

adapter(适配器模式)

  1. 使用情景:client要用的方法,和已有的方法类似但是不完全一样,需要开发者让其“适配”。
  2. 实现方法:通过增加一个接口,在该接口的实现中,把任务委托给能实现的具体类中的方法去完成,client面向接口编程,从而隐藏了具体子类。

decorator(装饰器模式)

  1. 使用情景:为对象增加不同侧面的特性。
  2. 实现方法:对每一个特性构造子类,把通性通过委派机制从接口的左支实现(一些基本方法)增加到这些具有特性的子类对象上。使用具体特性只需要想穿衣服似的,不断往上套构造方法。

strategy(策略模式)

  1. 使用情景:有多种不同的算法来实现同一个任务,但需要client根据需要动态切换算法,而不是写死在代码里。
  2. 实现方法:为不同的实现算法构造抽象接口,运行时通过委托给接口,动态传入client倾向的算法类实例。

template method(模板模式)

  1. 使用情景:做事情的步骤一样,但具体方法不同。
  2. 实现方法:共性的步骤在抽象类内公共实现,差异化的步骤在各个子类中实现。使用继承和重写实现模板模式。

iterator/iterable(迭代器模式)

  1. 使用情景:客户端希望遍历被放入容器/集合类的一组ADT对象,无需关心容器的具体类型,提供的遍历方式相同。
  2. 实现方法:委托给Iterator完成。

visitor(访问者模式)

  1. 使用情景:对特定类型的object的特定操作(visit),在运行时将二者动态绑定到一起,该操作可以灵活更改,无需更改被visit的类。
  2. 实现方法:为ADT预留一个将来可扩展功能的“接入点”,外部实现的功能代码可以在不改变ADT本身的情况下通过delegation接入ADT。

设计模式总结

主要采用了继承思路:适配器模式(但其实最关键那步函数调用是委托)、模板模式。
主要采用了委托思路:工厂方法模式、策略模式、访问者模式、迭代器模式。

四、面向正确性与健壮性的软件构造

健壮性和正确性

健壮性:系统在不正常输入或不正常外部环境下仍能够表现正常的程度。
面向健壮性的编程:处理未期望的行为和错误终止;即使终止执行,也要准确/无歧义的向用户展示全面的错误信息;错误信息有助于进行debug。

正确性:程序按照spec加以执行的能力,是最重要的质量指标。

健壮性则倾向于容错(fault-tolerance);正确性倾向于直接报错(error)。
对外的接口,倾向于健壮;对内的实现,倾向于正确。

Throwable

Java中所有异常类都最终继承自Throwable类。
Throwable有两个子类:Error和Exception。
整体概览图:
java Throwable

Checked异常、Unchecked异常

二者差异checkedunchecked
本质区分编译器可以帮助检查只有到运行时才会出现
开发者角度不是开发者的责任是开发者代码不够完善导致
处理方法try-catch-finally-throw修改完善代码,从根本上解决
导致结果编译不过运行失败
客户端可补救无能为力
处理后的表现代码冗长复杂清晰简单
优势代码健壮性更好代码简洁性更高

Checked异常的处理机制

  1. 声明:在方法规约的后置条件,用throws声明本方法会抛出什么异常。
  2. 抛出:用throw( new)抛出异常。
  3. 捕获:try执行时,catch等待随时捕获被抛出的异常。
  4. 处理:catch内执行代码。
  5. 清理现场、释放资源等:finally内执行代码(不管异常有没有都执行)。

自定义异常类

如果JDK提供的exception类无法充分描述你的程序发生的错误,可以创建自己的异常类。要定义一个checked异常类,需要继承自java.lang.exception的一个子类如IOException;要定义一个unchecked异常类,需要继承自java.lang.exception.RuntimeException。可能引发此异常的方法规约中必须声明。

断言的作用、应用场合

断言:在开发阶段的代码中嵌入,检验某些“假设”是否成立。若成立,表明程序运行正常,否则表明存在错误。断言即是对代码中程序员所做假设的文档化,也不会影响运行时性能(在实际使用时,assertion都会被disabled)。
断言主要用于开发阶段尽快避免错误,避免引入bug并帮助发现bug。
注:Java缺省关闭断言,要记得打开(-ea)。

防御式编程的基本思路

  1. 对来自外部的数据源要仔细检查,例如:文件、网络数据、用户输入等;
  2. 对每个函数的输入参数合法性要做仔细检查,并决定如何处理非法输入;
  3. public方法接受到的外部数据时,需要假设这些参数是不安全或不合法的,需要检查这些参数的合法性再传给private方法。

五、软件测试与测试优先的编程

黑盒测试用例的设计

等价类划分:

  1. 基于的假设:相似的输入,会展示相似的行为,故从每个等价类中抽取一个测试即可。
  2. 划分依据:每个输入数据需要满足的条件。

边界值分析(等价类划分的补充)

  1. 基于的假设:大量的错误发生在边界附近而不是中央。
  2. 实现方法:等价类划分的同时,把边界也作为等价类加入考虑。

以注释的形式撰写测试策略

写明是根据什么选择的测试用例,让其他人理解测试并能评判是否充分。
在测试类的顶部记录测试策略,在每个测试方法上面说明它的测试用例是如何被选择的,即它覆盖了分区的哪些部分。

JUnit测试用例写法

测试方法前用@Test告诉编译器。
Junit

测试覆盖度

已有的测试用例有多大程度覆盖了被测程序。
使用如EclEmma等进行检查。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值