@ 软件工程笔记
生命周期
- 问题定义:开发目标及可行性
- 需求分析:对软件需要实现的各个功能详细分析,需求会不断变更
- 软件设计:根据需求分析结果,对整个软件系统进行设计
- 软件开发:编码
- 软件测试:找出问题加以纠正,有单元测试、组装测试、系统测试三个阶段
- 软件维护:延续软件使用寿命,分为纠错性和改进性
软件过程
最基本的软件工程活动:
- 软件规格说明
- 软件开发
- 软件确认
- 软件演化
软件过程是个层次化的技术,从低到高为:质量,过程,方法,工具
软件过程模型
瀑布模型
1.需求分析
2.系统和软件设计
3.实现和单元测试
4.集成和系统测试
5.运行和维护
要求早期承诺并且在实施变更时进行系统返工,很少使用
只适用于:1.嵌入式系统(硬件不灵活)2.关键性系统(对安全性进行全面分析,文档必须完整)3.大型软件系统(完整规格说明以使不同子系统独立开发)
增量式开发
先开发出一个初始版本,然后获得使用反馈并经过多个版本演化得到所需系统
优势:1.降低变更需求成本
2.更容易得到客户对已完成的开发工作的反馈意见
3.更早获得价值
劣势:1.过程不可见,难以管理和掌握进度
2.系统结构逐渐退化(定期重构)
软件过程活动
-
系统规格说明
1.需求分析:得出系统需求
2.需求规格说明:转化为文档,包括用户需求和系统需求
3.需求确认:修改文档 -
软件设计和实现
1.体系结构设计:总体结构,基本构件
2.数据库设计
3.接口设计:独立开发
4.构件选取 -
软件确认
1.构件测试
2.系统测试
3.客户测试 -
软件演化
敏捷软件开发
- 需求被表达为用户故事,可用于规划系统迭代,其主要问题是完整性。
- 及时重构避免修改时发生的自然结构退化
- 即使是敏捷开发也需要系统需求文档
需求工程
大多数系统,开始前需要一个清晰可识别的需求(除了敏捷过程)
软件测试
编程过程
- 代码静态审查:代码有哪些错误,pylint
- 代码性能分析:分析模块耗时,profile
单元测试
保证单个质量即保证整体质量
- 模块接口数据流测试
- 局部数据结构
- 边界条件
- 独立路径,计算错误、判断错误、等
- 出错处理
pyunit,mock测试
白盒测试:允许利用内部逻辑对程序所有逻辑路径测试
UML
箭头含义
① 泛化、继承:实线空心三角,子类指向父类
![image-20220113202000115](https://i-blog.csdnimg.cn/blog_migrate/fa3f9db5691dbc9135dc647c78c1b72d.png)
② 实现、接口:虚线空心三角,实现类指向接口
![image-20220113202438184](https://i-blog.csdnimg.cn/blog_migrate/9c93cfb48b1c1b7a0ec4622d38578b72.png)
③ 依赖、方法参数:虚线箭头,使用类指向被使用类
![image-20220113205450308](https://i-blog.csdnimg.cn/blog_migrate/88b1c0351e2edc7d0dd34fa5b9d7c5f4.png)
④ 关联:实线箭头,使用类指向被使用类
![image-20220113205536422](https://i-blog.csdnimg.cn/blog_migrate/0ed582fd399de5569142943d879fce4e.png)
⑤ 聚合:尾部为空心菱形的实线箭头(也可以没箭头),使用类指向被使用类
![image-20220113210027672](https://i-blog.csdnimg.cn/blog_migrate/845a3c6b725931d04ca3e951c6aa3124.png)
⑥ 组合:尾部为实心菱形的实现箭头(也可以没箭头),使用类指向被使用类
![image-20220113210140399](https://i-blog.csdnimg.cn/blog_migrate/a2607cc6f69d196055461c83ecbaccd1.png)
面向对象(OOP)
面向过程是具体化的,流程化的,解决一个问题,你需要一步一步的分析,一步一步的实现。
面向对象是模型化的,你只需抽象出一个类,这是一个封闭的盒子,拥有数据也拥有解决问题的方法。需要什么功能直接使用就可以了,不必去一步一步的实现。
封装(encapsulation)的优点:
- 良好的封装能够减少耦合
- 类内部的结构可以自由修改
- 可以对成员变量进行更精确的控制
- 隐藏信息,实现细节
三大特征
-
抽象(abstract)
使用关键字 abstract 创建抽象类和抽象方法。抽象类包含抽象方法,当一个派生类继承自该抽象类时,实现即完成。
- 不能创建一个抽象类的实例
- 抽象方法必须声明在一个抽象类内部
- 抽象方法就是没有实现的,派生类必须重写所有基类的抽象方法:
public abstract void Init();
- 通过在类定义前面放置关键字**【sealed】,可以将类声明为【密封类】**。当一个类被声明为 sealed 时,它不能被继承。抽象类不能被声明为 sealed
比如我写了一个非抽象类,但是这个类我不想让人直接实例化,而只让人继承,我就可以把他变成一个抽象类,虽然他里面并没有抽象方法。
**面向抽象编程:**通过向上转型实现。上层代码只定义规范;不需要子类就可以实现业务逻辑(正常编译);具体的业务逻辑由不同的子类实现,调用者并不关心。
-
继承(inheritance)【is-a】
继承允许我们根据一个类来定义另一个类,这使得创建和维护应用程序变得更容易。同时也有利于重用代码和节省开发时间。
当创建一个类时,程序员不需要完全重新编写新的数据成员和成员函数,只需要设计一个新的类,继承了已有的类的成员即可。这个已有的类被称为的基类,这个新的类被称为派生类。
继承的思想实现了属于(IS-A) 关系。例如,哺乳动物属于(IS-A)动物,狗属于(IS-A)哺乳动物,因此狗属于(IS-A)动物
如果子类的方法重写了基类的方法,那么子类中该方法的访问级别不允许低于基类的访问级别。这是为了确保可以使用基类实例的地方都可以使用子类实例去代替,也就是确保满足**【里氏替换原则】**。
-
多态(polymorphism)
多态是同一个行为具有多个不同表现形式或形态的能力
多态性意味着有多重形式。在面向对象编程范式中,多态性往往表现为"一个接口,多个功能",即针对某个类型的方法调用,其真正执行的方法取决于运行时期实际类型的方法;允许添加更多类型的子类实现功能扩展,却不需要修改基于基类的代码
虚方法【virtual】:当有一个定义在类中的函数需要在继承类中实现时,可以使用虚方法。虚方法可以在不同的继承类中有不同的实现。对虚方法的调用是在运行时发生的。
多态性可以是静态的或动态的:
静态多态性:通过函数重载(函数特征不同)和运算符重载实现,函数的响应在编译时发生
动态多态性:通过抽象类和虚方法(函数特征相同)实现,函数的响应在运行时发生
其他特征
-
依赖【uses-a】
一个类要需要另一个来完成它的相关任务,体现在函数参数包含另一个类的实例
-
关联(association)
有相关性。例如 :人使用计算机,人和计算机的关系。关联关系较弱
-
聚合(aggregation)【has-a】
聚合关联关系的一种特例,是强的关联关系。一个对象作为另一个对象的属性存在,整体与部分。例如:轮胎和汽车的关系。关联较强
-
组合(composition)【contains-a】
组合也是关联关系的一种特例。一个对象包含另一对象时,外部对象负责管理内部对象的生命周期的情况。关联关系最强。内部对象的创建由外部对象自己控制。外部对象不存在时,内部对象也不能存在。例如:电视机与显示器。
-
内聚与耦合(cohesion&coupling)
-
绑定
将一个方法调用同一个方法主体关联起来的过程就称作绑定。
静态绑定:绑定发生在程序运行前,如 final 和 static 关键字
动态绑定:运行时根据对象的类型自动的进行绑定,动态绑定是多态的基础。
接口
接口定义了所有类继承接口时应遵循的语法合同。接口定义了语法合同 “是什么” 部分,派生类定义了语法合同 “怎么做” 部分,【like-a】
接口定义了属性、方法和事件,这些都是接口的成员。接口只包含了成员的声明。成员的定义是派生类的责任。接口提供了派生类应遵循的标准结构。
接口使得实现接口的类或结构在形式上保持一致。
抽象类在某种程度上与接口类似,但通常当只有少数方法由基类声明由派生类实现时才使用抽象类,【如果一个抽象类没有字段,所有方法全部都是抽象方法,就可以把该抽象类改写为接口】
接口和抽象类最本质的区别:抽象类是一个不完全的类,是对对象的抽象,而接口是一种行为规范。
接口使用 interface 关键字声明,它与类的声明类似,接口声明默认是 abstract
的
如果一个接口继承其他接口,相当于扩展了接口的方法,那么实现类或结构就需要实现所有接口的成员
**优点:**提高了代码的可维护性和可扩展性,降低了代码的耦合度,不涉及任何具体的实现细节,比较安全
区别接口,抽象类,虚方法
-
接口
-
接口只提供方法规约,不提供方法体;
-
接口中的方法不能用关键字修饰;
-
接口里不能有接口和变量;
-
接口里的方法在子类中必须全部实现;
-
接口可以实现多重继承;
-
-
抽象类
-
抽象类可以从接口继承;
-
抽象类中的实体方法必须有方法体,并且在子类中不可以重写,只可以被引用,
-
抽象类中的抽象方法不可以有方法体,抽象类中的抽象方法在子类中必须重写;
-
抽象类中的虚方法在子类中可以选择性的重写;
-
-
虚方法
-
可以在子类选择性的重写;
-
不重写也可被子类调用;
-
-
接口 vs 抽象类
- 抽象类的抽象方法和接口内的方法,在子类中必须全部被实现;
- 抽象方法和接口中的方法都不可以有方法体;
- 从设计层面上看,抽象类提供了一种【is-a】 关系,需要满足里式替换原则,即子类对象必须能够替换掉所有父类对象。而接口更像是一种【like-a】关系,它只是提供一种方法实现契约,并不要求接口和实现接口的类具有【is-a】 关系;
- (JAVA)从使用上来看,一个类可以实现多个接口,但是不能继承多个抽象类;
- (JAVA)接口的字段只能是
public static final
类型的,而抽象类的字段没有这种限制; - (JAVA)接口的成员只能是
public abstract
的,而抽象类的成员可以有多种访问权限;
-
抽象方法 vs 虚方法
-
抽象方法所在的类必须是抽象类,虚方法可以在任何类里;
-
抽象方法必须被重写,虚方法的重写有选择性;
-
抽象方法不可以被子类调用,虚方法可以被子类调用;
-
抽象方法不可以有方法体,虚方法必须有方法体
-
类与结构
类的对象是存储在堆空间中,结构存储在栈中。堆空间大,但访问速度较慢,栈空间小,访问速度相对更快。故而,当我们描述一个轻量级对象的时候,结构可提高效率,成本更低。当然,这也得从需求出发,假如我们在传值的时候希望传递的是对象的引用地址而不是对象的拷贝,就应该使用类了
静态成员
当我们声明一个类成员为静态时,意味着无论有多少个类的对象被创建,只会有一个该静态成员的副本。静态变量可在成员函数或类的定义外部进行初始化,也可以在类的定义内部初始化静态变量。也可以把一个成员函数声明为 static,这样的函数只能访问静态变量,静态函数在对象被创建之前就已经存在。
命名空间
命名空间的设计目的是提供一种让一组名称与其他名称分隔开的方式。在一个命名空间中声明的类的名称与另一个命名空间中声明的相同的类的名称不冲突
【using】 关键字表明程序使用的是给定命名空间中的名称
命名空间可以被嵌套,可以使用点(.)运算符访问嵌套的命名空间的成员