哈工大软件构造学生笔记学习与软件复用心得

哈工大软件构造学生笔记学习与软件复用心得**软件的多维度视角软件多维度视角分为三组:代码和组件构建时和运行时时刻与周期(period)读数1:静态检查静态打字优秀软件的三大属性冰雹层序作为一个运行示例,我们将探索冰雹序列,定义如下

**
软件的多维度视角
软件多维度视角分为三组:
代码和组件
构建时和运行时
时刻与周期(period)

读数1:静态检查

静态打字
优秀软件的三大属性
冰雹层序
作为一个运行示例,我们将探索冰雹序列,定义如下。从一个数字n开始,如果n是偶数,那么序列中的下一个数字是N/2,如果n是奇数,那么为3n+1。该序列在到1时,结束。下面是一些示例:
2, 1
3, 10, 5, 16, 8, 4, 2, 1
4, 2, 1
2n, 2n-1 , … , 4, 2, 1
5, 16, 8, 4, 2, 1
7, 22, 11, 34, 17, 52, 26, 13, 40, …? (where does this stop?)
由于奇数法则,冰雹序列在下降到1之前,可能会上下跳。据推测,所有冰雹最终都会落在地面上,也就是说,冰雹序列以任何n开始时,最终都到达1。但是这仍然是未决问题。为什么叫冰雹序列?因为冰雹是在云层中上下颠簸形成的,直到它们最终形成足够的重量,落在地球上。
计算冰雹
下面是一些计算和打印冰雹序列的代码n。我们将编写Java和Python,以便进行比较:
** Java**
int n = 3;
while (n != 1) {

System.out.println(n);
if (n % 2 == 0) {

n = n / 2;
} else {

n = 3 * n + 1;
}
}
System.out.println(n);
Python
n = 3
while n != 1:
print(n)
if n % 2 == 0:
n = n / 2
else:
n = 3 * n + 1
print(n)
这里有几件事值得注意:

Java中表达式和语句的基本语义非常类似于Python:例如while和if,做同样的事情。
Java需要语句末尾加分号。额外的标点符号可能是麻烦的,但它也给我们提供了更多的自由,您如何组织您的代码,您可以分割成多行语句,以获得更多的可读性。
在if和while的条件下,Java需要括号。
需要在块周围加花括号{},而不是缩进。 您应该总是缩进代码块,即使 Java 不会注意您的额外空间。 编程是一种沟通形式,你不仅要与编译器沟通,还要与人类沟通。 人类需要这样的标记。 我们稍后再谈这个问题
上面的 Python 和 Java 代码之间最重要的语义差异是变量 n 的声明,java指定了它的类型: int。
A 类型是一组值,以及可以对这些值执行的操作。
Java有几个基本类型,其中:
int,long,boolean,double,char
Java也有对象类型,例如:
String表示字符序列,如’zs‘字符串。
BigInteger表示任意大小的整数,因此它的作用类似于Python中的整数。
按照Java的惯例,基本类型是小写的,而对象类型以大写字母开头。
java和python中的操作是接收输入并产生输出的函数(有时还会改变值本身)。操作的语法各不相同,但不管它们是如何编写的,我们仍然认为它们是函数。下面是Python或Java中操作的三种不同语法:
作为中缀、前缀或后缀运算符,例如,a + b调用操作 ‘+’ : int × int → int
(在这个表示法中:+是操作的名称,int × int在箭头描述这两个输入之前,int在箭头描述输出之后。)
作为对象的一种方法。例如,bigint1.add(bigint2)调用操作add: BigInteger × BigInteger → BigInteger.
作为一种功能。例如,Math.sin(theta)调用操作sin: double → double。这里,Math不是对象。是包含sin功能。(类的方法,不依赖对象)
对比Java的str.length()和Python的len(str)。这是两种语言中相同的操作–一个接受字符串并返回其长度的函数–但它只是使用了不同的语法

一些操作是过载因为相同的操作名称用于不同的类型。算术算子+, -, *, /对于Java中的数字基本类型来说,都是重载的。操作可以重载,方法属于操作,方法也可以重载。大多数编程语言都有一定程度的重载。

静态类型
Java是一个静态类型语言所有变量的类型在编译时(程序运行之前)都是已知的,因此编译器也可以推断所有表达式的类型。如果a和b被声明为int,然后编译器得出结论,a+b也是int。实际上,Eclipse环境在编写代码时就会这样做,因此您在键入代码时就会发现许多错误。

在 Python 这样的动态类型语言中,这种检查推迟到运行时(当程序运行时)。

静态类型是一种特殊的静态检查,这意味着在编译时检查错误。 Bug 是编程的祸根。 本课程中的许多想法都是为了消除代码中的 bug,而静态检查是我们看到的第一个想法。 静态类型可以防止大量的 bug 感染您的程序: 准确地说,是将操作应用于错误类型的参数而导致的 bug。 如果你写了一行中断代码,比如:
“5” * “6”
这会使两个字符串相乘,然后静态类型将在您仍在编程时捕获此错误,而不是等到执行过程中到达行为止。
静态检查,动态检查,无检查
考虑一下语言可以提供的三种自动检查是很有用的:
静态检查: 在程序运行之前自动发现 bug
动态检查: 代码执行时自动发现错误
没有检查: 语言根本不能帮助你找到错误。 你必须自己注意,否则就会得到错误的答案
不用说,静态地捕捉 bug 要比动态地捕捉好,动态地捕捉 bug 要比根本不捕捉好。
下面是一些经验法则,告诉您在每次执行这些操作时可能会遇到哪些错误
静态检查可以捕获:
语法错误,比如多余的标点符号或者虚假(错误)的单词。 即使是 Python 这样的动态类型语言也会执行这种静态检查。 如果您的 Python 程序中出现缩进错误,您将在程序开始运行之前发现这个错误。
名字不对,比如Math.sine(2). 正确的名字是sin
参数个数错误,比如Math.sin(30, 20).
错误的参数类型,比如Math.sin(“30”).
错误的返回类型,比如return “30”; 从一个被声明为返回int。
动态检查可以捕捉到:
非法参数值。例如,整数表达式x/y只有在y实际上是零时,才是错误的,否则它会工作。因此,在这个表达式中,除以零不是静态错误,而是动态错误
无法表示的返回值,即当特定的返回值不能用类型表示时。
超出范围的索引,例如,对字符串使用负的或过大的索引.
null对象引用调用方法,(null就像Python中的None).
静态检查倾向于类型,即与变量的特定值无关的错误。 类型是一组值。 静态类型确保变量将具有该集合中的某些值,但直到运行时才能确切知道该变量具有哪些值。 因此,如果错误只是由某些值引起的,比如 divide-by-zero 或 index-out-of-range,那么编译器就不会为此产生静态错误。
相比之下,动态检查往往是关于由特定值引起的错误
令人惊讶的是: 基本类型不是真数
Java 和许多其他编程语言中的一个陷阱是,它的基本数值类型我们习惯的整数和实数不同。 结果,一些真正应该动态检查的错误根本没有得到检查。 以下是一些陷阱:
整数除法。 5 / 2不返回一个分数,它返回一个截断的整数。 所以这是一个例子,我们可能希望是一个动态错误(因为一个分数不能表示为整数)经常产生错误的答案。
整数溢出。 int 和 long 类型实际上是有限的整数集,具有最大值和最小值。 当你计算一个答案正向太大或负向过小的问题时,会发生什么? 计算悄悄地溢出(包装) ,并从法定范围的某个地方返回一个整数,但不是正确的答案。
浮点类型中的特殊值。 像 double 这样的浮点类型有几个不是实数的特殊值: NaN (代表“不是一个数”)、正 infinity 和负 infinity。 因此,当你对一个双精度数应用某些操作时,你可能会产生动态的错误,比如除以0或者取负数的平方根,你会得到这些特殊值中的一个。 如果你继续用它来计算,你最终会得到一个糟糕的答案。
阅读练习
让我们尝试一些有 bug 的代码示例,看看它们在 Java 中的表现如何。 这些 bug 是静态的、动态的还是根本没有被捕获?
int n = 5;
if (n) {

n = n + 1;
}
这是一个静态类型错误,因为 if 语句需要布尔类型的表达式,但是 n 有 int 类型。
int big = 200000; // 200,000
big = big * big; // big should be 40 billion now
这是一个整数溢出,因为 int 值不能表示大于231(大约20亿)的数字。 它不是静态捕获的,但不幸的是在 Java 中它也不是动态捕获的。 整数溢出会悄悄地产生错误的答案。
double probability = 1/5;
如果程序员的目的是得到0.2,那么这是使用了错误的操作。 / 重载整数除法和浮点除法。 但是因为它是用整数1和5来调用的,所以 Java 选择了整数除法,然后静静地截断这个分数,得到了一个错误的答案: 0。
int sum = 0;
int n = 0;
int average = sum/n;
零除法不能产生整数,所以它会产生一个动态错误。
double sum = 7;
double n = 0;
double average = sum/n;
除以零也不能产生实数——但是与实数不同,double 对于正无穷有一个特殊的值,所以当你除以零的时候,它就会返回这个值。 如果代码试图计算一个平均值,无穷大不太可能是一个正确或有用的答案。
数组和集合
让我们改变我们的冰雹计算,以便将序列存储在一个数据结构中,而不仅仅是打印出来。 Java 有两种类似列表的类型可以使用: 数组和列表。
数组是另一种类型T的固定长度序列。例如,下面是如何声明一个数组变量并构造一个数组值以分配给它:
int[] a = new int[100];
这个int[]数组类型包括所有可能的数组值,但是一个特定的数组值一旦创建就不能更改其长度。对数组类型的操作包括:
索引:a[2]
任务:a[2]=0
长度:a.length(请注意,这个a.length与String.length() 不是一个方法调用,所以不要在它后面加上括号)
下面是使用数组的冰雹代码的漏洞。我们首先构造数组,然后使用一个索引变量 i 逐步遍历数组,在生成序列时存储它们的值。
int[] a = new int[100]; // <==== DANGER WILL ROBINSON
int i = 0;
int n = 3;
while (n != 1) {

a[i] = n;
i++; // very common shorthand for i=i+1
if (n % 2 == 0) {

n = n / 2;
} else {

n = 3 * n + 1;
}
}
a[i] = n;
i++;
在这种方法中,有些东西应该马上就会觉得不对劲。 那个神奇的数字100是什么? 如果我们尝试一个 n,结果是一个很长的冰雹序列,会发生什么? 它不适合长度为100的数组。 我们有一个漏洞。 Java 会静态地、动态地捕捉这个 bug,还是根本不会捕捉? 顺便说一句,类似这样的漏洞——溢出一个固定长度的数组,这种漏洞通常在不太安全的语言中使用,比如 c 和 c + + ,这些语言不会在运行时自动检查数组访问——已经造成了大量的网络数字证书认证机构和互联网蠕虫。
不使用固定长度的数组,而是使用 List 类型。 List 是另一种类型 T 的可变长度序列。下面是我们如何声明一个 List 变量并创建一个 List 值:
List list = new ArrayList();
下面是它的一些操作:
indexing: 索引:list.get(2)
assignment: 任务作业:list.set(2, 0)
length: 长度:list.size()
注意,List 是一个接口,不能直接用 new 构造类(类型),而是指定 List 必须提供的操作。 我们将在以后关于抽象数据类型的课程中讨论这个概念。 Arraylist(Array:数组,List:列表) 是一个类,一个提供这些操作实现的具体类型。 数组列表并不是 List 类型的唯一实现,尽管它是最常用的一种。 另一个例子是 LinkedList。 你可以在“ Java 8 API ”文档中找到它们,你可以在网上搜索“Java 8 API”。 了解 Java API 文档,他们是你的朋友。 (“ API”意味着“应用程序员接口” ,通常用作“库”的同义词。)
还要注意,我们编写的是List 而不是 List。 不幸的是,我们不能将List直接类比为 int []。 列表只知道如何处理对象类型,而不知道基本类型。 在 Java 中,每个基本类型(以小写形式编写,通常是缩写形式,如 int)都有一个等价的对象类型(大写形式,拼写完整,如 Integer)。 当我们用尖括号参数化一个类型时(泛型)Java 要求我们使用这些等价的对象类型但在其他上下文中,Java 会自动在 int 和 Integer 之间进行转换,(自动装包拆包)因此我们可以编写Integer i = 5而不会出现任何类型错误。
下面是用 List 写的冰雹代码:
List list = new ArrayList();
int n = 3;
while (n != 1) {

list.add(n);
if (n % 2 == 0) {

n = n / 2;
} else {

n = 3 * n + 1;
}
}
list.add(n);
不仅更简单,而且更安全,因为 List 会自动放大自己(扩容,底层仍是数组,对数组的封装和对其的操作),以适应您向它添加的数字(当然,直到您耗尽内存)。
迭代
for循环遍历数组或列表的元素,就像Python中的那样,尽管语法看起来有点不同。例如:
// find the maximum point of a hailstone sequence stored in list
int max = 0;
for (int x : list) {

max = Math.max(x, max);
}
您可以迭代数组和列表。 如果列表被数组替换,同样的代码也可以工作。
Math.max()是Java API中的一个(方便)的函数。这个Math类中有很多有用的函数,比如网上搜索“java 8 Math”以找到它的文档。
方法
在 Java 中,语句通常必须在一个方法中,每个方法都必须在一个类中,所以编写冰雹(Hailstone)程序最简单的方法是这样的:
public class Hailstone {

/

* Compute a hailstone sequence.
* @param n Starting number for sequence. Assumes n > 0.
* @return hailstone sequence starting with n and ending with 1.
*/
public static List hailstoneSequence(int n) {

List list = new ArrayList();
while (n != 1) {

list.add(n);
if (n % 2 == 0) {

n = n / 2;
} else {

n = 3 * n + 1;
}
}
list.add(n);
return list;
}
}
让我们来解释一下这里的一些新东西。
Public 是指程序中任何地方的任何代码都可以引用类或方法。 其他访问修饰符(比如 private)用于在程序中获得更多的安全性,并保证不可变类型的不变性。 我们将在接下来的课程中进一步讨论它们。
Static 意味着
该方法不接受 self 参数**——在 Java 中这是隐式的(在Python中是显式的),您永远不会将其看作方法参数。 不能对对象调用静态方法。 比较一下 List add ()方法或 String length ()方法,例如,它们要求先有一个对象。 相反,调用静态方法的正确方法是使用类名而不是对象引用(static修饰的方法,既可通过类来调用,也可通过实例来调用,但是不能使用this引用。java中类和对象都可以调用静态方法,但是python中只能对象调用静态方法。
不过还是建议使用类来调用静态方法,这样代码更加清晰明确。)
:
Hailstone.hailstoneSequence(83)
在方法之前也要注意注释,因为它非常重要。 此注释是方法的规范,描述了操作的输入和输出。 说明书应简明、清晰、准确。 注释提供了方法类型中尚未明确的信息。 例如,它没有说 n 是一个整数,因为下面的 int n, 声明已经说明了这一点。 但是它确实说明了 n 必须是正数,这并没有被类型声明所描述(捕获),但是对于调用者来说知道这一点非常重要。
关于如何在几个类中编写好的规范,我们还有很多要说的,但是您必须马上开始阅读和使用它们。
变化值与重新分配变量
接下来的阅读将介绍快照图,让我们可以直观地看到变量和值之间的区别。 当你给一个变量赋值时,你改变了变量的箭头指向的位置。 您可以将其指向不同的值。
当您为可变值(如数组或列表)的内容赋值时,您正在更改该值内部的引用。
当您为可变值(例如数组或列表)的内容分配时,您正在更改该值中的引用。
改变是罪恶的。优秀的程序员会避免改变的事情,因为它们可能会出乎意料地改变。
不变性(免于变化)是本课程的主要设计原则。 不可变类型是其值在创建后永远不会更改的类型。 (至少不是以外部世界可见的方式——这里有一些微妙之处,我们将在未来的课程中更多地讨论不变性。) 到目前为止我们讨论的类型中,哪些是不可变的,哪些是可变的?
Java 还为我们提供了不可变的引用: 一次赋值的变量,从不重新赋值。 要使一个引用成为不可变的,用关键字 final 声明它:
final int n = 5;
如果 Java 编译器不相信你的最终变量只会在运行时被赋值一次,那么它就会产生一个编译器错误。 因此 final 为您提供了对不可变引用的静态检查。
**最好使用 final 声明方法的参数和尽可能多的局部变量。 与变量的类型一样,这些声明也是重要的文档,**对于代码读者很有用,并且由编译器静态检查。
在我们的 hailstoneSequence 方法中有两个变量: 我们是否可以声明它们是最终的?

public static List hailstoneSequence(final int n) {

final List list = new ArrayList();
在这里插入图片描述
在这里插入图片描述
不能重复赋值。
记录假设
变量的类型写入文档作为一个假设: 例如,这个变量总是引用一个整数。 Java 实际上在编译时检查了这个假设,并保证在程序中没有违背这个假设的地方。
声明变量 final 也是一种文档形式,声明变量在初始赋值之后永远不会改变。 Java 也会静态地检查这一点。
我们记录了 Java (不幸的是)不能自动检查的另一个假设: n 必须是正数。
为什么我们需要写下我们的假设? 因为编程中充满了这些错误,如果我们不把它们写下来,我们就不会记住它们,其他需要阅读或更改我们的程序的人也不会知道它们。 他们只能靠猜了。
编写程序时必须考虑两个目标:
与计算机交流(通信)。 首先要说服编译器你的程序是合理的——1.语法正确和2.类型正确。 然后使3.逻辑正确,以便在运行时给出正确的结果
和其他人交流(可读性)。 使程序易于理解,这样当有人需要修改、改进或者在未来对其进行调整时,他们就可以这样做。
黑客(Hacking)与工程
我们在这门课上编写了一些黑客代码,黑客攻击通常带有不受约束的乐观主义特征:
坏: 在测试任何代码之前都要编写大量的代码
坏处: 把所有的细节都记在脑子里,假设你会永远记住它们,而不是把它们写在代码里。
缺点: 假设缺陷不存在,或者很容易找到和修复
但软件工程不是黑客行为,工程师是悲观主义者:
好: 每次写一点,边写边测试。 在以后的课程中,我们将讨论测试优先编程
好: 记录你的代码所依赖的假设
好: 保护你的代码不受愚蠢的影响——尤其是你自己的! 静态检查可以帮助你做到这一点
目标
我们这个课程的主要目标是学习如何制作软件,它是:
Safe from bugs(安全)正确性(现在正确的行为)和防御性(将来正确的行为)
Easy to understand(很容易理解)
必须与未来的程序员沟通,他们需要理解它并对其进行修改(修复 bug 或添加新特性)。 未来的程序员可能是你,几个月或几年后。 如果你不把它写下来,你会惊讶地发现自己忘记了多少,而拥有一个好的设计对你未来的自己有多大帮助
Ready for change(为改变做好准备)
软件总是在变化。 有些设计使更改变得容易; 有些则需要丢弃和重写大量代码
软件还有其他重要的属性(比如性能、可用性、安全性) ,它们可以权衡这三个属性。 但是这些是关心的三大问题,而且
软件开发人员通常最重视的是构建软件的实践操作
。 这是值得每一个语言考虑的特性,每一个编程实践,每一个设计模式的特性,我们在这门课程中学习,并了解它们如何与三大联系。
为什么我们要在这门课上使用 Java
既然您已经使用了6.009,那么我们假设您对 Python 很熟悉。 那么为什么我们不在这门课中使用 Python 呢? 为什么我们要在6.031中使用 Java?
安全是第一个原因。 Java 有静态检查(主要是类型检查,但也有其他类型的静态检查,比如代码从声明为这样做的方法中返回值)。 我们在这个课程中学习软件工程,防止错误是这个方法的一个关键原则。 Java 将安全性调整到11,这使得它成为学习良好的软件工程实践的好语言。 用 Python 这样的动态语言编写安全代码当然是可能的,但是如果您了解了如何使用安全的、静态检查的语言,就更容易理解您需要做什么。
普遍存在是另一个原因。 Java 被广泛应用于研究、教育和工业。 Java 运行在许多平台上,而不仅仅是 windows / mac / linux。 Java 可以用于 web 编程(包括服务器和客户端) ,本地 Android 编程是用 Java 完成的。 虽然其他的编程语言更适合于教授编程(我想到了 Scheme 和 ML) ,但遗憾的是这些语言在现实世界中并不普遍。 你的简历上的 Java 语言会被认为是一项很有市场的技能。 但是不要误解我们的意思: 您将从本课程中获得的真正技能并不是特定于 java 的,而是可以应用到您可能使用的任何语言中。 本课程最重要的经验教训将在语言风潮中存活下来: 安全、清晰、抽象、工程本能。
在任何情况下,一个好的程序员必须掌握多种语言。 编程语言是工具,你必须使用正确的工具。 在你完成 MIT 的职业生涯之前,你肯定需要学习其他的编程语言(JavaScript,c / c + + ,Scheme 或者 Ruby 或者 ML 或者 Haskell) ,所以我们现在开始学习第二种语言。
java由于其无处不在的特性,Java 拥有大量有趣和有用的库(包括其巨大的内置库和网络上的其他库) ,以及优秀的免费开发工具(ide 如 Eclipse、idea、编辑器、编译器、测试框架、剖析器、代码覆盖率和样式检查器)。 甚至 Python 在其丰富的生态系统方面仍然落后于 Java。
有一些理由让我们后悔使用Java。 它很冗长,所以很难在黑板上写出例子。 它很大,多年来积累了很多功能。 内部不一致性(例如,final 关键字在不同的上下文中意味着不同的东西,而 Java 中的 static 关键字与静态检查毫无关系)。 它承载了诸如 c / c + + 之类的旧语言的包袱(基本类型和 switch 语句
是很好的示例
)。 它没有 Python 那样的解释器,你(python)可以通过处理一小段代码来学习。
但是总的来说,现在 Java 是一种合理的语言选择,它可以帮助你学习如何编写安全、易于理解并且可以随时改变的代码。 这就是我们的目标。
摘要总结:
我们今天介绍的主要思想是静态检查。以下是这个思想与课程目标的关系:
Safe from bugs:
避免 bug。静态检查通过在运行前捕获类型错误和其他 bug 来帮助安全。
Easy to understand:
易于理解。这有助于理解,因为类型在代码中是显式说明的。
Ready for change:
为改变做好准备。 静态检查可以通过确定需要连续更改的其他位置来更容易地更改代码。例如,当您更改变量的名称或类型时,编译器会立即在使用该变量的所有位置显示错误,并提醒您更新这些错误。
从类、API、框架三个层面学习如何设计可复用软件实体的具体技术
类和API的复用
早期软件复用仅仅是不同系统间共享数据以及某些与数据密切联系的秩序段 , 而现在的软件复用不仅仅是对程序复用 , 它包括对软件生产过程中任何活动所产生的制成品的复用 , 如 : 项目计划成本估计体系结构需求模型 和规格说明、设计 、源程序代码 、用户文档和技术文档 、用 户界 面 、数据结构和测试用例 。
按抽象程度的高低 , 划分复用级别 :
程序代码的复用 。 包括目标代码和源代码的复用 。其中目标代码的复用级别最低 , 历史也最久 。 由于这种复用对运行环境的配置及应用系统的上下文要求很严格 , 并 且不容易修改 , 复用的范围受很大限制 。
设计结果的复用 。 设计结果比源程序的抽象级别高 , 因此它的复用受环境的影响 较少 , 从而使可复用构件被复用的机会更多 , 并且所需的修改更少 。
分析结果的复用 。 分析结果是比设计结果更高级别的复用 。 可复用的分析构件是针对问题域的某些事物或问题的抽象程度更高的解法 , 受设计技术及实现的影响很少 , 所以可复用的机会更大 。
测试信息的复用 。 主要包括测试用例的复用和测试过程信息的复用 。前者是把一个软件的测试在新的软件测试中使用 , 或者在软件作出修改时在新的一轮测试中使用 。后者是在测试过程中通过软件工具自动地记录测试的过程信息 , 包括测试员的第一个操作 、输入参数 、测 试用例及运行环境等一切信 息。这种复用中 , 被复用的对象是信息 , 它的识别不便于和前几种作准确比较 , 但从这些信息的形态看 , 大体处于与程 序代码相当的级别 。
1.类具有可复用的特性 。面向对象中类是基本成分 。类库是实现对象类复用的基本 条件 。在面向对象中 , 所有的现实世界存在的事物都可以定义为对象 , 对象类是同种对象的抽象表示 , 它具有符合可复用构件所应具有的以下基本特性 : ①完整性 : 每一个类都具有自身的属性和为其他类提供 的服务 ; ② 独立性 : 对象的封装原则把对象的属性和服务结合成一个独 立的系统单位 , 并尽可能隐蔽了对象的内部细节 。只通过少量 的允许外部使用的服务接口提供服务, 与其他对象发生联系 。在使用对象时 , 只需要 注意它对外呈现什么行为 , 而不必关心它的内部细节 ; 同时由于其封装原则 , 当对构件内部进行修改时 , 对外部几乎没有影响 ; ③可标识性 : 类的命名通常和 现实生活中的一致 ;④一般性 : 类是具有共同特征的事物的最一般的抽象 。由于类具有以上特性 , 可用建立对象类来实现复用。面向对象从最初的构造类就以设计可复用的“ 插接相容性 ” 构件为类设计的主要目标 。
2、类与类之间的继承与一般一特殊结构、聚合与整体结构增强了类的可复用性,对复用提供了有力的支持。类之间的消息传递也体现了对软件复用的支持。
3、面向对象利用封装特性和继承特性为类的合成提供了方便。
4、具有独立的OOA:独立的OOA过程使得在构件的形成过程中不用过多考虑具体的实现环境,并对其进行一般化处理,使其具有较高的可复用的价值。
面向对象方法中的软件复用
1、照原样复用既存类。寻找现存类,提供所需要的特性。此时,所需要的类已经存在现在建立它的一个实例,用以提供所需要的特性。这个实例可直接为应用软件利用,或者它可以用来做另一个类的实现部分。通过复用一个现存类,可得到不加修改就能工作的已测试的代码。由于大多数面向对象语言的两个特性,即界面与实现的分离(信息隐蔽和封装),这种复用一般是成功的。但多数照原样复用被限制在低层上最基本的类,像基本数据结构。对于较一般的结构,可以在实例化时,使用参数来规定它们的行为。
2、从既存类演化。此时,没有一个完全符合条件的类存在,但存在一个类,它提供的行为类似于要为新类定义的行为。可通过使用既存类做为定义新类的起点,根据既存类演变而成。为了在子类中使用类库中基类的属性和操作,可以考虑在子类中引进基类的实例作为子类的实例变量。然后,在子类中通过实例变量来复用基类的属性或操作。也可以将新子类直接说明为类库中基类的子类。通过继承、修改基类的属性和操作来完成新子类的定义。
3、完全重新开发新类。当一个类不可能从已有的类中复用时,要重新开发新类。任何一个类,只要它的开发不涉及现存类,就可看作是一个新的继承结构的开始,因此,将建立两种类,一种是抽象类,它概括了将要表达的概念;另一种是具体类,它要实现这个概念。在新类开发中,设计给出类的所有细节。这个阶段的输出是有关类的属性的细节、可支持它们的实现。单个类的设计包括构造数据存储。其内部表示还包括一些私有函数,其低层设计还涉及一些重要联系,如继承和组装关系。通过变量的声明、操作界面的实现,可实现一个类的预期行为和状态。虽然不需要使用现存类来演变成新类,但还是有复用的可能性。在新类实现时,通过说明一些现存类的实例,可以加快一个类的实现。象表格、硬件接口等都可以用来作为一个新类的局部。
使用复用技术减少软件开发活动中大量的重复性工作,提高软件生产率,降低开发成本,缩短开发周期。同时,提高了软件的灵活性和标准化程度。面向对象的程序设计方法以及其概念和机制支持了软件的复用,只要今后在软件开发的过程中熟练运用面向对象方法及软件复用,定会促进软件开发行业的发展。
框架的复用
在计算机科学和软件工程领域里, 可复用性一直是一个比较热门的话题。一个软件的质量的好与坏, 一方面要看它的执行效率是否达到客户的最初需求; 另外一方面就是要看 它的设计初衷是不是充分考虑了可重用性, 而这也是软件可持续发展的比较重要的要求。 在现代软件工程里, 为了解决类似问题, 一个行之有效的方法就是复用, 可复用的方式有很多, 其中比较重要的手段之一就是框架和设计模式。事实上, 它们是现在软件业发展的基石, 在仓储管理、医疗卫生、商业销售、交通电力等若干领域得到广泛应用。
Customer 类包括一个地址类, 所以任何需求所导致对Address类的变化将会 导致客户应用 (这里是Customer 类 )的重构和重新编译和部署, 最大的问题是这样的代码中存在着紧耦合的关系, 也就是说, Customer类包括着对地址类的引用, 可以总结出这种模式 的以下几个问题:
1)Customer 控制着Address类的创建;
2)Customer 对Address 类的直接引用导致它们之间的一种紧耦合关系; 3)Customer 类对 Address类的变化是敏感的, 所以如果以后随着变化加入HomeAddress类, OfficeAddress类, 那么我们不得不重新重写Customer 类的处理逻辑。因为这里 Customer类也直接地暴露给了Address类。
在这里插入图片描述
所以理所当然的是当 Address类初始化失败的时候, 整 个Customer 也会跟着初始化失败。 探讨解决这种问题的方 案, 问题的关键在于解决图 1的两个问题, 我们先来解决第二个问题, 如果我们可以将创建Address对象的任务交由第三 方的实体类来完成, 那么至少这种创建具体类的依赖就不会 存在了。也就是说, 如果我们把这种控制 (在这里是创建 )反 向交由第三方类来解决, 我们就解决了这个问题, 这就是反向 控制 ( Inversion of Control , IoC )的基本思想。
可复用框架避免紧耦合的方法
这里DIP(依赖倒置原则)所倡导的原则是:
1)高层模块不应该依赖于底层模块, 二者都应该依赖于 抽象;
2)抽象不应该依赖于细节, 细节应该依赖于抽象。
以前, 当我们还在写 C程序, 而且熟悉的是一种面向过 程的设计思想, 所以在设计的时候, 我们总是先写好具体的底 层模块, 然后再逐步地向上封装, 最后我们的功能体现上层模 块对底层模块的调用上。但是应考虑依赖于底层模块的上层 模块意味着什么。正是高层模块包含了应用程序中的重要的 策略选择和业务模型, 这些高层的模块使得其所在的应用程 序区别于其他。然而, 如果这些高层模块依赖于底层模块, 对 底层模块的改动会直接影响到高层模块, 从而迫使它们依次 做出改动。
这种情形是非常不符合逻辑的。本来应该是高层的策略 设置模块去影响底层的细节的实现模块的, 包含高层业务规 则的模块应该优先并且独立于包含实现细节的模块, 如果高 层模块独立于底层模块, 那么高层模块就可以不绑定到具体 的实现, 从而可以独立地演化和发展, 所以重用性就会得到很 大的提高, 这也是框架设计的核心原则。
在这里插入图片描述
在这里插入图片描述
通过这种倒置的接口所有权, 所有具体的底层模块的改动或派生都不会影响到高层Customer 模块, Address的改动不 会导致Customer 模块的重新编译, 而且Customer 高层模块可 以在定义了符合 IAddress的任意上下文中重用。所以, 通过这种重用, 我们创建了一个更灵活、更持久、更易改变的通用 结构。
DIP的解释是: “依赖于抽象”, 就像上面我们利用抽象解决高层模块和底层模块之间的紧耦合问题一样。该规则所建议的是不应该依赖于具体的类, 也就是说程序中所有的依赖关系都应该终止于抽象类和接口:
1)任何变量都不应该持有一个指向具体类的应用;
2)任何类都不应该从具体类派生;
3)任何方法都不应该重写它的任何基类中已经实现的方法。
当然, 这只是一个指导性的规则, 如果系统完全按此规则来构造的话, 诚然可以获得很强的通用性, 但是另外一方面, 复杂性和维护工作量也大大增加。而且, 对于一些虽然是具体但是稳定的类来说也不太合理。当然, 我们实际上是用接口充当了一个稳定的结构, 从而隔离了具体服务实现(不稳定)的变化,就如用空间换取时间一样。安全性与速度(并发,锁)的转换(根据不同场景)
高层策略模块应用背后的抽象, 在上面的客户和地址的例子中, 背后的抽象机制是客户利用地址完成一些事情。具体是什么地址, 以及客户拿地址以后怎样处理, 这些都不会影响抽象实现的具体细节(就是高层模块使用依赖实现的细节)
什么是DI(依赖注入)
依赖注入来很好地达成了 IoC的目 标, 完成了解耦的目的。在这里, 只有 IoC Framework知道抽象 IAddress接口的存在, Address通过把自己的实例委托给IoC Framework , 然后IoC Framework通过调用 Customer类暴露的方法来设置地址。通过图 5的UML图可以更清楚地看到 Customer 和Address之间的解耦。
在这里插入图片描述
在这里插入图片描述
一般IoC框架都是通过创建 一个对象, 然后将对象传递给另外一个对象来解耦的。 其实, 实现IoC的DI模式有很多种, 上文提到的只是其中的一种叫做Setter模式, 常用的DI模式主要有: 构造器模式、Setter模式、接口模式和服务定位器模式
从一个紧耦合的案例出发, 通过应用 IoC设计原则, 使得Customer类和Address类都依赖于抽象 IAddress类来隔离变化, 使得它们都可以按照自己的应用独立地演化, 使复用 程度得到提高。同时在此基础上, 经过泛化得到了更一般的处理耦合的设计模式 DI , 以及常用的几种 DI 编程最佳实践。

今天的文章哈工大软件构造学生笔记学习与软件复用心得分享到此就结束了,感谢您的阅读。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/68225.html

(0)
编程小号编程小号

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注