学习收获
课程概述
这个学期选修了孟宁老师的高级软件工程课程,主要学习内容是《代码中的软件工程》。课程内容由浅入深,包括:
- 码农的自我修养与必备工具技能
- 工程化编程
- 需求分析和软件设计
- 设计模式和软件架构
- 软件危机和软件过程
通过课堂讲授与课后编程实践相结合的方式,让我对软件工程有了更深刻的理解和掌握。
认识的转变
以前的误解
- 认为软件就等于程序
- 软件开发就是写代码
- 掌握最新语言工具就能写好程序
现在的认识
- 软件是思想在硬件上的载体和体现
- 软件开发需要对整个过程有充分认识
- 软件工程是一套完整的问题分析与解决思维方法
- 过程控制和质量控制是软件产品开发的关键
软件工程不仅仅是一门课程,更是一种思想体系。它提供了系统化的问题分析和处理方法,其应用范围远超出课程本身的范畴,是一个综合性的问题解决方法论。
以下将对课程学习内容进行系统总结,以便后续复习。
工欲善其事必先利其器
Visual Studio Code
常用快捷键
功能 | 快捷键 |
---|---|
打开文件夹 | Ctrl/⌘+O |
关闭文件夹工作区 | Ctrl/⌘+K F |
新建文件夹 | Ctrl/⌘+N |
关闭文件 | Ctrl/⌘+W |
编辑文件和保存文件 | Ctrl/⌘+S |
文件内搜索 | Ctrl/⌘+F |
关闭所有文件 | Ctrl/⌘+K W |
关闭已保存的文件 | Ctrl/⌘+K U |
单行注释 | Ctrl+/ |
块注释 | Ctrl+Shift+/ |
文件资源管理器 | Ctrl/⌘+Shift+E |
源代码管理 | Ctrl+Shift+G |
跨文件搜索 | Ctrl/⌘+Shift+F |
启动和调试 | Ctrl/⌘+Shift+D |
查找并运行所有命令 | Ctrl/⌘+Shift+P |
查看错误和警告 | Ctrl/⌘+Shift+M |
管理扩展插件 | Ctrl/⌘+Shift+X |
切换集成终端 | Ctrl+` |
代码理解和调试桥梁
用于代码理解和调试的第三方插件与VS Code主进程之间的桥梁:
- LSP (Language Server Protocol)
- DAP (Debug Adapter Protocol)
Git
Git 本地常用命令
命令 | 说明 |
---|---|
git init | 在一个新建的目录下创建版本库 |
git status | 查看当前工作区的状态 |
git add Files | 把文件添加到暂存区 |
git commit -m "提交说明" | 把暂存区里的文件提交到仓库 |
git log | 查看当前HEAD之前的提交记录,便于回到过去 |
git reset --hard HEAD^^/HEAD-100/commit-id/commit-id的前几个字符 | 回退版本 |
git relog | 可以查看当前HEAD之后的提交记录,便于回溯 |
git reset --hard commit-id/commit-id的头几个字符 | 回退 |
git checkout 分支名 | 切换分支 |
Git 远程版本库常用命令
命令 | 说明 |
---|---|
git clone 仓库地址 | 通过clone远端的版本库从而在本地创建一个版本库 |
git fetch 仓库地址 | 下载一个远程存储库数据对象等信息到本地存储库 |
git push 仓库地址 | 将本地存储库的相关数据对象更新到远程存储库 |
git merge 分支名 | 将目标分支合并到当前分支中,并自动进行新的提交,如果不想提交,可以加 --no-commit 后缀 |
团队项目中的分叉合并
建议团队项目的每一个开发者都采用的工作流程大致如下:
- 克隆或同步最新的代码到本地存储库
- 为自己的工作创建一个分支,该分支应该只负责单一功能模块或代码模块的版本控制
- 在该分支上完成某单一功能模块或代码模块的开发工作
- 最后,将该分支合并到主分支
注意: Git默认的合并方式为快进式合并,会将分支里commit合并到主分支里,合并成一条时间线,与我们期望的呈现为一段独立的分支线段不符,因此合并时需要使用
--no-ff
参数关闭”快进式合并”
Vim
基本操作
Vim有三种主要模式:
- 命令模式
- 输入模式
- 底线命令模式
常用光标移动命令:
命令 | 说明 |
---|---|
h,j,k,l | 左下上右移动光标 |
n | 向后移动n个字符 |
0 或 Home | 移动到行首 |
$ 或 End | 移动到行尾 |
H | 移动到屏幕最上方一行 |
M | 移动到屏幕中央一行 |
L | 移动到屏幕最下方一行 |
G | 移动到文件末尾 |
nG | 移动到第n行 |
gg | 移动到文件开头 |
n | 向下移动n行 |
删除操作
命令 | 说明 |
---|---|
dd | 删除当前行 |
ndd | 删除当前行及向下n行 |
复制粘贴
命令 | 说明 |
---|---|
yy | 复制当前行 |
p | 粘贴到光标下一行 |
P | 粘贴到光标上一行 |
撤销和重做
命令 | 说明 |
---|---|
u | 撤销上一个操作 |
Ctrl+r | 重做上一个操作 |
搜索
命令 | 说明 |
---|---|
/word | 向下搜索word |
?word | 向上搜索word |
n | 下一个搜索结果 |
N | 上一个搜索结果 |
替换
替换命令格式::n1,n2s/word1/word2/g
- n1,n2: 指定行范围
- word1: 要替换的文本
- word2: 替换后的文本
- g: 全局替换标志
常用替换命令:
:1,$s/word1/word2/g
或:%s/word1/word2/g
- 全文替换:1,$s/word1/word2/gc
或:%s/word1/word2/gc
- 全文替换并确认
其他命令:
set nu
- 显示行号set nonu
- 取消行号
正则表达式
匹配模式
- 贪婪匹配:匹配最长可能的字符串
- 懒惰匹配:匹配最短可能的字符串
常用语法
语法 | 说明 |
---|---|
/ | vim中开启正则表达式 |
. * ? | | 基本元字符 |
{3,5} | 限定符 |
[a-z] [abc] [^] | 字符类 |
\d \D \w | 预定义字符类 |
*? | 开启懒惰模式 |
() | 捕获组 |
二、工程化编程实战
代码的风格原则:简明、易读、无二意性
编写高质量代码的基本方法
- 通过控制结构简化代码
- 通过数据结构简化代码
- 一定要有错误处理
- 性能优先策略背后隐藏的代价
- 拒绝修修补补,要不断重构代码
模块化软件设计
基本概念
-
模块化:在软件系统设计时保持系统内各部分相对独立,以便每一个部分可以被独立的进行设计和开发。这个做法背后的基本原理是关注点的分离。
-
耦合度:指软件模块之间的依赖程度,一般可以分为:
- 紧密耦合
- 松散耦合
- 无耦合
-
内聚度:指一个软件模块内部各种元素之间互相依赖的紧密程度。理想的内聚是功能内聚,即一个软件模块只做一件事,只完成一个主要功能点或者一个软件特性。
软件设计中的基本方法
KISS(Keep It Simple & Stupid)原则
- 一行代码只做一件事
- 一个代码块只做一件事
- 一个函数只做一件事
- 一个软件模块只做一件事
使用本地化外部接口来提高代码的适应能力,先写伪代码的代码结构更好一些。
消费者和生产者重用
消费者重用是指软件开发者在项目中重用已有的一些软件模块代码,以加快项目工作进度。
接口的基本概念
接口是互相练习的双方共同遵守的一种协议规范。在软件系统内部,一般通过定义一组API函数来约定软件模块之间的沟通方式。
接口规格的五个基本要素
- 接口的目的
- 接口使用前所需要满足的条件(前置条件或假定条件)
- 使用接口的双方遵守的协议规范
- 接口使用之后的效果(后置条件)
- 接口所隐含的质量属性
RESTful API
HTTP方法 | 用途 |
---|---|
GET | 获取资源 |
POST | 新建资源(也可以用来更新资源) |
PUT | 更新资源 |
DELETE | 删除资源 |
接口与耦合度的关系
耦合度类型
-
公共耦合(紧密)
- 软件模块之间共享数据区或变量名
- 接口定义不是通过显式调用,而是隐式共享数据区域或变量名
-
数据耦合(松散)
- 软件模块之间仅通过显式调用传递基本数据类型
-
标记耦合(松散)
- 软件模块之间通过显式调用传递复杂的数据结构(结构化数据)
- 耦合度比数据耦合高,但比公共耦合低
通用接口定义的基本方法
- 参数化上下文
- 移除前置条件
- 简化后置条件
可重入的函数不一定是线程安全的,不可重入的函数一定不是线程安全的
微服务的概念
-
由一系列独立的微服务共同组成软件系统的架构模式
-
每个微服务:
- 单独部署
- 运行在自己的进程中
- 拥有独立的运行环境和软件堆栈
-
特点:
- 为独立的业务功能开发
- 分解到最小可变产品(MVP)
- 通过RESTful API接口方式封装
- 分布式管理
- 强调隔离性
- 通过前端应用或API网关聚合各微服务
微服务架构可简单概括为:通过模块化思想垂直划分业务功能
三、从需求分析到软件设计
用例
用例的核心概念中首先它是一个业务过程,经过逻辑整理抽象出来一个业务过程,这是用例的实质。
用例的几个基本要素
- 一个用例应该由业务领域内的某个参与者所触发
- 用例必须能为特点的参与者完成一个特定的业务任务
- 一个用例必须终止于某个特定参与者,也就是特定参与者明确地或者隐含地得到了业务任务完成的结果
用例的三个抽象层级
- 抽象用例:只要用一个干什么、做什么或完成什么业务任务的动名词短语,就可以非常精简地指明一个用例。
- 高层用例:需要给用例的范围划定一个边界,也就是用例在什么时候什么地方开始,以及在什么时候什么地方结束。
- 扩展用例:需要将参与者和待开发软件系统为了完成用例所规定的业务任务的交互过程一步一步详细地描述出来,一般我们使用一个两列的表格将参与者和待开发软件系统之间从用例开始到用例结束的所有交互步骤都列举出来。
用例建模的基本步骤
- 从需求表述中找出用例,往往是动名词短语表示的抽象用例
- 描述用例开始和结束的状态,用TUCBW和TUCEW表示的高层用例
- 对用例按照子系统或不同的方面进行分类,描述用例与用例、用例与参与者之间的上下文关系,并画出用例图
- 进一步逐一分析用例与参与者的详细交互过程,完成一个两列的表格将参与者和待开发软件系统之间从用例开始到用例结束的所有交互步骤都列举出来扩展用例
准确提取用例的基本方法
-
从需求中寻找业务领域相关的动名词和动词短语,比如做什么事、什么事情必须完成、或者执行某任务等。
-
验证这些业务领域相关的动名词和动名词短语到底是不是用例。验证业务领域相关的动名词或动名词短语是不是用例的标准是满足四个必要条件:
- 它是不是一个业务过程
- 它是不是由某个参与者触发开始
- 它是不是显式地或隐式地终止于某个参与者
- 它是不是为某个参与者完成了有用的业务工作
-
在需求中识别出参与者、系统或子系统
参与者会触发某个用例开始,用例也会显式地或隐式地终止于某个参与者,用例会属于系统或子系统。
面向对象分析涉及的基本概念
对象和属性之间有依附关系,属性用来描述对象或存储对象的状态信息。
由于对象能够独立存在,那么对象的创建必须是通过显式地或隐式地调用构造过程。
继承关系
表达着两个概念之间具有概括化/具体化的关系。一个概念比另一个概念更加概括/具体。一般用三角形箭头连线表示两个类之间的继承关系。
聚合关系
聚合关系表示一个对象是另一个对象的一部分的情况。比如发动机引擎是小汽车的一部分。也被称为”部分与整体”的关系。
聚合关系使用一个平行四边形的箭头表示。
关联关系
关联关系表示继承和聚合以外的一般关系,是业务领域内特定的两个概念之间的关系。
业务领域建模的基本步骤
- 收集应用业务领域的信息。聚焦在功能需求层面,也考虑其他类型的需求和资料
- 头脑风暴,列出重要的应用业务领域概念,给出这些概念的属性,以及这些概念之间的关系
- 给这些应用业务领域概念分类。分别列出哪些是类、哪些属性和属性值、以及列出类之间的继承关系、聚合关系和关联关系
- 将结果用UML类图画出来
反范式化总结
使用双向引用来优化你的数据库架构,前提是你能接受无法原子更新的代价。
可以在引用关系中冗余数据到one端或N端。
在决定是否采用反范式化时需要考虑下面的因素:
- 你将无法对冗余的数据进行原子更新
- 只有读写比比较高的情况下才应该采取反范式化的设计
瀑布模型
最基本的过程模型,它把整个软件过程按顺序划分成了需求、设计、编码、测试和部署五个阶段。
统一过程的核心要义是用例驱动、以架构为中心、增量且迭代的过程。用例驱动就是我们前文中用例建模得到的用例作为驱动软件开发的目标;以架构为中心的架构是后续软件设计的结果,就是保持软件架构相对稳定,减小软件架构层面的重构造成的混乱。
敏捷统一过程的四个关键步骤
- 确定需求
- 通过用例的方式来满足这些需求
- 分配这些用例到各增量阶段
- 具体完成各增量阶段所计划的任务
敏捷统一过程的增量阶段
在每一次增量阶段的迭代中,都要进行从需求分析到软件设计实现的过程,具体敏捷统一过程将增量阶段分为五个步骤:
- 用例建模
- 业务领域建模
- 对象交互建模
- 形成设计类图
- 软件的编码实现和软件应用部署
对象交互建模的基本步骤
- 找出关键步骤进行剧情描述
- 将剧情描述转换成剧情描述表
- 将剧情描述表转换成序列图
四、软件科学基础理论
软件的基本结构
顺序结构
最简单的程序结构,也是最常用的程序结构。只要按照解决问题的顺序写出相应的语句就行,它的执行顺序是自上而下,依次执行。
分支结构
在顺序结构的基础上,利用影响标志寄存器上标志位的质量和跳转质量组合起来,借助于标志寄存器或特定寄存器暂存条件状态实现分支结构。
循环结构
顺序结构和分支结构的组合形成的更为复杂的程序结构,是指在程序中需要反复执行某个功能而设置的一种程序结构。
回调函数
回调函数是一个面向过程的概念,是代码执行过程的一种特殊流程。回调函数就是一个通过函数指针调用的函数。把函数的指针(地址)作为参数传递给另一个函数,当这个指针调用其所指向的函数时,就称这是回调函数。
设计模式的组成部分
- 设计模式的名称
- 设计模式的目的(即要解决什么问题)
- 设计模式的解决方案
- 设计模式的解决方案有哪些约束和限制条件
设计模式的分类
根据模式是主要用于类上还是主要用于对象上来划分,可以分为:
类模式
用于处理类与子类之间的关系,这些关系通过继承来建立,是静态的,在编译时刻便确定下来了。
对象模式
用于处理对象之间的关系,这些关系可以通过组合或聚合来实现,在运行时刻是可以变化的,更具动态性。
由于组合关系或聚合关系比继承关系耦合度低,因此多数设计模式都是对象模式。
常用的设计模式
创建型模式
- 单例模式:某个类只能生产一个实例,该类提供了一个全局访问点供外部获取该实例,典型的应用如数据库实例。
- 原型模式:将一个对象作为原型,通过对其进行复制而克隆出多个和原型类似的新实例。
- 建造者模式:将一个复杂对象分解成多个相对简单的部分,然后根据不同需要分别创建它们,最后构建成该复杂对象。
结构型模式
- 代理模式:为某对象提供一种代理以控制对该对象的访问。
- 适配器模式:将一个类的接口转换成客户希望的另外一个接口。
- 装饰(Decorator)模式:在不改变现有对象结构的情况下,动态地给对象增加一些职责。
- 外观(Facade)模式:为复杂的子系统提供一个一致的接口。
- 享元(Flyweight)模式:运用共享技术来有效地支持大量细粒度对象的复用。
行为型模式
- 策略模式:定义了一系列算法,并将每个算法封装起来。
- 命令模式:将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分隔开。
- 模板方法:定义一个操作的算法骨架,而将算法的一些步骤延迟到子类中。
- 职责链:将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链。
- 中介者:定义一个中介对象来简化原有对象之间的交互关系。
设计模式背后的设计原则
- 开闭原则
- Liskov替换原则
- 依赖倒置原则
- 单一职责原则
- 迪米特法则
- 合成复用原则
软件架构模型
软件架构模型是在高层抽象上对系统中关键要素的描述,而且表现出抽象的层次结构。构建软件架构模型的基本方法就是在不同层次上分解系统并抽象出其中的关键要素。
分解的常见方法
- 面向功能的分解方法:用例模型即是一种面向功能的分解方法
- 面向特征的分解方法:根据系统显著特征在不同抽象层次上划分模块
- 面向数据的分解方法:在业务领域建模中形成概念业务数据模型
- 面向并发的分解方法:将系统分解到不同的并发任务中
- 面向事件的分解方法:处理大量事件和状态转换关系
- 面向对象的分解方法:基于系统中抽象的对象元素进行分解
软件架构风格与策略
常见架构风格
-
管道-过滤器风格:面向数据流的软件体系结构,典型应用是编译系统。
-
客户机-服务器结构:客户代码通过请求和应答方式访问服务代码。
- 函数调用和返回值
socket
中的send
和recv
HTTP
中的get
请求和响应
-
P2P结构:每个构件既是客户端又是服务端,形成对等网络结构。
-
发布-订阅:
- 两类构件:发布者和订阅者
- 事件触发通知机制
- 观察者模式是典型实现
-
CRUD架构:
- Create(创建)
- Read(读取)
- Update(更新)
- Delete(删除) 适用于ERP、CRM等信息系统。
-
层次化架构:从垂直纵深角度组织软件单元,形成服务层次。
MVC结构
核心组成
-
模型(Model)
- 封装应用程序状态
- 响应状态查询
- 实现应用程序功能
- 通知视图改变
-
视图(View)
- 解释模型
- 处理模型更新请求
- 发送用户输入给控制器
- 允许控制器选择视图
-
控制器(Controller)
- 定义应用程序行为
- 用户动作映射成模型更新
- 选择响应的视图
软件架构的描述方法
- 分解视图:勾画系统结构层次
- 依赖视图:展现模块间依赖关系
- 泛化视图:展现模块间一般化或具体化关系
- 执行视图:展示系统运行时序结构
- 实现视图:描述架构与源文件映射关系
- 部署视图:建立执行实体与计算资源映射
- 工作分配视图:分解独立工作任务
高质量软件的特征
质量维度
- 外部质量:用户视角的产品质量(功能正确性、故障数量等)
- 内部质量:开发者视角的产品质量(代码质量)
关键质量属性
- 易于修改维护
- 良好的性能表现
- 安全性
- 可靠性
- 健壮性
- 易用性
- 商业目标达成
五、软件危机和软件过程
软件危机
- 根本问题:软件概念结构的复杂性
- 核心挑战:无法达成软件概念的完整性和一致性
软件生命周期
- 分析
- 设计
- 实现
- 交付
- 维护
团队的基本要素
- 团队规模
- 团队的凝聚力
- 团队协作的基本条件
敏捷宣言
- 个体和互动 > 流程和工具
- 工作的软件 > 详尽的文档
- 客户合作 > 合同谈判
- 响应变化 > 遵循计划
构件软件产品
驱动代码
调用或触发特定软件模块执行的测试程序,通过测试用例模拟模块执行上下文。
桩代码
模拟缺失软件模块的特殊程序,使依赖模块能正常测试执行。