参考资料https://gitee.com/mengning997/se
第一章、代码中的软件工程
软件工程瀑布模型
需求分析:分析现有软件, 用你们写的软件的用户量来证明你了解用户的需求
设计阶段:用快速发布来证明设计是有效的, 能适应变化的。
实现阶段:用各种软件工程的衡量手段来证明大家实现的能力。
稳定阶段:证明测试能否覆盖代码的大部分。
发布阶段: 如期发布, 用户量, 用户评价。
维护阶段:网上的观众或下一个年级的同学能很愿意接手你们的软件。
第二章、必备技能
1、vscode
简单使用
打开文件夹( Ctrl/⌘+O)和关闭文件夹工作区( Ctrl/⌘+K F)
新建文件(Ctrl/⌘+N)、关闭文件(Ctrl/⌘+W)、编辑文件和保存文件(Ctrl/⌘+S)
文件内搜索(Ctrl/⌘+F)
关闭所有文件(Ctrl/⌘+K W)
关闭已保存的文件(Ctrl/⌘+K U)
Ctrl+/用于单行代码注释和取消注释,Ctrl+Shift+A用于代码块注释和取消注释
文件资源管理器(Ctrl/⌘+Shift+E)
跨文件搜索 (Ctrl+Shift+F)
源代码管理 (Ctrl+Shift+G)
启动和调试 (Ctrl/⌘+Shift+D)
管理扩展插件 (Ctrl/⌘+Shift+X)
显示命令面板 (Ctrl/⌘+Shift+P)
显示问题面板 (查看错误和警告)(Ctrl/⌘+Shift+M)
显示集成终端 (Ctrl+`)
快捷键
2、Git
1、git本地版本库的基本用法
git init # 初始化一个本地版本库
git status # 查看当前工作区(workspace)的状态
git add [FILES] # 把文件添加到暂存区(Index)
git commit -m "wrote a commit log infro” # 把暂存区里的文件提交到仓库
git log # 查看当前HEAD之前的提交记录,便于回到过去
git reset —hard HEAD^^/HEAD~100/commit-id/commit-id的头几个字符 # 回退
git reflog # 可以查看当前HEAD之后的提交记录,便于回到未来
git reset —hard commit-id/commit-id的头几个字符 # 回退
要是在本地对源代码进行基本的版本控制,主要通过git add和git commit -m提交版本,有了提交记录之后可以灵活地将当前工作区里的源代码回退到过去的某个版本,也就是回到过去。回到过去之后,也有可能发现之前撤销的某个版本是有价值的,希望找回来,这就需要回到未来。过去和未来之间的分界点就是HEAD,即当前工作区所依赖的版本。
2、git远程版本库的基本用法
git clone命令官方的解释是“Clone a repository into a new directory”,即克隆一个存储库到一个新的目录下。
git fetch命令官方的解释是“Download objects and refs from another repository”,即下载一个远程存储库数据对象等信息到本地存储库。
git push命令官方的解释是“Update remote refs along with associated objects”,即将本地存储库的相关数据对象更新到远程存储库。
git merge命令官方的解释是“Join two or more development histories together”,即合并两个或多个开发历史记录。
git pull命令官方的解释是“Fetch from and integrate with another repository or a local branch”,即从其他存储库或分支抓取并合并到当前存储库的当前分支。
3、git的设计理念和时间线
line diff是形成增量补丁的技术方法,即一个文件按行对比(line diff)将差异的部分制作成一个增量补丁。
commit是存储到仓库里的一个版本,是整个项目范围内的一个或多个文件的增量补丁合并起来,形成项目的增量补丁,是一次提交记录。每个提交(commit)都生成一个唯一的commit ID。
branch是按时间线依次排列的一组提交记录(commit),理论上可以通过当前branch上最初的提交(commit)依次打补丁直到HEAD得到当前工作区里的源代码。
tag标签就是某次提交(commit)的commit ID的别名
4、团队的分叉与合并
建议团队项目的每一个开发者都采用的工作流程大致如下:
1 克隆或同步最新的代码到本地存储库;
2 为自己的工作创建一个分支,该分支应该只负责单一功能模块或代码模块的版本控制;
3 在该分支上完成某单一功能模块或代码模块的开发工作;
4 最后,将该分支合并到主分支。
5、合并方法
默认的合并方式为"快进式合并"(fast-farward merge),会将分支里commit合并到主分支里,合并成一条时间线,与我们期望的呈现为一段独立的分支线段不符,因此合并时需要使用--no-ff参数关闭"快进式合并"(fast-farward merge)。
一、克隆或同步最新的代码到本地存储库
git clonehttps://DOMAIN_NAME/YOUR_NAME/REPO_NAME.git
git pull
二、为自己的工作创建一个分支,该分支应该只负责单一功能模块或代码模块的版本控制;
git checkout -b mybranch
git branch
三、在该分支上完成某单一功能模块或代码模块的开发工作;多次进行如下操作:
git add FILES
git commit -m "commit log"
四、最后,先切换回master分支,将远程origin/master同步最新到本地存储库,再合并mybranch到master分支,推送到远程origin/master之后即完成了一项开发工作。
git checkout master
git pull
git merge --no-ff mybranch
git push
6、Git Rebase
一般我们在软件开发的流程中,有一个朴素的版本管理哲学:开发者的提交要尽量干净、简单。开发者要把自己的代码修改按照功能拆分成一个个相对独立的提交,一个提交对应一个功能点,而且要在对应的 commit log message 里面描述清楚。因此在合并和 push 之前检查修改一下 commit 记录时常需要。
团队项目工作流程中增加一步Git Rebase,即在mybranch分支上完成自己的工作之后,为了让 log 记录将来更容易回顾参考,用 git rebase 重新整理一下提交记录。注意不要通过rebase对任何已经提交到远程仓库中的commit进行修改。
git rebase命令格式大致如下:
git rebase -i [startpoint] [endpoint]
其中-i的意思是--interactive,即弹出交互式的界面让用户编辑完成合并操作,[startpoint] [endpoint]则指定了一个编辑区间,如果不指定[endpoint],则该区间的终点默认是当前分支的HEAD。
一般只指定[startpoint] ,即指定从某一个commit节点开始,可以使用HEAD^^、HEAD~100、commit ID或者commit ID的头几个字符来指定一个commit节点,比如下面的代码指定重新整理HEAD之前的三个commit节点。
$ git rebase -i HEAD^^^
git rebase —abort
git rebase --continue
7、Fork + Pull request
为了解决开源社区松散团队的协作问题,Github提供了Fork+ Pull request的协作开发工作流程。
当你想更正别人仓库里的Bug或者向别人仓库里贡献代码时,要走Fork+ Pull request的协作开发工作流程:
1 先 fork(分叉) 别人的仓库,相当于拷贝一份;
2 做一些 bug fix或其他的代码贡献;
3 发起 Pull request 给原仓库;
4 原仓库的所有者 review Pull request,如果没有问题的话,就会 merge Pull request 到原仓库中。
3、Vim
1、安装Vim
It is included as "vi" with most UNIX systems and with Apple OS X.
使用VSCode则可以在Linux、Windows和OS X上都能使用Vim
在VSCode中Ctrl/⌘+Shift+X管理扩展插件中搜索vim即可安装使用
命令模式(Command mode),用户刚刚启动vi/vim,便进入了命令模式。此状态下敲击键盘动作会被vim识别为命令,而非输入字符。比如我们此时按下i,并不会输入一个字符,i被当作了一个命令。命令模式只有一些最基本的命令,因此仍要依靠底线命令模式输入更多命令
输入模式(Insert mode),在命令模式下按下i就进入了输入模式,按ESC退出输入模式,切换到命令模式。
底线命令模式(Last line mode),在命令模式下按下:(英文冒号)就进入了底线命令模式。底线命令模式可以输入单个或多个字符的命令,可用的命令非常多。基本的命令有q(退出程序)、w(保存文件)等。按ESC键可随时退出底线命令模式。
2、三种模式之间的切换
•命令模式(Command mode),用户刚刚启动vi/vim,便进入了命令模式。此状态下敲击键盘动作会被vim识别为命令,而非输入字符。比如我们此时按下i,并不会输入一个字符,i被当作了一个命令。命令模式只有一些最基本的命令,因此仍要依靠底线命令模式输入更多命令
•输入模式(Insert mode),在命令模式下按下i就进入了输入模式,按ESC退出输入模式,切换到命令模式。
•底线命令模式(Last line mode),在命令模式下按下:(英文冒号)就进入了底线命令模式。底线命令模式可以输入单个或多个字符的命令,可用的命令非常多。基本的命令有q(退出程序)、w(保存文件)等。按ESC键可随时退出底线命令模式。
3、移动光标的基本方法
h 或 向左箭头键(←) 光标向左移动一个字符
j 或 向下箭头键(↓) 光标向下移动一个字符
k 或 向上箭头键(↑) 光标向上移动一个字符
l 或 向右箭头键(→) 光标向右移动一个字符
如果你将右手放在键盘上的话,你会发现 hjkl 是排列在一起的,因此可以使用这四个按钮来移动光标。 如果想要进行多次移动的话,例如向下移动 30 行,可以使用 "30j" 或 "30↓" 的组合按键, 亦即加上想要进行的次数(数字)后,按下动作即可!
4、移动光标的更多方法
n 那个 n 表示『数字』,例如 20 。按下数字后再按空格键,光标会向右移动这一行的 n 个字符。例如 20 则光标会向后面移动 20 个字符距离。
0 或功能键[Home] 这是数字『 0 』:移动到这一行的最前面字符处 (常用)
$ 或功能键[End] 移动到这一行的最后面字符处(常用)
H 光标移动到这个屏幕的最上方那一行的第一个字符
M 光标移动到这个屏幕的中央那一行的第一个字符
L 光标移动到这个屏幕的最下方那一行的第一个字符
G 移动到这个档案的最后一行(常用)
nG n为数字。移动到这个档案的第 n 行。例如 20G 则会移动到这个档案的第 20 行
gg 移动到这个档案的第一行,相当于 1G 啊! (常用)
n n 为数字。光标向下移动 n 行(常用)
5、删除
x, X 在一行字当中,x 为向后删除一个字符 (相当于 [del] 按键), X 为向前删除一个字符(相当于 [backspace] 亦即是退格键) (常用)
nx n 为数字,连续向后删除 n 个字符。举例来说,我要连续删除 10 个字符, 『10x』。
dd 删除游标所在的那一整行(常用)
ndd n 为数字。删除光标所在的向下 n 行,例如 20dd 则是删除 20 行 (常用)
d1G 删除光标所在到第一行的所有数据
dG 删除光标所在到最后一行的所有数据
d$ 删除游标所在处,到该行的最后一个字符
d0 那个是数字的0 ,删除游标所在处,到该行的最前面一个字符
6、复制与粘贴
yy 复制游标所在的那一行(常用)
p, P p为将已复制的数据在光标下一行贴上,P则为贴在游标上一行! 举例来说,我目前光标在第 20 行,且已经复制了 10 行数据。则按下 p 后, 那 10 行数据会贴在原本的 20 行之后,亦即由 21 行开始贴。但如果是按下 P 呢? 那么原本的第 20 行会被推到变成 30 行。 (常用)
nyy n 为数字。复制光标所在的向下 n 行,例如 20yy 则是复制 20 行(常用)
y1G 复制游标所在行到第一行的所有数据
yG 复制游标所在行到最后一行的所有数据
y0 复制光标所在的那个字符到该行行首的所有数据
y$ 复制光标所在的那个字符到该行行尾的所有数据
7、复原和重做
u 复原前一个动作。(常用)
[Ctrl]+r 重做上一个动作。(常用)
这个 u 与 [Ctrl]+r 是很常用的指令!一个是复原,另一个则是重做一次
8、自动化执行宏命令
J 将光标所在行与下一行的数据结合成同一行,2Jj则把下面的2行合并为一行并将光标下移一行
在normal mode下q[a-z]开始录制宏命令,再次按q结束宏命令定义。
qa2Jjq,q-开始录制宏;a-宏的编号是a,最后一个q-结束宏定义
3@a,执行三次a宏
9、基本搜索
/word 向光标之下寻找一个名称为 word 的字符串。例如要在档案内搜寻 vbird 这个字符串,就输入 /vbird 即可! (常用)
?word 向光标之上寻找一个字符串名称为 word 的字符串。
n 这个 n 是英文按键。代表重复前一个搜寻的动作。举例来说, 如果刚刚我们执行 /vbird 去向下搜寻 vbird 这个字符串,则按下 n 后,会向下继续搜寻下一个名称为 vbird 的字符串。如果是执行 ?vbird 的话,那么按下 n 则会向上继续搜寻名称为 vbird 的字符串!
N 这个 N 是英文按键。与 n 刚好相反,为『反向』进行前一个搜寻动作。 例如 /vbird 后,按下 N 则表示『向上』搜寻 vbird 。
使用 /word 配合 n 及 N 是非常有帮助的!可以让你重复的找到一些你搜寻的关键词!
10、基本搜索替换
:n1,n2s/word1/word2/g n1 与 n2 为数字。在第 n1 与 n2 行之间寻找 word1 这个字符串,并将该字符串取代为 word2 !举例来说,在 100 到 200 行之间搜寻 vbird 并取代为 VBIRD 则:『:100,200s/vbird/VBIRD/g』。(常用)
s是substitute的简写,表示执行替换字符串操作
g(global)表示全局替换;c(comfirm)表示操作时需要确认;i(ignorecase)表示不区分大小写
:1,s/word1/word2/g 或 :%s/word1/word2/g 从第一行到最后一行寻找 word1 字符串,并将该字符串取代为 word2 !(常用)
:1,s/word1/word2/gc 或 :%s/word1/word2/gc 从第一行到最后一行寻找 word1 字符串,并将该字符串取代为 word2 !且在取代前显示提示字符给用户确认 (confirm) 是否需要取代!(常用)
11、切换到编辑模式
i, I 进入输入模式(Insert mode):
i 为『从目前光标所在处输入』, I 为『在目前所在行的第一个非空格符处开始输入』。 (常用)
a, A 进入输入模式(Insert mode):
a 为『从目前光标所在的下一个字符处开始输入』, A 为『从光标所在行的最后一个字符处开始输入』。(常用)
o, O 进入输入模式(Insert mode):
这是英文字母 o 的大小写。o 为『在目前光标所在的下一行处输入新的一行』; O 为在目前光标所在处的上一行输入新的一行!(常用)
r, R 进入取代模式(Replace mode):
r 只会取代光标所在的那一个字符一次;R会一直取代光标所在的文字,直到按下 ESC 为止;(常用)
[Esc] 退出编辑模式,回到一般模式中(常用)
编辑模式在vi画面的左下角处会出现『--INSERT--』或『--REPLACE--』的字样
12、命令行模式
:w 将编辑的数据写入硬盘档案中(常用)
:w! 若文件属性为『只读』时,强制写入该档案。不过,到底能不能写入, 还是跟你对该档案的档案权限有关啊!
:w [filename] 将编辑的数据储存成另一个档案(类似另存新档)
:q 离开 vi (常用)
:q! 若曾修改过档案,又不想储存,使用 ! 为强制离开不储存档案。
注意一下啊,那个惊叹号 (!) 在 vi 当中,常常具有『强制』的意思~
:wq 储存后离开,若为 :wq! 则为强制储存后离开 (常用)
13、代码中批量添加注释
批量注释:Ctrl + v 进入块选择模式,然后移动光标选中你要注释的行(VSCode可以鼠标选择代码块),再按大写的 I 进入行首插入模式输入注释符号如 // 或 #,输入完毕之后,按两下 ESC,Vim 会自动将你选中的所有行首都加上注释,保存退出完成注释。
取消注释:Ctrl + v 进入块选择模式,选中你要删除的行首的注释符号,注意 // 要选中两个,选好之后按 d 即可删除注释,ESC 保存退出。
批量注释:使用下面命令在指定的行首添加注释。使用命令格式: :起始行号,结束行号s/^/注释符/g(注意冒号),如:10,20s#^#//#g,:10,20s/^/#/g
取消注释:使用名命令格式: :起始行号,结束行号s/^注释符//g(注意冒号),如:10,20s#^//##g,:10,20s/#//g
14、命令行环境下vim的配置
:set nu 显示行号,设定之后,会在每一行的前缀显示该行的行号
:set nonu 与 set nu 相反,为取消行号!
4、正则
1、基本概念
正则表达式是对字符串操作的一种逻辑公式。
正则表达式的应用范围非常之广泛,最初是由Unix普及开来的,后来在广泛运用于Scala 、PHP、C# 、Java、C++ 、 Objective-c、Perl 、Swift、VBScript 、Javascript、Ruby 以及Python等等
使用正则表达式的原因
测试字符串内的模式。例如,可以测试输入字符串,以查看字符串内是否出现电话号码模式或信用卡号码模式。这称为数据验证。
替换文本。可以使用正则表达式来识别文档中的特定文本,完全删除该文本或者用其他文本替换它。
基于模式匹配从字符串中提取子字符串。可以查找文档内或输入域内特定的文本。
例如,您可能需要搜索整个网站,删除过时的材料,以及替换某些 HTML 格式标记。在这种情况下,可以使用正则表达式来确定在每个文件中是否出现该材料或该 HTML 格式标记。此过程将受影响的文件列表缩小到包含需要删除或更改的材料的那些文件。然后可以使用正则表达式来删除过时的材料。最后,可以使用正则表达式来搜索和替换标记。
2、基本的字符串搜索方法
在VS Code中跨文件搜索(Ctrl+Shift+F)和文件内搜索(Ctrl+F)的输入框输入字符串即可进行基本的字符串搜索。
文件内搜索(Ctrl+F)可以使用Enter键代表继续搜索下一个,Shift+Enter键代表继续搜索上一个。这两个操作在文件内搜索面板上有对应的两个上下箭头,箭头右侧三条横线的小按钮是指在选定内容中查找(Alt+L)。跨文件搜索(Ctrl+Shift+F)和文件内搜索(Ctrl+F)的输入框内部右侧都有三个小按钮:
第一个选中的话就是搜索时区分大小写(Alt+C);
第二个选中的话表示搜索时全字匹配(Alt+W);
第三个选中的话表示搜索时采用正则表达式(Alt+R),如果输入框中使用了正则表达式的语法规则,则需要选中第三个按钮或使用Alt+R快捷键。显然上图中还没有使用正则表达式。
在 Vim 一般命令模式(Normal Mode)下输入/word 向光标之下寻找一个名称为 word 的字符串。例如要在档案内搜寻 vbird 这个字符串,就输入 /vbird 即可! (常用)
?word 向光标之上寻找一个字符串名称为 word 的字符串。
n 这个 n 是英文按键。代表重复前一个搜寻的动作。举例来说, 如果刚刚我们执行 /vbird 去向下搜寻 vbird 这个字符串,则按下 n 后,会向下继续搜寻下一个名称为 vbird 的字符串。如果是执行 ?vbird 的话,那么按下 n 则会向上继续搜寻名称为 vbird 的字符串!
N 这个 N 是英文按键。与 n 刚好相反,为『反向』进行前一个搜寻动作。 例如 /vbird 后,按下 N 则表示『向上』搜寻 vbird 。
使用 /word 配合 n 及 N 是非常有帮助的!可以让你重复的找到一些你搜寻的关键词
3、同时搜索多个字符串的方法
在VS Code跨文件搜索(Ctrl+Shift+F)或文件内搜索(Ctrl+F)时,只要将多个字符串之间增加或运算符“|”,比如"main|int" ,同时选中输入框最右侧使用正则表达式(Alt+R)的小图标,即可同时搜索多个字符串。
在 Vim 中搜索多个字符串的用法基本一致。如果你想匹配"yes"或"no",你需要的正则表达式是/yes|no
你也可以搜索超过两种模式,通过添加更多的模式来添加更多的或运算符来分隔它们,如/yes|no|maybe
4、在匹配字符串时的大小写问题
在VS Code跨文件搜索(Ctrl+Shift+F)和文件内搜索(Ctrl+F)中,默认是忽略大小写的,只有通过选中搜索输入框中区分大小写(Alt+C)小按钮,才会按照字符串的大小写严格匹配。
在 Vim 中通过底线命令方式:set ignorecase 设置为忽略大小写;通过:set noignorecase 恢复到大小写敏感的状态,Vim 环境下默认是大小写敏感的。
Unix类的系统默认都是大小写敏感的,而Windows系统下默认是大小写不敏感的。这大概是VS Code和Vim在大小写的默认设置上不同的原因吧。
在 Vim 中也可以通过快捷方式\c 表示大小写不敏感,\C 表示大小写敏感,比如/ignorecase\c,这个正则表达式可以匹配”ignorecase”,"igNoreCase"和"IgnoreCase"。
在VS Code跨文件搜索(Ctrl+Shift+F)和文件内搜索(Ctrl+F)中不支持在正则表达式中使用\c这种快捷方式
5、通配符的基本用法
通配符“.”将匹配任意一个字符。通配符也可称为 dot 和 period。你可以像正则表达式中的任何其他字符一样使用通配符。例如,如果你想匹配“hug”,“huh”,“hut”和“hum”,可以使用正则表达式hu.来匹配这所有四个字符串。
通配符“+”用来查找出现一次或多次的字符,例如hahhhhh,可以使用正则表达式hah+来匹配。
通配符“* ”匹配零次或多次出现的字符,使用正则表达式hah*来匹配,还可以匹配ha字符串。
通配符“?”指定可能存在的元素,也就是检查前一个元素存在与否,如正则表达式colou?r、favou?rite中通配符“?”前面的u字符存在和不存在两种情况的字符串都会匹配。
简要总结一下通配符“.”表示任意一个字符;“?”表示前一个字符是否存在,也就是存在 0 次或 1 次;“+”表示前一个字符出现一次或多次;“*”表示前一个字符出现 0 次、1 次或多次。
如果是指定只查找某个字符出现3次到5次的情况怎么办呢?可以使用 quantity specifiers 数量说明符指定模式的下限和上限数。数量说明符使用大括号{and}。你将两个数字放在大括号之间用逗号“,”隔开表示上限和下限数。
要匹配字符串"aaah"中出现 3 到 5 次的 a,你的正则表达式将是a{3,5}h;
仅匹配字符串"haaah"与至少出现 3 次的字母 a,正则表达式将是/ha{3,}h;
为了仅匹配"hah"中出现 3 次的字母 a,你的正则表达式将是/ha{3}h。
6、匹配具有多种可能性的字符集
[]
方括号[and]中来定义一组你希望匹配的字符。character sets 字符集允许你通过将其放在方括号[and]中来定义一组你希望匹配的字符。例如,你要匹配"bag","big"和"bug",而不是"bog"。你可以创建正则表达式/b[aiu]g 来执行此操作。[aiu]是只匹配字符"a","i"或"u"的 character sets 字符集。
[ - ]
在 character sets 字符集中,你可以使用连字符“-”定义要匹配的字符范围。例如,要匹配小写字母 a 到 e,你将使用[a-e]。使用连字符“-”匹配一系列字符并不只限于字母,它也可以匹配一系列数字。例如character sets 字符集[0-5]匹配 0 和 5 之间的所有数字,包括 0 和 5。
字符“^”定义不想要匹配的字符,称为negated character sets 否定字符集。要创建一个 否定字符集,你可以在方括号的开括号之后放置一个插入字符“^”。例如[^aeiou]排除元音的所有字符。
如果匹配的可能的字符太多,写起来不是很方便,因而字符集还提供快捷方式的写法。
快捷方式\w 匹配字母数字[A-Za-z0-9_ ]。这个character sets 字符集匹配大小写字母加数字。注意,这个character sets 字符集还包括下划线字符“_”。
快捷方式\W 搜索\w 的相反方向。需要注意相反的模式使用大写字母。此快捷方式与 [^A-Za-z0-9_]相同。
快捷方式\d 搜索数字字符集[0-9]。
快捷方式\D查找非数字字符,等于字符集[^0-9]。
7、贪婪匹配 默认 vs. 懒惰匹配 ?
在正则表达式中,greedy 贪婪匹配找到符合正则表达式模式的字符串的最长可能部分,并将其作为匹配返回。相反还有 lazy 懒惰匹配,是找到符合正则表达式模式的字符串的最小可能部分。你可以将正则表达式t[a-z]i应用于字符串"titanic"。这个正则表达式基本上是以 t 开始的模式,以 i 结尾,并且之间有0个、1个或多个字母。
正则表达式是默认的是 greedy 贪婪匹配,所以匹配将返回"titani"。它可以找到最大的子字符串,以符合该模式。
但是可以使用?字符将其更改为 lazy 懒惰匹配。“titanic”匹配调整后的t[a-z]?i正则表达式会返回["ti"]。注意这时字符“?”表示 lazy 懒惰匹配,字符“?”还可以作为通配符表示检查前一个元素存在与否。
8、一些特殊位置和特殊字符
插入字符“^”用于表示字符串的开头。
美元字符“$”表示字符串的末尾。
如在"Ricky is first and can be found”查找开头的 Ricky 则为^Ricky,查找结尾的 found 则为/found$。
可以使用\s 搜索空格,这是一个小写的 s 即 space 之意。此模式不仅匹配空格,还包括回车、制表符、换页和新行字符。你可以将其看作与字符集[\r\t\f\n\v]类似。
使用\S 搜索非空格,这是一个大写的 S。此模式将不匹配空格、回车符、制表符、换页和新行字符。你可以想象它类似于字符类[^\r\t\f\n\v]。
\n:换行(光标到下行行首);
\r:回车(光标到本行行首);
\f:换页;
\t:水平跳格(水平制表);
\v:垂直跳格(垂直制表)。
9、使用捕获组复用模式
可能搜索的某些模式在字符串中多次出现,手动重复这些正则表达式是浪费时间的。有一个更好的方法可在你的字符串中有多个重复子串时进行指定,那就是capture groups 捕获组。
用括号(and)可以定义capture groups 捕获组,用于查找重复的子串,即把会重复的模式的正则表达式放在括号内。
要指定重复字符串的出现位置,可以使用反斜杠“\”,然后使用数字。该数字从 1 开始,并随着用括号定义的捕获组数量而增加。比如\1 来匹配前面通过括号定义的第一个捕获组。
使用 capture groups 捕获组来匹配字符串中连续出现三次的数字,每个数字由空格分隔,如(\d+)\s\1\s\1。
10、基本的字符串搜索替换方法
在VS Code中基本的字符串搜索替换方法比较简单,只要点击查找输入框左侧的“>”小按钮就可以打开替换输入框,如下图所示。也可以使用快捷键跨文件替换(Ctrl+Shift+H)和文件内替换(Ctrl+H),与跨文件搜索(Ctrl+Shift+F)和文件内搜索(Ctrl+F)相对应。
在 Vim 中基本的字符串搜索替换方法为:n1,n2s/word1/word2/g,以:开头,n1 与 n2 为数字,即在第 n1 与 n2 行之间寻找 word1 这个字符串,并将该字符串取代为 word2 字符串。举例来说,在 100 到 200 行之间搜寻 regex 并取代为 RegEx 则为:100,200s/regex/RegEx/g。
其中 s 是 substitute 的简写,表示执行替换字符串操作;最后的/g 是 global 的简写,表示全局替换。另外与/g 的用法相似,/c 是 comfirm 的简写,表示操作时需要确认;/i 是 ignorecase 的简写,表示不区分大小写。
:1,$s/word1/word2/g 或 :%s/word1/word2/g 从第一行到最后一行寻找 word1 字符串,并将该字符串取代为 word2 字符串。
:1,$s/word1/word2/gc 或 :%s/word1/word2/gc 从第一行到最后一行寻找 word1 字符串,并将该字符串取代为 word2 字符串,且在取代前显示提示信息给用户确认 (confirm) 是否需要取代。
11、复用捕获组的方式进行替换
如果我们在搜索替换中希望保留搜索字符串中的某些字符串作为替换字符串的一部分,可以使用美元符号$访问替换字符串中的捕获组。
比如在搜索正则表达式中的捕获组为(capture groups),则替换的正则表达式中可以直接使用$1复用搜索正则表达式中的捕获组为(capture groups)。
在VS Code中,如果想将项目中所有的HTML标题标签中h改为大写H,搜索正则表达式
替换的正则表达式
使用1复用了搜索正则表达式中定义的捕获组(\d)。
在Vim中,复用捕获组的方式进行替换的用法为:1,s/(capture groups)/1/g
如果想在当前文件中将所有的HTML标题标签中h改为大写H则正则表达式为:
1,$s/
第三章、从需求分析到软件设计
需求分析就是需求分析师对用户期望的软件行为进行表述,并进一步用对象或实体的状态、属性和行为来定义需求。
1.对需求进行分析和建模
原型化方法(Prototyping)和建模的方法(Modeling)
用例建模(Use Case Modeling)
面向对象分析涉及的基本概念
业务领域建模(Domain Modeling)
关联类及关系数据模型
关系数据模型的MongoDB设计与实现
需求分析的两类基本方法
原型化方法(Prototyping)和建模的方法(Modeling)是整理需求的两类基本方法。
原型化方法可以很好地整理出用户接口方式(UI,User Interface),比如界面布局和交互操作过程。
建模的方法可以快速给出有关事件发生顺序或活动同步约束的问题,能够在逻辑上形成模型来整顿繁杂的需求细节。
用例
用例(Use Case)的核心概念中首先它是一个业务过程(business process),经过逻辑整理抽象出来的一个业务过程,这是用例的实质。什么是业务过程?在待开发软件所处的业务领域内完成特定业务任务(business task)的一系列活动就是业务过程。
用例的几个基本要素
A use case is initiated by (or begins with) an actor. 一个用例应该由业务领域内的某个参与者(Actor)所触发。
A use case must accomplish a business task (for the actor).用例必须能为特定的参与者完成一个特定的业务任务。
A use case must end with an actor. 一个用例必须终止于某个特定参与者,也就是特定参与者明确地或者隐含地得到了业务任务完成的结果。
这里的参与者是业务领域内的参与者或者业务实体。
参与者不是待开发软件系统的一部分,但参与者需要和待开发软件系统交互。
参与者常常是人,比如客户,但也可以是一个外部的硬件或软件,甚至是待开发软件系统内部的一个组件,比如内部计时器可以触发某个业务过程。
某个参与者触发某个用例为相应的参与者完成一个业务任务。
用例的三个抽象层级
在准确理解用例概念的基础上,我们可以进一步将用例划分为三个抽象层级:
抽象用例(Abstract use case)。只要用一个干什么、做什么或完成什么业务任务的动名词短语,就可以非常精简地指明一个用例;
高层用例(High level use case)。需要给用例的范围划定一个边界,也就是用例在什么时候什么地方开始,以及在什么时候什么地方结束;
扩展用例(Expanded use case)。需要将参与者和待开发软件系统为了完成用例所规定的业务任务的交互过程一步一步详细地描述出来,一般我们使用一个两列的表格将参与者和待开发软件系统之间从用例开始到用例结束的所有交互步骤都列举出来。
用例建模的基本步骤
第一步,从需求表述中找出用例,往往是动名词短语表示的抽象用例;
第二步,描述用例开始和结束的状态,用TUCBW和TUCEW表示的高层用例;
第三步,对用例按照子系统或不同的方面进行分类,描述用例与用例、用例与参与者之间的上下文关系,并画出用例图;
第四步,进一步逐一分析用例与参与者的详细交互过程,完成一个两列的表格将参与者和待开发软件系统之间从用例开始到用例结束的所有交互步骤都列举出来扩展用例。
其中第一步到第三步是计划阶段,第四步是增量实现阶段。
用例图的基本画法
关联关系是用一条直线表示的,一般不强调关联的方向。关联关系是一种静态关系,是由常识、规则和法律等因素前置约定的。
一对一的关联关系可以在直线的两端都标上1表示一对一;一对多的关联关系可以在直线的两端分别标上1和*(代表任意数)表示一对多;多对多的关联关系可以在直线的两端都标上表示多对多。
单向关联关系,是用一条直线加箭头表示的。比如在用例图中参与者和用例就是一种单向关联关系,参与者总是“知道”用例,而用例“不知道”参与者,所以箭头可以从参与者指向用例,不过在UML建模工具中,关联方向是不被强调的。
包含关系是用一条带箭头的虚线加<>来表示的。包含关系特别用于用例模型中,是指在执行基本用例的过程中插入包含用例。
扩展关系是用一条带箭头的虚线加<>来表示的。扩展用例特别用于用例模型中,表示向基本用例中的某个扩展点插入扩展用例。
自动售货机的用例图
2、面向对象分析涉及的基本概念
对象(Object)和属性(Attribute)
继承关系(Inheritance Relationship)
继承关系表达着两个概念之间具有概括化/具体化(generalization/specialization)的关系
一般用三角形箭头连线表示两个类之间的继承关系
聚合关系(Aggregation Relationship)
聚合关系表示一个对象是另一个对象的一部分的情况。比如发动机引擎是小汽车的一部分。也被称为“部分与整体”(part-of)的关系。
聚合关系使用一个平行四边形的箭头表示
关联关系(Association Relationship)
关联关系表示继承和聚合以外的一般关系,是业务领域内特定的两个概念之间的关系,
既不是继承关系也不是聚合关系。比如教授参与了退休计划、讲师教授课程、用户拥有账户等都是两个概念之间一般关系,我们用一个直线连起来来表示教授和退休计划两个业务领域概念之间的关联关系,
业务领域建模(Domain Modeling)
业务领域建模是开发团队用于获取业务领域知识的过程。因为软件工程师往往需要工作在不同的业务领域或者不同项目中,他们需要业务领域知识来开发软件系统。软件工程师往往来自不同的专业背景,这可能会影响他们对业务领域的认知。因此业务领域建模有助于开发团队获取业务领域知识形成统一的业务认知
关联类及数据模型
关联关系是业务数据建模的关键,我们来专门研究一下关联关系,并引入一个关联类(Association Class)的概念,而且关联类为两个类的关联关系定义了一些属性和方法。
关系数据模型的MongoDB设计与实现
3、从需求分析到软件设计
敏捷统一过程(Agile Unified Process)
对象交互建模(Object Interaction Modeling)
形成软件设计方案的基本方法
统一过程(Unified Process)
统一过程(UP,Unified Process)的核心要义是用例驱动(Use case driven)、以架构为中心(Architecture centric)、增量且迭代(Incremental and Iterative)的过程。用例驱动就是我们前文中用例建模得到的用例作为驱动软件开发的目标;以架构为中心的架构是后续软件设计的结果,就是保持软件架构相对稳定,减小软件架构层面的重构造成的混乱;增量且迭代体现在下图中。
结合瀑布模型我们可以将统一过程模型简单理解为如图所示。
敏捷统一过程(Agile Unified Process)进一步将软件过程中每一次迭代过程划分为计划阶段和增量阶段。
瀑布模型
在继续进行从需求分析到软件设计的后续部分之前,我们有必要从整体上探讨一下我们所遵循的软件过程——敏捷统一过程。为了理解敏捷统一过程,我们先从瀑布模型(Waterfall Process)说起。
瀑布模型是最基本的过程模型,它把整个软件过程按顺序划分成了需求、设计、编码、测试和部署五个阶段。瀑布模型的根本特点是按顺序划分阶段,至于是像我们这样划分成五阶段,还是划分三个阶段或八个阶段,并不是关键。这一点需要读者注意,以免在阅读其他资料时产生困惑。
敏捷统一过程的计划阶段
在项目正式动手开工之前,敏捷统一过程要求进行精心周密的构思完成计划阶段。计划阶段要做的工作有如下几点:
首先明确项目的动机、业务上的实际需求,
以及对项目动机和业务需求可供替代选择的多种可能性;
然后充分调研获取需求并明确定义需求规格;
在明确需求规格的基础上进行项目的可行性研究;
如果项目可行,接下来接着进行用例建模并给出用例图;
同时明确需求和用例之间的可跟踪矩阵;
从而形成项目的概念模型草案;
以及项目可能的日程安排、需要的资源以及大致的预算范围。
敏捷统一过程的四个关键步骤
第一,确定需求;
第二,通过用例的方式来满足这些需求;
第三,分配这些用例到各增量阶段;
第四,具体完成各增量阶段所计划的任务。
显然,第一到第三步主要是计划阶段的工作,第四步是接下来要进一步详述的增量阶段的工作。
敏捷统一过程的增量阶段
在每一次增量阶段的迭代过程中,都要进行从需求分析到软件设计实现的过程,具体敏捷统一过程将增量阶段分为五个步骤:
用例建模(Use case modeling);
业务领域建模(Domain modeling);
对象交互建模(Object Interaction modeling);
形成设计类图(design class diagram);
软件的编码实现和软件应用部署;
对象交互建模的基本步骤
找出关键步骤进行剧情描述(scenario)
将剧情描述(scenario)转换成剧情描述表(scenario table)
将剧情描述表转换成序列图的基本方法
从分析序列图到设计序列图
一个完整用例的对象交互建模
•第一步,在扩展用例中右侧一列中找出关键步骤(nontrivial steps)。关键步骤是那些需要在背后进行业务过程处理的步骤,而不是仅仅在表现层(presentation layer, i.e., the Graphical User Interface or GUI)与参与者进行用户接口层面交互的琐碎步骤。
•第二步,对于每一个关键步骤,从关键步骤在扩展用例两列表格中的左侧作为开始,完成剧情描述(scenario),描述一步一步的对象交互过程,直到执行完该关键步骤。
•第三步,如果需要的话,将剧情描述(scenario)进一步转换成剧情描述表(scenario table)。
•第四步,将剧情描述(scenario)或剧情描述表(scenario table)转换成序列图。
•对象交互建模的四个基本步骤以某个用例的扩展用例为输入,中间借助业务领域知识及业务领域建模中的相关对象、属性等,最终产出结果为序列图。
将剧情描述表转换成序列图的基本方法
情形一:主体(Subject)是一个参与者(Actor)
情形二:主体(Subject)是一个对象(Object)
情形三:主体(Subject)需要接收返回值的情形
情形四:主体(Subject)和主体行为的作用对象(object acted upon)是同一个对象的情形
形成软件设计方案的基本方法
软件产品庞大复杂,前面的形成的设计类图只是其中一个用例得到的设计结果,我们需要对每一个用例进行分析和设计,最终再将各用例得到的设计结果综合成一个软件产品的整体设计方案。其中涉及两个基本的方法:分析(analysis)和综合(synthesis)。
分析是分解大问题变成易于理解的小问题。比如用例建模是将错综复杂的需求分解成一个一个的用例。在分析的过程中除了“分而治之”的切分分解的方法外,抽象方法的运用是一个关键
综合是将一个个小问题的解决方案组合起来构建软件的整体解决方案。我们对每一个用例的关键步骤进行对象交互建模逐步形成了用例对应的解决方案,如何将多个用例的小解决方案组合起来构建软件整体设计方案?这在软件设计中是一个非常有挑战性的问题,一般我们通过参考已有的软件设计模式提供一个思路从而综合出一个软件整体解决方案
关系数据模型的MongoDB设计与实现
MongoDB简介
MongoDB是一个通用的、基于文档的、分布式的数据库,为云计算时代的现代应用程序开发者而生,没有数据库比MongoDB在应用开发效率上更加高效。
MongoDB是一种文档数据库,也就是说MongoDB用类似JSON格式的文档来存储数据。目前普遍认为JSON格式是理解和存储数据最自然的方式,JSON格式比传统的关系数据模型有更强大的数据表达能力。
MongoDB中存储的JSON格式文档范例如下所示。每一个JSON文档对应一个ID即下图中“_id"的值,除“_id"外的数据按照“key:value”的方式可以任意定义数据的结构
在一个MongoDB数据库中,可以创建多个集合(Collection),集合的概念类似于关系数据库中的表(Table),只是比表更加灵活。下图是在users集合中检索邮政编码为90404的数据,可见其检索方式比传统的SQL语言更加强大灵活
内嵌,子引用,父引用
三种基本的设计方案:内嵌,子引用,父引用
在选择方案时需要考虑的两个关键因素:
一对多中的多是否需要一个单独的实体;
这个关系中集合的规模是一对很少,很多,还是非常多。
一对很少且不需要单独访问内嵌内容的情况下可以使用内嵌多的一方。
一对很多且很多的一端内容因为各种理由需要单独存在的情况下可以通过数组的方式引用多的一方的。
一对非常多的情况下,请将一的那端引用嵌入进多的一端对象中
反范式化
•反范式化在节省你读的代价的同时会带来更新的代价:如果你将零件的名字冗余到产品的文档对象中,那么你想更改某个零件的名字你就必须同时更新所有包含这个零件的产品对象。
•在一个读比写频率高的多的系统里,反范式是有使用的意义的。如果你很经常的需要高效的读取冗余的数据,但是几乎不去变更他的话,那么付出更新上的代价还是值得的。更新的频率越高,这种设计方案的带来的好处越少。
•例如:假设零件的名字变化的频率很低,但是零件的库存变化很频繁,那么你可以冗余零件的名字到产品对象中,但是别冗余零件的库存。
•需要注意的是,一旦你冗余了一个字段,那么对于这个字段的更新将不再是原子的。和上面双向引用的例子一样,如果你在零件对象中更新了零件的名字,那么更新产品对象中保存的名字字段前将会存在短时间的不一致。
使用双向引用来优化你的数据库架构,前提是你能接受无法原子更新的代价。
可以在引用关系中冗余数据到one端或者N端。
在决定是否采用反范式化时需要考虑下面的因素:
你将无法对冗余的数据进行原子更新。
只有读写比比较高的情况下才应该采取反范式化的设计
关系数据模型的MongoDB设计与实现总结
优先考虑内嵌,除非有什么迫不得已的原因。
需要单独访问一个对象,那这个对象就不适合被内嵌到其他对象中。
数组不应该无限制增长。如果many端有数百个文档对象就不要去内嵌他们可以采用引用ObjectID的方案;如果有数千个文档对象,那么就不要内嵌ObjectID的数组。该采取哪些方案取决于数组的大小。
不要害怕应用层级别的join:如果索引建的正确并且通过投影条件限制返回的结果,那么应用层级别的join并不会比关系数据库中join开销大多少。
在进行反范式设计时请先确认读写比。一个几乎不更改只是读取的字段才适合冗余到其他对象中。
在mongodb中如何对你的数据建模,取决于你的应用程序如何去访问它们。数据的结构要去适应你的程序的读写场景。
第四章、软件科学基础概论
软件的基本架构
• 顺序结构
• 分支结构
• 循环结构
• 函数调用框架
• 继承和对象组合
软件中的一些特殊机制
回调函数
回调函数是一个面向过程的概念,是代码执行过程的一种特殊流程。回调函数就是一个通过函数指针调用的函数。把函数的指针(地址)作为参数传递给另一个函数,当这个指针调用其所指向的函数时,就称这是回调函数。回调函数不是该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
//回调函数
int SearchCondition(tLinkTableNode * pLinkTableNode, void * args)
{
char * cmd = (char*) args;
tDataNode * pNode = (tDataNode *)pLinkTableNode;
if(strcmp(pNode->cmd, cmd) == 0)
{
return SUCCESS;
}
return FAILURE;
}
int main()
{
...
//传递回调函数
SearchLinkTableNode(head, SearchCondition, (void*)cmd);
…
}
闭包
闭包是变量作用域的一种特殊情形,一般用在将函数作为返回值时,该函数执行所需的上下文环境也作为返回的函数对象的一部分,这样该函数对象就是一个闭包。
更严谨的定义是,函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起构成闭包(closure)。也就是说,闭包可以让你从内部函数访问外部函数作用域。在JavaScript中,每当函数被创建,就会在函数生成时生成闭包。
function makeFunc() {
var name = "Mozilla";
function displayName() {
alert(name);
}
return displayName;
}
var myFunc = makeFunc();
myFunc();
多态
多态(Polymorphism)按字面的意思就是“多种状态”。在面向对象语言中,接口的多种不同的实现方式即为多态。多态是实例化变量可以指向不同的实例对象,这样同一个实例化变量在不同的实例对象上下文环境中执行不同的代码表现出不同的行为状态,而通过实例化变量调用实例对象的方法的那一块代码却是完全相同的,这就顾名思义,同一段代码执行时却表现出不同的行为状态,因而叫多态。简单的说,可以理解为允许将不同的子类类型的对象动态赋值给父类类型的变量,通过父类的变量调用方法在执行时实际执行的可能是不同的子类对象方法,因而表现出不同的执行效果
异步调用
Promise对象可以将异步调用以同步调用的流程表达出来,避免了通过嵌套回调函数实现异步调用。
Promise对象的基本用法
Promise对象实际上是对回调函数机制的封装,也就是通过then方法定义的函数与resolve/reject函数绑定,简化了回调函数传入的接口实现,在逻辑上也更加通顺,看起来像是个同步接口。
var promise = new Promise(function(resolve, reject) {
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
promise.then(function(value) { // resolve(value)
// success
}, function(value) { // reject(error)
// failure
});
匿名函数
lamda函数是函数式编程中的高阶函数
设计模式
设计模式的本质是面向对象设计原则的实际运用总结出的经验模型。对类的封装性、继承性和多态性以及类的关联关系和组合关系的充分理解的基础上才能准确理解设计模式。
设计模式的优点
正确使用设计模式具有以下优点。
可以提高程序员的思维能力、编程能力和设计能力。
使程序设计更加标准化、代码编制更加工程化,使软件开发效率大大提高,从而缩短软件的开发周期。
使设计的代码可重用性高、可读性强、可靠性高、灵活性好、可维护性强。
用模块化来包容变化,使用模块化封装的方法,按照模块化追求的高内聚低耦合目标,借助于抽象思维对模块内部信息的隐藏并使用封装接口对外只暴露必要的可见信息,利用多态、闭包、lamda函数、回调函数等特殊的机制方法,将变化的部分和不变的部分进行适当隔离。这些都是设计模式的拿手好戏
设计模式的分类
根据模式是主要用于类上还是主要用于对象上来划分的话,可分为类模式和对象模式两种类型的设计模式:
类模式:用于处理类与子类之间的关系,这些关系通过继承来建立,是静态的,在编译时刻便确定下来了。比如模板方法模式等属于类模式。
对象模式:用于处理对象之间的关系,这些关系可以通过组合或聚合来实现,在运行时刻是可以变化的,更具动态性。由于组合关系或聚合关系比继承关系耦合度低,因此多数设计模式都是对象模式。
根据设计模式可以完成的任务类型来划分的话,可以分为创建型模式、结构型模式和行为型模式 3 种类型的设计模式:
创建型模式:用于描述“怎样创建对象”,它的主要特点是“将对象的创建与使用分离”。比如单例模式、原型模式、建造者模式等属于创建型模式。
结构型模式:用于描述如何将类或对象按某种布局组成更大的结构,比如代理模式、适配器模式、桥接模式、装饰模式、外观模式、享元模式、组合模式等属于结构型模式。结构型模式分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。由于组合关系或聚合关系比继承关系耦合度低,所以对象结构型模式比类结构型模式具有更大的灵活性。
行为型模式:**用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。**比如模板方法模式、策略模式、命令模式、职责链模式、观察者模式等属于行为型模式。行为型模式分为类行为模式和对象行为模式,前者采用继承在类间分配行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,所以对象行为模式比类行为模式具有更大的灵活性
常用的设计模式
单例(Singleton)模式:某个类只能生成一个实例,该类提供了一个全局访问点供外部获取该实例,典型的应用如数据库实例。
原型(Prototype)模式:将一个对象作为原型,通过对其进行复制而克隆出多个和原型类似的新实例,原型模式的应用场景非常多,几乎所有通过复制的方式创建新实例的场景都有原型模式。
建造者(Builder)模式:将一个复杂对象分解成多个相对简单的部分,然后根据不同需要分别创建它们,最后构建成该复杂对象。主要应用于复杂对象中的各部分的建造顺序相对固定或者创建复杂对象的算法独立于各组成部分
代理(Proxy)模式:为某对象提供一种代理以控制对该对象的访问。即客户端通过代理间接地访问该对象,从而限制、增强或修改该对象的一些特性。代理模式是不要和陌生人说话原则的体现,典型的应用如外部接口本地化将外部的输入和输出封装成本地接口,有效降低模块与外部的耦合度。
适配器(Adapter)模式:将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。继承和对象组合都可以实现适配器模式,但由于组合关系或聚合关系比继承关系耦合度低,所以对象组合方式的适配器模式比较常用。
装饰(Decorator)模式:在不改变现有对象结构的情况下,动态地给对象增加一些职责,即增加其额外的功能。装饰模式实质上是用对象组合的方式扩展功能,因为比继承的方式扩展功能耦合度低。装饰模式在 Java 语言中的最著名的应用莫过于 Java I/O 标准库的设计了。例如,InputStream 的子类 FilterInputStream,OutputStream 的子类 FilterOutputStream,Reader 的子类 BufferedReader 以及 FilterReader,还有 Writer 的子类 BufferedWriter、FilterWriter 以及 PrintWriter 等,它们都是抽象装饰类。
外观(Facade)模式:为复杂的子系统提供一个一致的接口,使这些子系统更加容易被访问。
享元(Flyweight)模式:运用共享技术来有效地支持大量细粒度对象的复用。比如线程池、固定分配存储空间的消息队列等往往都是该模式的应用场景。
策略(Strategy)模式:定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的改变不会影响使用算法的客户。策略 模式是多态和对象组合的综合应用。
命令(Command)模式:将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开。这样两者之间通过命令对象进行沟通,这样方便将命令对象进行储存、传递、调用、增加与管理。
模板方法(TemplateMethod)模式:定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。**模版方法是继承和重载机制的应用,属于类模式。
职责链(Chain of Responsibility)模式:为了避免请求发送者与多个请求处理者耦合在一起,将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链;当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。通过这种方式将多个请求处理者串联为一个链表,去除请求发送者与它们之间的耦合。
中介者(Mediator)模式:定义一个中介对象来简化原有对象之间的交互关系,降低系统中对象间的耦合度,使原有对象之间不必相互了解。在现实生活中,常常会出现好多对象之间存在复杂的交互关系,这种交互关系常常是“网状结构”,它要求每个对象都必须知道它需要交互的对象。如果把这种“网状结构”改为“星形结构”的话,将大大降低它们之间的“耦合性”,这时只要找一个“中介者”就可以了。在软件的开发过程中,这样的例子也很多,例如,在 MVC 框架中,控制器(C)就是模型(M)和视图(V)的中介者,采用“中介者模式”大大降低了对象之间的耦合性,提高系统的灵活性。
观察者(Observer)模式:指多个对象间存在一对多的依赖关系,当一个对象的状态发生改变时,把这种改变通知给其他多个对象,从而影响其他对象的行为,这样所有依赖于它的对象都得到通知并被自动更新。这种模式有时又称作发布-订阅模式
设计模式背后的设计原则
开闭原则(Open Closed Principle,OCP)
Liskov替换原则(Liskov Substitution Principle,LSP)
依赖倒置原则(Dependence Inversion Principle,DIP)
单一职责原则(Single Responsibility Principle,SRP)
迪米特法则(Law of Demeter,LoD)
合成复用原则(Composite Reuse Principle,CRP)
开闭原则(Open Closed Principle,OCP)
软件应当对扩展开放,对修改关闭。
遵守开闭原则使软件拥有一定的适应性和灵活性的同时具备稳定性和延续性。统一过程以架构为中心增量且迭代的过程和开闭原则具有内在的一致性,它们都追求软件结构上的稳定性。当我们理解了软件结构模型本质上具有不稳定性的时候,一个小小的需求变更便很可能会触动软件结构的灵魂,瞬间让软件结构崩塌,即便通过破坏软件结构的内在逻辑模型打上丑陋的补丁程序,也会使得软件内在结构恶化加速软件的死亡。因此开闭原则在基本需求稳定且被充分理解的前提下才具有一定的价值。
Liskov替换原则(Liskov Substitution Principle,LSP)
Liskov替换原则主要阐述了继承用法的原则,也就是什么时候应该使用继承,什么时候不应该使用继承,以及其中蕴含的原理。通俗来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。也就是说儿子和父母要在DNA基因上一脉相承,尽管程序员是自己的代码的上帝,但也不能胡来,要做一个遵守自然规律的上帝
Liskov替换原则如今看来其价值也大大折扣,因为为了降低耦合度我们往往使用对象组合来替代继承关系。反而是Liskov替换原则不推荐的多态成为诸多设计模式的基础。
依赖倒置原则(Dependence Inversion Principle,DIP)
依赖倒置原则的原始定义为:高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。其核心思想是:要面向接口编程,不要面向实现编程。
单一职责原则(Single Responsibility Principle,SRP)
单一职责原则(Single Responsibility Principle,SRP)又称单一功能原则,单一职责原则规定一个类应该有且仅有一个引起它变化的原因,否则类应该被拆分。
单一职责原则的核心就是控制类的粒度大小、提高其内聚度。如果遵循单一职责原则可以降低类的复杂度,因为一个类只负责一项职责,其逻辑肯定要比负责多项职责简单得多;同时可以提高类的内聚度,符合模块化设计的高内聚低耦合的设计原则
迪米特法则(Law of Demeter,LoD)
迪米特法则的定义是:只与你的直接朋友交谈,不跟“陌生人”说话(Talk only to your immediate friends and not to strangers)。其含义是:如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。
合成复用原则(Composite Reuse Principle,CRP)
合成复用原则(Composite Reuse Principle,CRP)又叫组合/聚合复用原则(Composition/Aggregate Reuse Principle,CARP)。它要求在软件复用时,要尽量先使用组合或者聚合关系来实现,其次才考虑使用继承关系来实现。如果要使用继承关系,则必须严格遵循Liskov替换原则。
通常类的复用分为继承复用和对象组合复用两种。
继承复用的缺点
继承复用破坏了类的封装性。因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为“白箱”复用。
子类与父类的耦合度高。父类的实现的任何改变都会导致子类的实现发生变化,这不利于类的扩展与维护。
继承复用限制了复用的灵活性。从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可能发生变化。
合成复用的优点
采用组合或聚合复用时,可以将已有对象纳入新对象中,使之成为新对象的一部分,新对象可以调用已有对象的功能,它有以下优点。
组合或聚合复用维持了类的封装性。因为属性对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱”复用。
新旧类之间的耦合度低。这种复用所需的依赖较少,新对象存取属性对象的唯一方法是通过属性对象的接口。
复用的灵活性高。这种复用可以在运行时动态进行,新对象可以动态地引用与属性对象类型相同的对象。
常见的软件架构举例
三层架构
层次化架构是利用面向接口编程的原则将层次化的结构型设计模式作为软件的主体结构。比如三层架构是层次化架构中比较典型的代表,如下图所示我们以层次化架构为例来介绍
MVC架构
MVC即为Model-View-Controller(模型-视图-控制器),MVC是一种设计模式,以MVC设计模式为主体结构实现的基础代码框架一般称为MVC框架,如果MVC设计模式决定了整个软件的架构,不管是直接实现了MVC模式还是以某一种MVC框架为基础,只要软件的整体结构主要表现为MVC模式,我们就称为该软件的架构为MVC架构。
MVC中M、V和C所代表的含义如下:
Model(模型)代表一个存取数据的对象及其数据模型。
View(视图)代表模型包含的数据的表达方式,一般表达为可视化的界面接口。
Controller(控制器)作用于模型和视图上,控制数据流向模型对象,并在数据变化时更新视图。控制器可以使视图与模型分离开解耦合。
MVC模式通常用开发具有人机交互界面的软件,这类软件的最大特点就是用户界面容易随着需求变更而发生改变,例如,当你要扩展一个应用程序的功能时,通常需要修改菜单和添加页面来反映这种变化。如果用户界面和核心功能逻辑紧密耦合在一起,要扩展功能通常是非常困难的,因为任何改动很容易在其他功能上产生错误。
为了包容需求上的变化而导致的用户界面的修改不会影响软件的核心功能代码,可以采用将模型(Model)、视图(View)和控制器(Controller)相分离的思想。采用MVC设计模式的话往往决定了整个软件的主体结构,因此我们称该软件为MVC架构。
MVC架构 vs. 三层架构
模型和视图有着业务层面的业务数据紧密耦合关系,控制器的核心工作就是业务逻辑处理,显然MVC架构和三层架构有着某种对应关系,但又不是层次架构的抽象接口依赖关系,因此为了体现它们的区别和联系,我们在MVC的结构示意图中将模型和视图上下垂直对齐表示它们内在的业务层次及业务数据的对应关系,而将控制器放在左侧表示控制器处于优先重要位置,放在模型和视图的中间位置是为了与三层架构对应与业务逻辑层处于相似的层次
应用MVC架构的软件其基本的实现过程
控制器创建模型;
控制器创建一个或多个视图,并将它们与模型相关联;
控制器负责改变模型的状态;
当模型的状态发生改变时,模型会通知与之相关的视图进行更新。
可以看到这与抽象简化的MVC设计模式已经有了明显的区别,变得更为复杂,但这与实际软件结构相比还是极其简化的模型,实际情况可能会有更多合理的和不合理的复杂联系,要保持软件结构在概念上的完整性极为困难。
MVC架构中的模型、视图与控制器
在MVC架构下,模型用来封装核心数据和功能,它独立于特定的输出表示和输入行为,是执行某些任务的代码,至于这些任务以什么形式显示给用户,并不是模型所关注的问题。模型只有纯粹的功能性接口,也就是一系列的公开方法,这些方法有的是取值方法,让系统其它部分可以得到模型的内部状态,有的则是写入更新数据的方法,允许系统的其它部分修改模型的内部状态。
在MVC架构下,视图用来向用户显示信息,它获得来自模型的数据,决定模型以什么样的方式展示给用户。同一个模型可以对应于多个视图,这样对于视图而言,模型就是可重用的代码。一般来说,模型内部必须保留所有对应视图的相关信息,以便在模型的状态发生改变时,可以通知所有的视图进行更新。
在MVC架构下,控制器是和视图联合使用的,它捕捉鼠标移动、鼠标点击和键盘输入等事件,将其转化成服务请求,然后再传给模型或者视图。软件的用户是通过控制器来与系统交互的,他通过控制器来操纵模型,从而向模型传递数据,改变模型的状态,并最后导致视图的更新。
什么是MVVM?
Vue.js框架的MVVM架构
•在前端页面中,把Model用纯JavaScript对象表示,View负责显示,两者做到了最大限度的分离。把Model和View关联起来的就是ViewModel。ViewModel负责把Model的数据同步到View显示出来,还负责把View的修改同步回Model。以比较流行的Vue.js框架为例,MVVM架构示意图如下:
MVVM的优点
MVVM模式和MVC模式一样,主要目的是分离视图(View)和模型(Model),有几大优点:
低耦合。视图(View)可以独立于Model变化和修改,一个ViewModel可以绑定到不同的"View"上,当View变化的时候Model可以不变,当Model变化的时候View也可以不变。
可重用性。你可以把一些视图逻辑放在一个ViewModel里面,让很多View重用这段视图逻辑。
独立开发。开发人员可以专注于业务逻辑和数据的开发(ViewModel),设计人员可以专注于页面设计。
可测试。界面素来是比较难于测试的,测试可以针对ViewModel来写。
两种不同层级的软件架构复用方法
克隆(Cloning),完整地借鉴相似项目的设计方案,甚至代码,只需要完成一些细枝末节处的修改适配工作。
重构(Refactoring),构建软件架构模型的基本方法,通过指引我们如何进行系统分解,并在参考已有的软件架构模型的基础上逐步形成系统软件架构的一种基本建模方法。
分解的常见方法
面向功能的分解方法,用例建模即是一种面向功能的分解方法;
面向特征的分解方法,根据数量众多的某种系统显著特征在不同抽象层次上划分模块的方法;
面向数据的分解方法,在业务领域建模中形成概念业务数据模型即应用了面向数据的分解方法;
面向并发的分解方法,在一些系统中具有多种并发任务的特点,那么我们可以将系统分解到不同的并发任务中(进程或线程),并描述并发任务的时序交互过程;
面向事件的分解方法,当系统中需要处理大量的事件,而且往往事件会触发复杂的状态转换关系,这时系统就要考虑面向事件的分解方法,并内在状态转换关系进行清晰的描述;
面向对象的分解方法,是一种通用的分析设计范式,是基于系统中抽象的对象元素在不同抽象层次上分解的系统的方法。
管道-过滤器
管道-过滤器风格的软件架构是面向数据流的软件体系结构,最典型的应用是编译系统。 一个普通的编译系统包括词法分析器、语法分析器、语义分析与中间代码生成器、目标代码生成器等一系列对源代码进行处理的过程。就像如图:管道-过滤器风格示意图,对源代码处理的过滤器通过管道连接起来,实现了端到端的从源代码到编译目标的完整系统
客户-服务
Client/Server(C/S)和Browser/Server(B/S)是我们常用的对软件的网络结构特点的表述方式,但它们背后蕴含着一种普遍存在的软件架构风格,即客户-服务模式的架构风格。
客户-服务模式的架构风格是指客户代码通过请求和应答的方式访问或者调用服务代码。这里的请求和应答可以是函数调用和返回值,也可以是TCP Socket中的send和recv,还可以是HTTP协议中的GET请求和响应。
在客户-服务模式中,客户是主动的,服务是被动的。客户知道它向哪个服务发出请求,而服务却不知道它正在为哪个客户提供服务,甚至不知道正在为多少客户提供服务。
客户-服务模式的架构风格具有典型的模块化特征,降低了系统中客户和服务构件之间耦合度,提高了服务构件的可重用性。
P2P
P2P(peer-to-peer)架构是客户-服务模式的一种特殊情形,P2P架构中每一个构件既是客户端又是服务端,即每一个构件都有一种接口,该接口不仅定义了构件提供的服务,同时也指定了向同类构件发送的服务请求。这样众多构件一起形成了一种对等的网络结构,如图:P2P网络结构示意图。
P2P架构典型的应用有文件共享网络、比特币网络等。
发布-订阅
在发布-订阅架构中,有两类构件:发布者和订阅者。如果订阅者订阅了某一事件,则该事件一旦发生,发布者就会发布通知给该订阅者。观察者模式体现了发布-订阅架构的基本结构。
在实际应用中往往会需要不同的订阅组,以及不同的发布者。由于订阅者数量庞大往往在消息推送时采用消息队列的方式延时推送。如图:包含消息队列的发布-订阅架构示意图
CRUD
CRUD 是创建(Create)、 读取(Read)、更新(Update)和删除(Delete)四种数据库持久化信息的基本操作的助记符,表示对于存储的信息可以进行这四种持久化操作。CRUD也代表了一种围绕中心化管理系统关键数据的软件架构风格。一般常见的各类信息系统,比如ERP、CRM、医院信息化平台等都可以用CRUD架构来概括。
层次化
•较为复杂的系统中的软件单元,仅仅从平面展开的角度进行模块化分解是不够的,还需要从垂直纵深的角度将软件单元按层次化组织,每一层为它的上一层提供服务,同时又作为下一层的客户。
•通信网络中的OSI(Open System Interconnection)参考模型是典型的层次化架构风格,如图:OSI参考模型示意图。在OSI参考模型中每一层都将相邻底层提供的通信服务抽象化,隐藏它的实现细节,同时为相邻的上一层提供服务。
软件架构的描述方法
软件架构模型是通过一组关键视图来描述的,同一个软件架构,由于选取的视角(Perspective)和抽象层次不同可以得到不同的视图,这样一组关键视图搭配起来可以完整地描述一个逻辑自洽的软件架构模型。一般来说,我们常用的几种视图有分解视图、依赖视图、泛化视图、执行视图、实现视图、部署视图和工作任务分配视图。
几种重要的软件质量属性
易于修改维护(Modifiability)
良好的性能表现(Performance)
安全性(Security)
可靠性(Reliability)
健壮性(Robustness)
易用性(Usability)
商业目标(Business goals)
第五章、软件危机和软件过程
• 软件危机
他体会到开发大型软件过程中的根本问题是难以汇集众多参与人员的设计理念形成完整的、一致的软件复杂概念结构,从而使得大型软件项目往往会进展缓慢、成本暴涨及错误百出,就是所谓的软件危机(software
crisis)的典型表现。
没有银弹
• 软件过程模型
• 2.1.软件的生命周期概述
我们将软件的生命周期划分为:分析、设计、实现、交付和维护这么五个阶段
• 2.2.瀑布模型
因为瀑布模型假定需求不会发生任何变化。由此看来,瀑布模型将软件开发过程看作类似于工业生产制造的过程,而不是一个创造性的开发过程。
• 2.3.带原型的瀑布模型
• 2.4.V模型
V模型也是在瀑布模型基础上发展出来的,我们发现单元测试、集成测试和系统测试是为了在不同层面验证设计,而交付测试则是确认需求是否得到满足。也就是瀑布模型中前后两端的过程活动具有内在的紧密联系,如果将模块化设计的思想拿到软件开发过程活动的组织中来,可以发现通过将瀑布模型前后两端的过程活动结合起来,可以提高过程活动的内聚度,从而改善软件开发效率。这就是V模型
• 2.5.分阶段增量和迭代
• 2.6.螺旋模型
螺旋模型最大的特点在于引入了其他模型不具备的风险管理,使软件在无法排除重大风险时有机会停止
• PSP和TSP
• CMM/CMMI
•CMMI一级,初始级。没有包含任何过程域。
•CMMI二级,管理级。包含的过程域有需求管理 RM、项目计划PP、项目监督与控制PMC、供应协议管理SAM、过程与产品质量保证PPQA、配置管理CM和度量与分析 MA。
•CMMl三级,已定义级。包含的过程域有需求制定RD、技术方案TS、产品集成PI、验证VER、确认VAL、组织过程聚焦OPF、组织过程定义OPD、组织培训OT、集成项目管理IPM、风险管理RSKM、决策分析与决定DAR、集成供应商管理ISM、组织集成环境OEI和集成组队IT。
•CMMI四级,量化管理级。包含的过程域有组织过程性能OPP和定量项目管理QPM。
•CMMI五级,持续优化级。包含的过程域有组织革新与部署OID和原因分析与决策CAR。
• 敏捷方法
•我们一直在实践中探寻更好的软件开发方法,身体力行的同时也帮助他人。由此我们建立了如下价值观:
• 个体和互动 高于 流程和工具
• 工作的软件 高于 详尽的文档
• 客户合作 高于 合同谈判
• 响应变化 高于 遵循计划
• DevOps
学习感想:
一款软件的需求分析在设计之初尤为重要,这是地基。所以我们在“考勤助手”的设计当中也是尽可能地把用户的需求完善,做到每个用户各司其职。结合需求分析的ER图的设计其实是比较复杂的,因为必须要考虑清楚每个实体集之间的联系情况,是多对多、多对一等等。其次是用例图,如果需求能够定好,那用例图的设计自然不会太难。最后就是时序图和类图,其实从ER图开始,我认为接下来的时序图和UML类图也同样需要对敲和斟酌,一个小的疏忽可能在设计的某个时刻被发现,从而带来“翻天覆地”的变化,所以需要不断地返工和完善这两项设计。