目录

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

1 软件维护和演化

2 可维护性的度量

3 模块化设计和模块性准则

模块划分的五个准则

模块设计的五个原则

耦合度和聚合度

4 OO设计准则:SOLID

SRP

OCP

LSP

ISP

DIP

总结

5 语法驱动的构造

正则语法和正则表达式

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

1 创建型模式

工厂方法模式

2 结构型模式

(1)适配器模式

(2)装饰器模式

3 行为类模式

策略模式

模板模式

迭代器模式

Vistor Pattern

设计模式的共性和不同

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

什么是健壮性和正确性

如何度量健壮性和正确性

Java中的错误和异常

异常处理

什么是异常

异常的分类

Checked和Unchecked异常

使用throws声明Checked异常

如何抛出异常

创建异常类

捕获异常

重抛和链接异常

finally 语句

断言

什么是断言

使用断言

断言和异常的区别


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

1 软件维护和演化

  • 修复错误和改善性能。软件开发中最困难的工作之一,涉及其他所有环节。处理来自用户报告的故障。还需要测试所做的修改,回归测试,记录变化。
  • 除了要修复问题,在修改中不能引入新的故障。
  • 最大的问题:没有足够的文档记录和测试用例。
  • 纠错性(25%)、适应性(21%)、完善性(50%)、预防性(4%)
  • 软件演化是软件维护的重要部分:对软件进行持续的更新,软件的大部分成本来自于维护阶段。from 1 to n
  • 软件维护从设计和开发阶段就开始了,在这个阶段就要考虑将来的可维护性。设计方案“east to change”。
  • 方法:模块化、OO设计原则、OO设计模式、基于状态的构造技术、表驱动的构造技术、基于语法的构造技术。

2 可维护性的度量

  • 设计结构是否足够简单
  • 模块之间是否松散耦合
  • 模块内部是否高度聚合
  • 是否使用了非常深的继承树,是否使用了委托代替继承
  • 代码的圈/环复杂度是否太高(独立路径的数量)
  • 是否存在重复的代码
  • 可维护指数MI,根据各指标使用公式算出来的

3 模块化设计和模块性准则

高内聚,低耦合,分离关注点,信息隐藏

模块划分的五个准则

  • 可分解性:大模块分解为子模块

使模块之间的依赖关系显示化和最小化

  • 可组合性:小模块组合在一起,是模块可在不同的环境下复用
  • 可理解性
  • 可持续性:发生变化时,受影响范围最小
  • 出现异常之后的保护:出现异常后受影响范围最小

模块设计的五个原则

  • 直接映射(模块的结构与现实世界中问题领域的结构保持一致)
  • 尽可能少的接口
  • 尽可能小的接口(模块通讯,尽可能的交换少的信息)
  • 显式接口
  • 信息隐藏

耦合度和聚合度

耦合度:模块间的接口数目和每个接口的复杂度

要做到到高内聚低耦合

4 OO设计准则: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 依赖转置原则

SRP

ADT中不应该有多于一个原因让其发生变化,否则就拆分开

OCP

对拓展性的开放:模块的行为可拓展

对修改的封闭:模块自身的代码是不应被修改的

LSP

子类型必须能够替换其基类型

ISP

不能强迫客户端依赖于它们不需要的接口:只提供必需的接口

避免接口污染

避免胖接口

只提供每个客户端需要的接口

 

DIP

高层模块不应该依赖于低层模块,二者都应该依赖于抽象。抽象不应该是依赖于实现细节,实现细节应该依赖于抽象。使用接口和抽象类。

通过这种实现方式,无论具体的reader和writer怎么实现,如何改变,都不需要重写copy。

总结

抽象(abstraction):模块之间通过抽象隔离开来,将稳定部分和容易变化部分分开
– LSP:对外界看来,父类和子类是“一样”的;
– DIP:对接口编程,而不是对实现编程,通过抽象接口隔离变化;
– OCP:当需要变化时,通过扩展隐藏在接口之后的子类加以完成,而不要修改接口本身。
分离(Separation) Keep It Simple, Stupid (KISS)
– SRP:按责任将大类拆分为多个小类,每个类完成单一职责,规避变化,提高复用度;
– ISP:将接口拆分为多个小接口,规避不必要的耦合。
归纳起来:让类保持责任单一、接口稳定。

5 语法驱动的构造

连接:x ::= y z

重复:x ::= y*

选择:x ::= y | z

x是y或者空:x ::= y?

正闭包:x ::= y+

字母类:x ::= [a-c] equals to x ::= 'a' | 'b' | 'c' equals to x ::= [abc]

x ::= [^a-c] equals to x ::= 'd' | 'e' | 'f' .....

正则语法和正则表达式

正则语法:简化之后,可以表达为一个产生式,而不包含任何非终止节点

正则表达式:去除引号和空格

. any single character

\d any digit, same as [0-9]

\s any whitespace character, including space, tab, newline

\w any word character, including underscore, same as[a-zA-Z_0-9]

\., \(, \), \*, \+, ... 需要转义字符

Grammar定义语法规则(BNF格式的文本),Parser generator根据语法规则产生一个parser,用户利用parser来解析文本,看其是否符合语法定义并对其做各种处理(例如转成parse tree)。

java.util.regex

 

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

除了类本身,设计模式更强调多个类/对象之间的关系和交互过程,比接口/类复用的力度更大

包括创建型模式,结构型模式和行为类模式

1 创建型模式

工厂方法模式

也叫作虚拟构造器。

当client不知道/不确定要创建哪个具体类的实例,或者不想在client代码中指明要具体创建的实例时,用工厂方法。

定义一个用于创建对象的接口,让该接口的子类型来决定实例化哪一个类,从而使一个类的实例化延迟到其子类。

静态工厂方法: 既可以在ADT内部实现,也可以构造单独的工厂类。

相比于通过构造器(new)构建对象:

  1. 静态工厂方法可具有指定的更有意义的名称
  2. 不必在每次调用的时候都创建新的工厂对象
  3. 可以返回原返回类型的任意子类型

缺点:每增加一种产品就要增加一个新的工厂子类

遵循了OCP原则。

2 结构型模式

(1)适配器模式

将某个类/接口转换为client期望的其他形式。解决类之间接口不兼容的问题。通过增加一个接口,将已存在的子类封装起来,client面向接口编程,从而隐藏了具体子类。也叫作wrapper。

加个适配器以便于复用。

可采用继承或者委托的方法。

(2)装饰器模式

对一种数据结构进行不同的扩展,可以使用继承让每个子类实现不同的特性。但如果需要特性的任意组合,继承就不再适用,如果使用继承就会导致组合爆炸,有大量的代码重复。

问题就在于如何为对象增加不同侧面的特性。可以对每一个特性创造子类,然后通过委派机制增加到对象上。以递归的方式实现。

就像套娃一样。

3 行为类模式

策略模式

有多种不同的算法来实现同一个任务,但需要client根据需要动态切换算法,而不是写死在代码里。

例如排序有冒泡排序,快排等等。

可以为不同的实现算法构造抽象接口,利用委托,运行时动态传入client倾向的算法类实例。

优点:容易拓展新的算法实现;把算法和客户上下文分隔开。

使用委派来实现

模板模式

做事情的步骤一样,但具体方法不同。

共性的步骤在抽象类内公共实现,差异化的步骤在各个子类中实现。模板方法定义了一个算法的步骤,并允许子类作为一个或多个步骤提供实现。

使用继承和重写实现模板模式,被广泛用在框架中,白盒框架。例子如下

public abstract class CarBuilder {protected abstract void BuildSkeleton();protected abstract void InstallEngine();protected abstract void InstallDoor();// Template Method that specifies the general logicpublic void BuildCar() { //通用逻辑BuildSkeleton();InstallEngine();InstallDoor();}
}public class PorcheBuilder extends CarBuilder {protected void BuildSkeleton() {System.out.println("Building Porche Skeleton");}protected void InstallEngine() {System.out.println("Installing Porche Engine");}protected void InstallDoor() {System.out.println("Installing Porche Door");}
}public class BeetleBuilder extends CarBuilder {protected void BuildSkeleton() {System.out.println("Building Beetle Skeleton");}protected void InstallEngine() {System.out.println("Installing Beetle Engine");}protected void InstallDoor() {System.out.println("Installing Beetle Door");}
}

迭代器模式

客户端希望对放入容器/集合类的一组ADT对象进行遍历访问,而无需关心容器的具体类型。也就是说,不管对象被放到哪里,都应该提供同样的遍历方式。

让自己的集合类实现Iterable接口,并实现自己的独特Iterator迭代器,允许客户端利用这个迭代器进行显示或隐式的迭代遍历。

Vistor Pattern

对特定类型object的特定操作(visit),在运行时将二者动态绑定到一起,该操作可以灵活更改,无需更改被visit的类。

本质上:将数据和作用于数据上的某种/些特定操作分离开来。

为ADT预留一个将来可扩展功能的“接入点”,外部实现的功能代码可以在不改变ADT本身的情况下在需要时通过委派介入ADT

迭代器:以遍历的形式访问集合数据而无暴露其内部表示,将“遍历”这项功能委托到外部的迭代器对象。

Visitor: 在特定的ADT上执行某种特定操作,但该操作不在ADT内部实现,而是委托到独立的visitor对象,客户端可灵活扩展/改变visitor的操作算法,而不影响ADT。

策略和游客都是通过委派建立两个对象的动态联系。

但是游客强调的是外部定义某种对ADT的操作,该操作于ADT自身关系不大(只是访问ADT),故ADT内部只需要开放accept(visitor)即可,client通过它设定visitor操作并在外部调用。

而Strategy则强调是对ADT内部某些要实现的功能的相应算法的灵活的替换。这些算法是ADT功能的重要组成部分,只不过是委托到外部strategy类而已。

区别:visitor是站在外部client的角度,灵活增加对ADT的各种不同操作(哪怕ADT没实现该操作),strategy则是站在内部ADT的角度,灵活变化对其内部功能的不同配置。

设计模式的共性和不同

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

什么是健壮性和正确性

健壮性:系统在不正常输入或不正常外部环境下仍能够保持正常的程度。

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

准则:程序员应总是假定用户恶意、假定代码失败。严于律己,宽以待人。

  • 封闭实现细节,限定用户的恶意行为。
  • 考虑极端情况

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

正确性倾向于直接报错,健壮性则倾向于容错。

对比的例子如下:

健壮性:让用户变得更容易:出错也可以容忍,程序内部已有容错机制。

正确性:让开发者变得更容易:用户输入错误,直接结束。

对外的接口,倾向于健壮;对内的实现,倾向于正确。

可靠性=健壮性+正确性

如何度量健壮性和正确性

平均故障间隔时间(MTBF):指两次故障之间的平均工作时间。总的运行时间/总的故障次数。

MTTF(故障前平均时间)描述不可修复系统

残余缺陷率:每千行代码中遗留的bug的数量

Java中的错误和异常

不要抛出Error对象。

内部错误:程序员通常无能为力,一旦发生,想办法让程序优雅的结束。

异常:自己的程序导致的问题,可以捕获可以处理。

异常处理

什么是异常

程序执行中的非正常事件,导致程序无法再按预想的流程执行。

Exceptions将错误信息传递给上层调用者,并报告“案发现场”的信息。

return之外的第二种退出途径。若找不到异常处理程序,整个系统完全退出。

不使用处理机制的缺点:正常的逻辑代码与错误处理代码交织在一起。使用错误处理机制,把它们分隔开。

异常的分类

RuntimeException:运行时异常,由程序员在代码里处理不当造成,例如数组越界,只要修改代码就可。

其他异常:由外部原因造成。能想到可能会出现这种错误,但不可避免,要进行处理。

Checked和Unchecked异常

编译器可帮助检查程序是否已抛出或处理了可能的异常。(Checked Exceptions)

编译器不会检查Unchecked Exceptions。但在执行时出现就会导致程序失败,是程序中的潜在bug。类似动态类型检查。

Checked异常处理操作,关键词

  • try
  • catch
  • finally
  • throws(声明异常,本方法可能会发生XX异常)
  • throw(抛出XX异常)

使用Unchecked还是Checked

当客户端可以通过其他的方法恢复异常,那么采用Checked

当客户端对出现的这种异常无能为力,那么采用Unchecked

尽量使用unchecked exception来处理编程错误。如果客户端对一个Checked异常无能为力,可以把它转换为Unchecked异常,客户端会被挂起,并打印异常信息。

try{
..some code that throws SQLException
}catch(SQLException ex){
throw new RuntimeException(ex);
}

总结

  • Checked异常应该让客户端从中得到丰富的信息,以做出相应的处理
  • 要想让代码更加易读,倾向于使用Unchecked异常来处理程序中的错误

错误可预料,但无法预防,但可以有手段从中恢复,此时使用Checked异常,否则使用Unchecked异常。

  • Use checked exceptions for special results (i.e., anticipated situations)

  • Use unchecked exceptions to signal bugs (unexpected failures)

使用throws声明Checked异常

“异常(Checked)”也是规约的一部分,在post-condition中刻画。

Unchecked异常不用声明。

应该声明的异常包括调用的其他函数传来的异常和自己造出的异常。如果没有handler处理抛出的Checked异常,程序就会中止执行。

LSP:子类型可以抛出父类型抛出的异常的子类型,也可以不抛出任何异常。但不能抛出比父类型更宽泛的异常。

如何抛出异常

抛出一个异常类的对象,可以添加参数把信息传递给client

throw new EOFEXception();String gripe = "Content-length: " + len + ", Received: " + n;
throw new EOFEXception(gripe);

创建异常类

public class FooException extends Exception {public FooException() { super(); }public FooException(String message) { super(message); }public FooException(String message, Throwable cause) { super(message, cause); }public FooException(Throwable cause) { super(cause); }
}

捕获异常

异常发生后,如果找不到处理器,就终止执行程序,在控制台打印出stack trace。

try {codemore codemore code
}
catch (ExceptionType e) {handler for this type
}

try中代码抛出异常,并被catch捕获之后,try中抛出异常代码之后的代码不会再执行,执行捕获异常的catch中代码。

也可以不在本方法内处理,而是传递给调用方,由client处理。

注意:如果父类型的方法没有抛出异常,那么子类型中的方法必须捕获所有的Checked异常。

捕获多种异常,把父类型写下面。

重抛和链接异常

在catch中抛出异常,目的是更改异常的类型,更方便client端获取错误信息并处理。

try {access the database
}
catch (SQLException e) {throw new ServletException("database error: " + e.getMessage());
}

但这么做最好保留“根原因”

try {access the database
}
catch (SQLException e) {Throwable se = new ServletException("database error");se.initCause(e);throw se;
}
Throwable e = se.getCause();

finally 语句

当异常抛出时,方法中正常执行的代码被终止,如果异常发生前曾申请过某些资源,那么异常发生后这些资源要被恰当的清理。可以使用finally。

finally部分的代码,无论是否捕获异常都会被执行。

  • 未抛出异常:不执行catch,执行try之后执行finally。再执行模块以外的语句
  • 捕获抛出的异常:不执行try中抛出异常之后的代码,执行catch,finally,再执行模块以外的语句
  • 抛出异常但未被捕获:不执行try中抛出异常之后的代码,不执行catch,执行finally然后程序终止。

finally的优先级最高,以下代码返回false

static boolean decision() {try {return true;} finally {return false;}
}

断言

最好的防御就是不要引入bug。主要针对正确性,不符合,抛出error

避免扩散

什么是断言

在开发阶段的代码嵌入,检验某些“假设”是否成立。

可以添加信息

assert (number >= 0) : "number is negative: " + number;

断言是对代码中程序员所做假设的文档化,不会影响运行时性能(在实际使用时,断言会被disabled)。

使用断言

保证:内部不变量、表示不变量、控制流不变量、方法的前置条件和后置条件符合要求或保持不变。

因为在使用时,断言会被disabled。所以在断言中不要包含业务逻辑。

// don't do this:
assert list.remove(x);// do this:
boolean found = list.remove(x);
assert found;

程序之外的事情,不受控制的不要乱断言。断言是为了保证程序正确性。

使用异常处理文件/网络/用户输入等。

断言和异常的区别

断言保证正确性(开发阶段),处理绝不应该发生的错误

错误、异常处理保证健壮性(写在开发阶段,处理运行时的异常),处理预料到可以发生的不正常情况。

有些人认为不应该针对参数的合法性使用断言。(看参数的来源,内部就用断言,外部就抛出异常)

开发阶段尽可能用断言消除bugs,在发行版本里用异常处理机制处理漏掉的错误。

【哈工大软件构造】学习笔记10 第十章、第十一章、第十二章相关推荐

  1. 哈工大软件构造学习笔记1 Views and Quality Objectives of Software Construction

    先要搞清楚软件构造的对象是什么,如何刻画,在关注如何构造. 1,Five key quality objectives of software construction 软件构造的五个关键质量目标 容 ...

  2. 哈工大2022春软件构造学习笔记1

    课程概述 第一部分:软件构造基础 第二部分:ADT+OOP 第三部分:面向可复用性和可维护性的软件构造 第四部分:面向健壮性与正确性的软件构造 第一章 软件构造的多维度视图和质量目标 软件构造的多维度 ...

  3. 软件构造学习笔记(九)面向复用的软件构造技术

    目录链接 Part I What is Software Reuse? Part II How to measure "reusability"? Part III Levels ...

  4. 2020春季学期哈工大软件构造学习心得一

    前言: 今年是特殊的一年,由于新冠病毒的爆发,导致我们无法正常开学,所以网上开课如期进行,其中软件构造是这学期我们要面临的巨大挑战. 准备工作: 上学期末得知本门课程需要学习Java语言进行编程,所以 ...

  5. 软件构造学习笔记-第八周

    本周重点是Liskov可替换原则.它要求父类和子类的行为一致性,子类要有更强的不变量.更弱的前置条件.更强的后置条件.在该原则的要求下,每个子类都可以对父类进行替换.这在开发过程中会带来极大的便利,在 ...

  6. 哈工大软件构造2022笔记(持续更新----1)

    Class 1 第0节的主要内容是讲课程要求: 实验占35分,个人博客占5分,期末考试占60分(闭卷) 在cms中加入课程:链接在老师发的ppt里面有,这里就不展示了. 实验要求: 在Java+Ecl ...

  7. 2020春季学期哈工大软件构造学习心得二

    前言 上一章主要讲了软件构造的结果形态以及如何是一个"好"的软件 这一章主要学习软件开发遵循着一个什么样的过程 - 软件生命周期与配置管理 From 0 to 1,from 1 t ...

  8. 软件构造学习笔记ATD

    在面向对象的编程中,ADT的编写十分重要,与传统的c语言不同,面向对象的编程更加商业化一点,所以保密需要做好,有点商业机密的感觉.如何设计良好的抽象数据结构,通过封装来避免客户端获取数据的内部表示,避 ...

  9. 软件构造学习笔记-第九周、第十周

    因为本周五开始五一假期,所以只有一节软件构造课.因为内容还属于创建模式.结构模式.行为模式.将该堂课的内容整合到本博客中.本周的重点是程序开发模式,在写代码之前首先充分考虑采用哪种模式更有利于开发.维 ...

最新文章

  1. 武汉python培训哪一家好一些-武汉哪个Python培训机构比较好?
  2. UWP 查找模板中的控件
  3. android动画封装,Android属性动画封装,快速构建动画
  4. 如何查看linux系统的体系结构
  5. 158.5. manifests
  6. leetcode 278. 第一个错误的版本(Java版)
  7. 快速排序算法的优化思路总结
  8. mysql无法识别双引号_sqlite3迁移mysql问题集合攻略
  9. mysql省市联动_sql全国 省市 联动级联
  10. selenium入门详细指南(附淘宝抢购案例)
  11. 调用谷歌Chrome浏览器打不开网页崩溃了
  12. 炫酷粒子表白,双十一脱单靠它了!
  13. 20190713 关于session串号问题的记录
  14. 文科生学计算机有前途吗,文科生学习计算机有难度吗?计算机的前景如何?
  15. Python的编译器
  16. python的third party llibs
  17. et格式如何转换Excel
  18. Eclipse 从SVN检出项目之《文件夹 “” 已不存在 》
  19. KesionCMS V9.03 Final SQL注射
  20. MPEG LA推出一站式Qi无线充电许可

热门文章

  1. python中的列表生成式 | 字典生成式
  2. Android 引入高德3D地图 显示白屏或黑屏解决办法
  3. 地图数据设计(三):坐标参考系统的选择
  4. 4076 字符串权值(模拟)
  5. 开心一笑 最近没什么要更新的 准备不干程序了
  6. 对于互联网行业,学历真的重要吗?
  7. Collapse组件(一) collapse过渡动画
  8. 为什么 2 * (i * i) 比 2 * i * i 效率高?
  9. vs(c#)做table(表格)之GridView
  10. 使用Glade3.0进行界面开发