Swagger & Mock

简介

之前在开发数据库课程项目的时候,作为 Leader,一直在想怎么解决前后端开发同时进行的问题,这学期知道了Swagger 这个东西,有了它我们的开发流程是这样的:

1
2
3
4
5
6
- 协商API
- 前端开发
- 编写前端页面
- 通过 Mock Server 充当真服务端
- 服务端开发
- ...

也就是说,前端在开发过程中,不需要等待服务端的小火鸡写完接口再进行测试,通过 Mock Server 即可返回想要的 Response 进行各种测试。

Swagger

何为 Swagger,照我的理解,就是一个写API文档的框架,之前写项目基本都是手撸API文档,然后从零编写服务端和客户端代码,而有了它,我们只需要编写一个 yaml 文件,它就可以自动帮我们生成客户端 API 调用代码、服务端大致框架以及一个好用的API文档。

多说无益,上手玩玩才知道多棒!传送门:Swagger Editer

怎么用?

看看下面这个小例子吧(主要看有注释的那几行即可)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
paths:
/pet: # api path
post: # api method
tags:
- "pet"
summary: "Add a new pet to the store"
description: ""
operationId: "addPet" # 这个名字会成为上面所说生成代码的调用接口
consumes: # Request 数据格式为:
- "application/json" # - json
- "application/xml" # - xml
produces: # Response 数据格式为:
- "application/xml" # - xml
- "application/json" # - json
parameters: # Request 参数
- in: "body" # 参数位于 body 中 (因为 Method 为 POST)
name: "body"
description: "Pet object that needs to be added to the store"
required: true # Request 必须有此参数
schema:
$ref: "#/definitions/Pet" # 参数的样式,这里为 Pet
responses: # Response 返回样例
405: # Response status
description: "Invalid input"

上面编写的 yaml 文件就帮我们描述了 POST /pet 这个添加宠物的接口啦!

生成代码

我们可以分别生成 Server 和 Client 代码,这里分别选择 go-server 和 JavaScript

先看看前端 JavaScript 代码,实际上,如果让我们手撸一个这样的接口,我们并不会写得这么健全,如果我们需要发送一个POST请求,那么我们可能只会写 body 这部分的参数,而不会像下面所示代码那样,面面俱到,也许是有些许冗余了,但是机器生成的代码,还要什么自行车呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
* Add a new pet to the store
*
* @param {module:model/Pet} body Pet object that needs to be added to the store
* @param {module:api/PetApi~addPetCallback} callback The callback function, accepting three arguments: error, data, response
*/
this.addPet = function(body, callback) {
var postBody = body;

// verify the required parameter 'body' is set
if (body === undefined || body === null) {
throw new Error("Missing the required parameter 'body' when calling addPet");
}

var pathParams = {
};
var queryParams = {
};
var collectionQueryParams = {
};
var headerParams = {
};
var formParams = {
};

var authNames = ['petstore_auth'];
var contentTypes = ['application/json', 'application/xml'];
var accepts = ['application/xml', 'application/json'];
var returnType = null;

return this.apiClient.callApi(
'/pet', 'POST',
pathParams, queryParams, collectionQueryParams, headerParams, formParams, postBody,
authNames, contentTypes, accepts, returnType, callback
);
}

再看看 go-server 代码,其只是一个简单的模板,帮我们写好了 Response 的头,以及帮我们定义了 Pet 这个结构体,需要注意这个需要我们在 yaml 中定义。服务端需要做的,就是搞搞数据库,填填逻辑代码,有了这么一个框架,工作量真的少了!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// pet_api.go
func AddPet(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
w.WriteHeader(http.StatusOK)
}

// pet.go
type Pet struct {

Id int64 `json:"id,omitempty"`

Category *Category `json:"category,omitempty"`

Name string `json:"name"`

PhotoUrls []string `json:"photoUrls"`

Tags []Tag `json:"tags,omitempty"`

// pet status in the store
Status string `json:"status,omitempty"`
}

Mock

mock 是在测试过程中,对于一些不容易构造/获取的对象,创建一个mock对象来模拟对象的行为,这里我们不深究,我们选 Mockjs 来玩玩看。

Mock.js 是一款模拟数据生成器,旨在帮助前端工程师独立于后端进行开发,帮助编写单元测试。提供了以下模拟功能:

  • 根据数据模板生成模拟数据
  • 模拟 Ajax 请求,生成并返回模拟数据
  • 基于 HTML 模板生成模拟数据

直接 传送,打开控制台就可以照着示例玩耍了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 例如我想创建一个博客列表,每个条目包含id、评分
Mock.mock({
'posts|5': [{
'id|+1': 1,
'stars|1-10': '*'
}]
})
// 下面为上述代码所生成
{
"posts": [
{
"id": 1,
"stars": "*"
},
{
"id": 2,
"stars": "*"
},
{
"id": 3,
"stars": "**"
},
{
"id": 4,
"stars": "*********"
},
{
"id": 5,
"stars": "*"
}
]
}

Swagger-Mock

当两者结合起来的威力有多大呢,试想一下,一个可以帮你搭建好服务端框架,一个可以帮你任意生成数据,那么合起来,好像就能弄出一个能 work 的 Server 了不是吗!

swagger-node

作为一个前端开发人员,node 当然是最亲的啦!swagger支持多种语言,所以我们选择 swagger-node 作为伪服务端不是很棒吗。

安装

1
npm install -g swagger

创建一个新项目

1
swagger project create hello-world

看看项目的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.
├── README.md
├── api
│ ├── assets
│ ├── controllers
│ ├── helpers
│ ├── mocks
│ └── swagger
├── app.js
├── config
│ ├── README.md
│ └── default.yaml
├── package-lock.json
├── package.json
└── test
└── api

9 directories, 6 files

编辑你的API文档

这里与前面说的编写 yaml 基本一致,所编辑的 yaml 文件就在 ./api/swagger 中

1
swagger project edit

不过你需要干一件事,就是给你的 swagger-node 标记一下 controller 所在位置,即:

1
2
3
paths:
/hello:
x-swagger-router-controller: hello_world # 处理/hello的controller在hello_world.js

然后再看看 controller:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ./api/controllers/hello_world.js

/* 记住要将接口暴露出去 */
module.exports = { hello };

/*
Functions in a127 controllers used for operations should take two parameters:

Param 1: a handle to the request object
Param 2: a handle to the response object
*/
function hello(req, res) {
// variables defined in the Swagger document can be referenced using req.swagger.params.{parameter_name}
var name = req.swagger.params.name.value || 'stranger';
var hello = util.format('Hello, %s!', name);

// this sends back a JSON response which is a single string
res.json(hello);
}

总结

如果你还在手撸 API 文档,不如玩玩 Swagger 啦!

如果你还在从零构建服务端代码,不如让 Swagger 帮帮你啦!

如果你还在等服务端的小火鸡完成API开发,不如自己搭建一个 Swagger-Mock-Server 啦!

SWAD | Homework4

简答题

  • 用例的概念
    • 用例是描述一个参与者使用一个系统来实现一个目标的成功或失败场景的集合
  • 用例和场景的关系?什么是主场景或 happy path?
    • 场景就是参与者和系统的交互过程,由若干行为和会话组成的特定序列构成,也被称为用例的实例。
    • 用例事实上就是一系列场景的集合,例如:主场景,正零路径或者是其他备选流
    • 主场景就是最常用的一个业务场景,反映的是用户最为基本的目的,简单的说就是这个系统所实现的基本业务
  • 用例有哪些形式?
    • 摘要 (Brief):一段总结,通常是主要的成功场景的建立,只需要几分钟的时间
    • 简便格式 (Casual):不是很正式的形式,并且包括了不同的场景
    • 详述 (Fully):完整的场景建立,完整的步骤以及用例的细节,以及一些可能发生的情况的应对以及如何确保一个成功的场景
  • 对于复杂业务,为什么编制完整用例非常难?
    • 因为复杂业务的子用例非常多,流程复杂,且需要处理的场景很多,很难考虑完全所有子用例和场景,绘制的用例图繁杂,容易出错
  • 什么是用例图?
    • 用例图是表示系统上下文的一张图片,它显示了系统的边界,展示了与系统交互的外部对象,描述了系统的使用方法
  • 用例图的基本符号与元素?
    • 参与者 (Actor):表示一个系统用户,包括与应用程序进行交互的用户、组织或外部系统
    • 用例 (Use Case):表示一个用例,通常用作对系统提供的功能、服务的一种描述
    • 包含关系 (Includes)
    • 扩展关系 (Extends)
    • 关联关系 (Association)
  • 用例图的画法与步骤:
    • 确定系统边界
    • 识别 Actors
      • 识别使用系统的主要参与者 (primary actors) / 角色(roles)
        • 使用用例图 actor符号表示,通常放在系统的左边
        • 企业应用可以通过企业组织架构,业务角色与职责识别
        • 互联网应用则必须通过市场分析,确定受众范围
        • 千万不要用“用户”代表系统使用者,以避免过于通用导致缺乏用户体验。例如,你的系统服务对象是程序员,但你必须明白 c/c++ 程序员、java 程序员、 PHP 程序员之间的相同与不同
      • 识别系统依赖的外部系统
        • 使用用例图 Neighboursystem框 表示用例依赖的外部系统、服务、设备,并使用构造型(Stereotype)识别
          • <> 例如:Account System(财务系统),教务系统
          • <> 例如:第三方身份认证、地理信息服务、短信服务等第三方在线服务
          • <> 或 <> 例如:GPS 等等
    • 识别用例 (服务)
      • 识别用户级别用例(user goal level)
        • 以主要参与者目标驱动
        • 收集主要参与者的业务事件
        • 必须满足以下准则
          • boss test
          • EBP test
          • Size Test
        • manage 用例。特指管理一些事物的 CRUD 操作,例如管理文件、管理用户等
      • 识别子功能级别的用例(sub function level)
        • 子用例特征
          • 业务复用。例如:现金支付
          • 复杂业务分解。将业务分解为若干步,便于交互设计与迭代实现
          • 强调技术或业务创新。例如:“识别人脸”,尽管从用户角度是单步操作,但背后涉及技术解决方案是比较复杂的
        • 正确使用用例与子用例之间的关系
          • <> 表示子用例是父用例的一部分,通常强调离开这个特性,父用例无法达成目标或失去意义!
          • <> 表示子用例是父用例的可选场景或技术特征。
          • <> 箭头指向子用例;<> 箭头指向父用例。箭头表示的依赖关系!
    • 建立 Actor 和 Use Cases 之间的关联
      • 请使用 无方向连线,表示两间之间是双向交互的协议
  • 用例图给利益相关人与开发者的价值有哪些?
    • 用例图对于利益相关者来说,他们可以非常直观的了解到自己所要实现的功能是否被很好的体现,开发者是否理解了自己的需求;而对于开发者来说,这不仅是向客户传递自己对需求的理解,也是方便开发者进行系统设计和开发

建模练习题(用例模型)

Ctrip Use Case

  • 然后,回答下列问题

    • 为什么相似系统的用例图是相似的?

      • 因为相似系统提供的服务是相似或相同的,面对的用户和用例是相似的,而用例之间的关系是由服务确定的,也是相似的,即使有特色的扩展服务,也是在基本业务上的扩展,都是为了满足相同的需求而提出的,所以最终相似系统的用例图是相似的
    • 如果是定旅馆业务,请对比 Asg_RH 用例图,简述如何利用不同时代、不同地区产品的用例图,展现、突出创新业务和技术

      • 在现在,可以使用大数据的方法分析每个用户对旅馆类型、价格、位置等的偏好,给每个用户推荐最适合他们的旅馆
      • 对于不同地区的旅馆,可以结合当地特色,在用户选择时为用户介绍,帮助用户做出最佳的选择
    • 如何利用用例图定位创新思路(业务创新、或技术创新、或商业模式创新)在系统中的作用

      • 在用例图中对创新用例使用某种颜色进行高亮标记,可以很方便地让需求方、开发人员快速了解该系统的创新功能以及该模块相关依赖和输入输出结果
    • 请使用 SCRUM 方法,选择一个用例图,编制某定旅馆开发的需求(backlog)开发计划表

ID Title Est Imp Demo
1 登录 2 20 官方、微信、阿里账号登录
2 预定酒店 5 30 搜索酒店、管理订单
3 搜索酒店 2 40 多种方式搜索酒店
4 城市搜索 2 30 按城市搜索
5 地图搜索 10 20 调用第三方地图API来按地图搜索
6 标志物搜索 5 10 按标志物来搜索
7 订单管理 3 30 添加、删除、修改、查询订单
8 支付 3 40 使用银行卡、微信、支付宝支付
  • 根据任务4,给出项目用例点的估算
用例 # 事务 # 计算 原因 UC 权重
登录 3 2 简单
预定酒店 8 8 复杂性
搜索酒店 4 3 复杂性
城市搜索 1 1 简单
地图搜索 1 1 简单
标志物搜索 1 1 简单
订单管理 4 4 平均
支付 3 3 平均

SWAD | Homework3

简述瀑布模型、增量模型、螺旋模型(含原型方法),并分析优缺点

从项目特点、风险特征、人力资源利用角度思考

(以下部分为个人观点)

瀑布模型

瀑布模型将软件生命周期划分为制定计划、分析、设计、编码、测试、运行维护等阶段,其规定了上述阶段必须自上而下,像瀑布一样。本阶段活动的工作对象来自于上一项活动的输出,这些输出一般是代表本阶段活动结束的里程碑式的文档,本阶段活动产出相关的软件工件,作为下一阶段活动的输入。改进的瀑布模型可以在下游阶段发现缺陷后返回上一个阶段,或者如果下游阶段认为不足则返回到设计阶段。

优点

  1. 降低软件开发的复杂程度,提高软件开发过程的透明性,提高软件开发过程的可管理性,可适用于大型项目
  2. 不同人员可以只根据上一阶段的文档,进行本阶段的工作,有利于人员的组织和管理

缺点

  1. 缺乏灵活性,尤其无法解决软件需求不明确或不准确的问题
  2. 风险控制能力较弱
  3. 管理人员如果仅仅以文档的完成情况来评估项目完成进度,往
    往会产生错误的结论

增量模型

增量模型采用随着日程时间的进展而交错的线性序列,每一个线性序列产生软件的一个可发布的“增量”。其首先对系统最核心或最清晰的需求进行分析、设计、实现、测试并集成到系统中,再按优先级逐步实现后续需求。增量模型强调每一个增量均发布一个可操作的产品。

优点

  1. 提高系统可靠性
  2. 降低系统失败风险
  3. 通过同一个团队的工作来交付每个增量,保持所有团队处于工作状态,减少了员工的工作量,工作分布曲线通过项目中的时间阶段被拉平。

缺点

  1. 建立初始模型时,作为增量基础的基本业务服务的确定有一定难度
  2. 增量粒度难以选择

螺旋模型

螺旋模型结合了瀑布模型和快速原型方法,将瀑布模型的多个阶段转化到多个迭代过程中,以降低项目的风险。螺旋模型最大的特点在于引入了其他模型不具有的风险分析,使软件在无法排除重大风险时有机会停止,以减小损失。同时,在每个迭代阶段构建原型是螺旋模型用以减少风险的途径。螺旋模型的每一次迭代都包含了以下六个步骤:

  • 决定目标、替代方案和约束条件
  • 识别和解决项目的风险
  • 评估技术方案和替代方案
  • 开发本次迭代的交付物,并验证迭代产出的正确性
  • 计划下一次迭代
  • 提交下一次迭代的步骤和方案

优点

  1. 整体过程具备很高的灵活性,在开发过程的任何阶段自由应对变化
  2. 引入风险分析,使软件在无法排除重大风险时有机会停止,以减小损失

缺点

  1. 螺旋模型强调风险分析,但说服外部客户接受和相信分析结果并做出相关反应并不容易,因此螺旋模型往往比较适合内部的大规模软件开发。
  2. 风险分析需要耗费相当的成本,因此螺旋模型比较适合投资规模较大的软件项目。
  3. 失误的风险分析可能带来更大的风险。

简述统一过程三大特点,与面向对象的方法有什么关系?

三大特点

  1. 用例驱动:用例驱动表明开发过程是沿着一个流(一系列从用例得到的工作流)前进的:用例被确定、用例被设计、用例被测试(最后用例又成为测试人员构造测试用例的基础)
  2. 以架构为中心:软件构架包含了系统中最重要的静态和动态特征。构架刻画了系统的整体设计,去掉了细节部分,突出了系统的重要特性。
  3. 迭代和增量的:软件开发是一项复杂的过程,因此可以将这些项目划分为切实可行并能够产生一个增量的迭代过程。迭代是指工作流中的步骤,增量是指产品中增加的部分。

它将软件开发过程要素和软件工件要素整合在统一的软件工程框架中,是一个面向对象的程序开发方法论。

简述统一过程四个阶段的划分准则是什么?每个阶段关键的里程碑是什么?

统一过程中的软件生命周期在时间维度上被分解为四个顺序的阶段:初始阶段 (Inception)、精化阶段 (Elaboration)、构建阶段(Construction) 和产品交付阶段 (Transition)。

  1. 初始阶段 (Inception):为系统建立业务案例 (Business Case) 并确定项目的边界。
    • 里程碑:生命周期目标 (Lifecycle Objective) 里程碑,包括一些重要的文档,如:项目构想 (Vision)、原始用例模型、原始业务风险评估、一个或者多个原型、原始业务案例等。通过对文档的评审确定用例需求理解正确、项目风险评估合理、阶段计划可行等
  2. 精化阶段 (Elaboration):分析问题领域,建立健全的体系结构基础,编制项目计划,完成项目中高风险需求部分的开发。
    • 里程碑:生命周期体系结构 (Lifecycle Architecture) 里程碑,包括风险分析文档、软件体系结构基线、项目计划、可执行的进化原型、初始版本的用户手册等。通过评审确定软件体系结构已经稳定、高风险的业务需求和技术机制已经解决、修订的项目计划可行等。
  3. 构建阶段(Construction):完成所有剩余的技术构件和稳定业务需求功能的开发,并集成为产品,详细测试所有功能。构建阶段只是一个制造过程,其重点放在管理资源及控制开发过程以优化成本、进度和质量。
    • 里程碑:初始运行能力 (Initial Operational Capability) 里程碑,包括可以运行的软件产品、用户手册等,它决定了产品是否可以在测试环境中进行部署。此刻,要确定软件、环境、用户是否可以开始系统的运行。
  4. 产品交付阶段 (Transition):确保软件对最终用户是可用的。产品化阶段可以跨越几次迭代,包括为发布做准备的产品测试,基于用户反馈的少量调整。
    • 里程碑:产品发布 (Product Release) 里程碑,确定最终目标是否实现,是否应该开始产品下一个版本的另一个开发周期。在一些情况下这个里程碑可能与下一个周期的初始阶段相重合。

软件企业为什么能按固定节奏生产、固定周期发布软件产品?它给企业项目管理带来哪些好处?

统一过程为企业按固定节奏生产、固定周期发布软件产品提供了依据。

统一过程中的软件生命周期在时间维度上被分解为四个顺序的阶段:初始阶段 (Inception)、精化阶段 (Elaboration)、构建阶段(Construction) 和产品交付阶段 (Transition),可以使企业能有一个固定的节奏来生产;统一过程的迭代性,使得项目组能周期性地交付产品。

给企业项目管理带来的好处:

  1. 在软件开发的早期就可以对关键的,影响大的风险进行处理
  2. 可以提出一个软件体系架构来指导开发
  3. 可以较早的得到一个可运行的系统,鼓舞开发团队的士气,增强项目成功的信心。

SWAD | Homework2

简答题

用简短的语言给出对分析、设计的理解

  • 分析:对问题和需求的进行调查研究,关注系统需要”实现什么”
  • 设计:定义系统或组件的体系结构、组件、接口和其他特征的过程。

用一句话描述面向对象的分析与设计的优势

面向对象的分析与设计让不同领域的人沟通更加地方便,系统更加简单易懂,因此更容易维护复用。

简述 UML(统一建模语言)的作用

UML(统一建模语言)是软件工程领域中一种通用的、开发的、建模语言,旨在可视化系统分析与设计的结果

考试考哪些图

  • 用例图:用户角度:功能、执行者
  • 静态图:系统静态结构
    • 类图:概念及关系
    • 对象图:某种状态或时间段内,系统中活跃的对象及其关系
    • 包图:描述系统的分解结构
  • 行为图:系统的动态行为
    • 交互图:描述对象间的消息传递
      • 顺序图:强调对象间消息发送的时序
      • 合作图:强调对象间的动态写作关系
    • 状态图:对象的动态行为。状态 - 事件 - 状态迁移 - 响应动作
    • 活动图:描述系统为完成某功能而执行的操作序列
  • 实现图:描述系统的组成和分布状况
    • 构件图:组成部件及其关系
    • 部署图:物理体系结构及与软件单元的对应关系

从软件本质的角度,解释软件范围(需求)控制的可行性

由于软件本身的复杂性、不可见性、不一致性、可变性,软件范围多数情况下对于客户和开发者都是模糊的,这形成软件产品与其他产品不同的开发过程。在多数情况下,客户与开发者能就项目的 20% 内容给出严格的需求约定,80% 的内容都是相对模糊的。换言之,围绕客户目标,发现并满足客户感兴趣的内容是最关键的,这表示软件范围控制是可行的。

项目管理实践

看板使用练习

image-20190412140818437

UML绘图工具练习

选自教材P49 Figure 1.5

SWAD | Homework1

简答题

  • 软件工程的定义

    软件工程是研究和应用如何以系统性的、规范化的、可定量的过程化方法去开发和维护软件,以及如何把经过时间考验而证明正确的管理技术和当前能够得到的最好的技术方法结合起来的学科。它涉及到程序设计语言、数据库、软件开发工具、系统平台、标准、设计模式等方面。

  • 解释导致 software crisis 本质原因、表现,述说克服软件危机的方法

    • 本质原因:由于软件的大量需求与软件生产力效率之间的矛盾以及软件系统的复杂性与软件开发方法之间的矛盾
    • 表现:
      • 项目运行超出预算
      • 项目运行超过时间
      • 软件质量低落
      • 软件通常不匹配需求
      • 项目无法管理,且代码难以维护
    • 克服软件危机的方法:
      • 借鉴其他工程项目中行之有效的原理、概念、技术与方法,特别是吸取人类从事计算机硬件研究和开发的经验教训。在开发软件的过程中做到良好的组织,严格的管理,相互友好的协作。
      • 推广在实践中总结出来的开发软件的成功的技术和方法,并研究更好、更有效的技术和方法,尽快克服在计算机系统早期发展阶段形成的一些错误概念和做法。
      • 根据不同的应用领域,开发更好的软件工具并使用这些工具。将软件开发各个阶段使用的软件工具合成一个整体,形成一个良好的软件开发环境。
  • 软件生命周期

    在时间维度,对软件项目任务进行划分,又成为软件开发过程。常见有瀑布模型、螺旋模型、敏捷的模型等。

  • SWEBoK 的 15 个知识域

    • Software Requirements 软件需求

      软件需求表达了对软件产品的需求和约束,这些需求和约束有助于解决一些实际问题。

    • Software Design 软件设计

      设计是定义系统或组件的体系结构、组件、接口和其他特征的过程、以及该过程的结果。

    • Software Construction 软件构造

      通过详细设计、编码、单元测试、集成测试、调试和验证相结合,对工作软件进行详细的创建。

    • Software Testing 软件测试

      为了验证产品质量并且通过指出缺陷来增进软件质量的活动。

    • Software Maintenance 软件维护

      包括增强现有的功能,使得软件在新的环境中运行以及修正缺陷。

    • Software Configuration Management 软件配置管理

      软件配置管理(Software Configuration Management): 系统的配置是硬件、固件、软件或它们的组合的功能和/或物理特征。它还可以看作是硬件、固件或软件项目的特定版本的集合,为了服务于特定的目的,将这些硬件、固件和软件项目根据特定的构建过程组合在一起。因此,软件配置管理(SCM)是在不同的时间点识别系统配置的规程,以便系统地控制配置的更改,并在整个软件生命周期中维护配置的完整性和可追溯性。

    • Software Engineering Management 软件工程管理

      包括计划、协调、测量、报告和控制一个项目或程序,确保软件的开发和维护是系统的、规范化的、可定量的。

    • Software Engineering Process 软件工程过程

      软件工程知识领域关注软件生命周期过程的定义,实现,评估,测量,管理和改进。主题包括过程实现和改变(过程基础设施,过程实现和改变模型,和软件过程管理),过程定义(软件生命周期模型和过程,过程定义,过程适应和自动化过程的符号),过程评估模型和方法,测量(过程测量,产品测量,技术测量和测量结果质量)和软件过程工具。

    • Software Engineering Models and Methods 软件工程模型和方法

      软件工程技术和管理的原理、原则、方法

    • Software Quality 软件质量

      软件质量是软件生命周期普遍关注的,存在于许多 SWEBOK V3 知识领域中。此外,软件质量知识领域包括软件质量基础(软件工程文化,软件质量特征,软件质量价值和成本和软件质量改进),软件质量管理过程(软件质量保证,认证和确认,审核和审计)和实际考量(缺陷特征,软件质量测量和软件质量工具)。

    • Software Engineering Professional Practice 软件工程专业实践

      软件工程专业实践关注软件工程师必须具备的知识,技巧和态度,以用一种专业,负责和正直的态度来时间软件工程。软件工程专业实践知识领域涵盖专业性(专业行为,专业协会,软件工程标准,雇佣合同和法律问题),道德准则,动态小组(团队合作,认知问题复杂性,与利益相关者交互,不确定性和模糊性的处理,多元环境处理)和交流技巧。

    • Software Engineering Economics 软件工程经济学

      软件工程经济学知识领域关注在商业环境中做出决策,以使技术决策和商业目标达成一致。

    • Computing Foundations 计算基础

      计算基础知识领域涵盖对于软件工程实践必须的计算背景的基础主题。涵盖主题包括问题解决技术,抽象,算法和复杂度,编程基础,并行和分布式计算基础,计算机组成,操作系统和网络通信。

    • Mathematical Foundations

      数学基础知识领域涵盖对于软件工程实践必须的数学背景的基础主题。涵盖的主题包括集合,关系和函数,基础命题和谓词逻辑,证明技术,图和树,离散概率,语法和有限状态机,数论。

    • Engineering Foundations

      工程基础知识领域涵盖对于软件工程实践必须的工程背景的基础主题。涵盖的主题包括经验方法和实验技术,统计分析,测量和指标,工程设计,模拟和建模,根本原因分析。

  • CMMI 的五个级别

    • Level 1 - Initial: 没有可预知的生产过程,缺乏控制和应变能力
    • Level 2 - Managed: 生产过程为每个项目特制,一般有一定的应变能力。
    • Level 3 - Defined: 生产过程为组织所定制,并且积极主动。
    • Level 4 - Quantitatively Managed: 生产过程可测度(预知)且可控。
    • Level 5 - Optimizing: 生产专注于过程优化。
  • 用自己语言简述 SWEBok 或 CMMI

    CMMI 全称为能力成熟度模型集成,其本质上就是衡量软件工程的成熟度的五个级别。CMMI可以帮助企业确认当前软件工程处于的级别并且对其进行管理和改进,增强软件的开发和改进能力。其目的就在于使得软件可以在限定时间和预算内完成并且保证其高质量。CMMI分为五个级别,分别是无序、已管理、已定义、量化管理和优化,这五个级别对于软件工程的管理规范一级比一级要严格,是一个渐进性的模型框架,指导了企业对于现有的软件工程进行评估和改进以及开始新的系统性规范化的软件工程。其主要关注点在于:成本效益、明确重过程集中和灵活性。

服务计算 | gorilla/mux 源码解析

gorilla/mux 源码解析

在开始阅读 gorilla/mux 源码之前,不妨让我们看看 Golangnet/http 包是怎么实现多路复用的。

net/http

用一个简单的例子过一下流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Hello world, the web server
package main

import (
"io"
"log"
"net/http"
)

func main() {
helloHandler := func(w http.ResponseWriter, req *http.Request) {
io.WriteString(w, "Hello, world!\n")
}

http.HandleFunc("/hello", helloHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}

首先我们调用了 http.HandleFunc 来为 /hello 注册一个 Handler

1
http.HandleFunc("/hello", helloHandler)

依次调用下面三个函数/方法,第一个函数表明当我们使用 http.HandleFunc 时默认是注册在 DefaultServeMux 这个默认的多路复用器上;第二个方法中 HandlerFunc(handler) 意思是将 handler 强制转化为 HandlerFunc 因为其实现了 http.Handler 这个接口;第三个方法在 mux.m 中查找是否已为该 URL 注册 handler ,如果有就报错,没有就为其注册:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// ServeMux *
type ServeMux struct {
mu sync.RWMutex
m map[string]muxEntry
hosts bool // whether any patterns contain hostnames
}

// HandleFunc registers the handler function for the given pattern
// in the DefaultServeMux.
// The documentation for ServeMux explains how patterns are matched.
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
DefaultServeMux.HandleFunc(pattern, handler)
}

// HandleFunc registers the handler function for the given pattern.
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
mux.Handle(pattern, HandlerFunc(handler))
}

// Handle registers the handler for the given pattern.
// If a handler already exists for pattern, Handle panics.
func (mux *ServeMux) Handle(pattern string, handler Handler) {
mux.mu.Lock()
defer mux.mu.Unlock()

if pattern == "" {
panic("http: invalid pattern")
}
if handler == nil {
panic("http: nil handler")
}
if _, exist := mux.m[pattern]; exist {
panic("http: multiple registrations for " + pattern)
}

if mux.m == nil {
mux.m = make(map[string]muxEntry)
}
mux.m[pattern] = muxEntry{h: handler, pattern: pattern}

if pattern[0] != '/' {
mux.hosts = true
}
}

接下来是调用:

1
http.ListenAndServe(":8080", nil)

本文不打算追踪启动 server 的整个过程,不了解或者感兴趣的可以看看 Go的http包详解 。我们只挑对我们讲解多路复用感兴趣的部分来讲解,下面看看:

1
2
3
4
5
6
7
8
9
10
func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
handler := sh.srv.Handler
if handler == nil {
handler = DefaultServeMux
}
if req.RequestURI == "*" && req.Method == "OPTIONS" {
handler = globalOptionsHandler{}
}
handler.ServeHTTP(rw, req)
}

上面的方法表明当我们在调用 http.ListenAndServe(":8080", nil) 没有给 handler 的时候,默认使用 DefaultServeMux ,所以上面的例子调用的是 DefaultServeMuxServeHTTP

1
2
3
4
5
6
7
8
9
10
11
12
13
// ServeHTTP dispatches the request to the handler whose
// pattern most closely matches the request URL.
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
if r.RequestURI == "*" {
if r.ProtoAtLeast(1, 1) {
w.Header().Set("Connection", "close")
}
w.WriteHeader(StatusBadRequest)
return
}
h, _ := mux.Handler(r)
h.ServeHTTP(w, r)
}

让我们看看 DefaultServeMux 是如何查找相应的 handler ,看看 match 的实现,只能简单地实现字符串匹配,用起来不够爽吧?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Find a handler on a handler map given a path string.
// Most-specific (longest) pattern wins.
func (mux *ServeMux) match(path string) (h Handler, pattern string) {
// Check for exact match first.
v, ok := mux.m[path]
if ok {
return v.h, v.pattern
}

// Check for longest valid match.
var n = 0
for k, v := range mux.m {
if !pathMatch(k, path) {
continue
}
if h == nil || len(k) > n {
n = len(k)
h = v.h
pattern = v.pattern
}
}
return
}

当然,复杂也可能意味着性能下降,有兴趣的同学可以看看 Go HTTP Router Benchmark ,毕竟系统是拿来用的,不只是拿来写的。不过作为一个合格的程序员,应该学会用轮子,所以让我们看看 gorilla/mux 是怎么实现路由的吧。

gorilla/mux

一个简单使用的例子(摘自官方文档):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"net/http"
"log"
"github.com/gorilla/mux"
)

func YourHandler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Gorilla!\n"))
}

func main() {
r := mux.NewRouter()
// Routes consist of a path and a handler function.
r.HandleFunc("/", YourHandler)

// Bind to a port and pass our router in
log.Fatal(http.ListenAndServe(":8000", r))
}

一些进阶用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 可以匹配 URL 中的变量
r := mux.NewRouter()
r.HandleFunc("/products/{key}", ProductHandler)
r.HandleFunc("/articles/{category}/", ArticlesCategoryHandler)
r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler)

// 使用匹配的变量
func ArticlesCategoryHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "Category: %v\n", vars["category"])
}

// 路径前缀
r.PathPrefix("/products/")

// Only matches if domain is "www.example.com".
r.Host("www.example.com")
// Matches a dynamic subdomain.
r.Host("{subdomain:[a-z]+}.domain.com")

// 子路由
r := mux.NewRouter()
s := r.Host("www.example.com").Subrouter()

ok,总之它的功能比官方包要强大的多,下面开始源码分析。

mux.go

先看看其数据结构,与 http.ServeMux 相比,要多出不少东西,我们主要关注 routesparent 以及三个 flagstrictSlashskipCleanuseEncodedPath

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// This will send all incoming requests to the router.
type Router struct {
// Configurable Handler to be used when no route matches.
NotFoundHandler http.Handler

// Configurable Handler to be used when the request method does not match the route.
MethodNotAllowedHandler http.Handler

// Parent route, if this is a subrouter.
parent parentRoute
// Routes to be matched, in order.
routes []*Route
// Routes by name for URL building.
namedRoutes map[string]*Route
// See Router.StrictSlash(). This defines the flag for new routes.
strictSlash bool
// See Router.SkipClean(). This defines the flag for new routes.
skipClean bool
// If true, do not clear the request context after handling the request.
// This has no effect when go1.7+ is used, since the context is stored
// on the request itself.
KeepContext bool
// see Router.UseEncodedPath(). This defines a flag for all routes.
useEncodedPath bool
// Slice of middlewares to be called after a match is found
middlewares []middleware
}

其中三个 flag 只要看注释就知道它们的作用了,不多解释:

  • strictSlash

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // StrictSlash defines the trailing slash behavior for new routes. The initial
    // value is false.
    //
    // When true, if the route path is "/path/", accessing "/path" will perform a redirect
    // to the former and vice versa. In other words, your application will always
    // see the path as specified in the route.
    //
    // When false, if the route path is "/path", accessing "/path/" will not match
    // this route and vice versa.
    func (r *Router) StrictSlash(value bool) *Router {
    r.strictSlash = value
    return r
    }
  • skipClean

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // SkipClean defines the path cleaning behaviour for new routes. The initial
    // value is false. Users should be careful about which routes are not cleaned
    //
    // When true, if the route path is "/path//to", it will remain with the double
    // slash. This is helpful if you have a route like: /fetch/http://xkcd.com/534/
    //
    // When false, the path will be cleaned, so /fetch/http://xkcd.com/534/ will
    // become /fetch/http/xkcd.com/534
    func (r *Router) SkipClean(value bool) *Router {
    r.skipClean = value
    return r
    }
  • useEncodedPath

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // UseEncodedPath tells the router to match the encoded original path
    // to the routes.
    // For eg. "/path/foo%2Fbar/to" will match the path "/path/{var}/to".
    //
    // If not called, the router will match the unencoded path to the routes.
    // For eg. "/path/foo%2Fbar/to" will match the path "/path/foo/bar/to"
    func (r *Router) UseEncodedPath() *Router {
    r.useEncodedPath = true
    return r
    }

先看看它的 HandleFunc ,首先 NewRoute 创建一个 Route ,然后 Pathpath 添加一个 matcher ,最后 HandlerFunc 为其设置一个 handler

1
2
3
4
5
6
// HandleFunc registers a new route with a matcher for the URL path.
// See Route.Path() and Route.HandlerFunc().
func (r *Router) HandleFunc(path string, f func(http.ResponseWriter,
*http.Request)) *Route {
return r.NewRoute().Path(path).HandlerFunc(f)
}

其次看看 RouterServeHTTP ,先使用上面说的 flagRequest 中的 URL 进行预处理,然后使用 RouterMatch 方法查找 handler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// ServeHTTP dispatches the handler registered in the matched route.
//
// When there is a match, the route variables can be retrieved calling
// mux.Vars(request).
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if !r.skipClean {
path := req.URL.Path
if r.useEncodedPath {
path = req.URL.EscapedPath()
}
// Clean path to canonical form and redirect.
if p := cleanPath(path); p != path {

// Added 3 lines (Philip Schlump) - It was dropping the query string and #whatever from query.
// This matches with fix in go 1.2 r.c. 4 for same problem. Go Issue:
// http://code.google.com/p/go/issues/detail?id=5252
url := *req.URL
url.Path = p
p = url.String()

w.Header().Set("Location", p)
w.WriteHeader(http.StatusMovedPermanently)
return
}
}
var match RouteMatch
var handler http.Handler
if r.Match(req, &match) {
handler = match.Handler
req = setVars(req, match.Vars)
req = setCurrentRoute(req, match.Route)
}

if handler == nil && match.MatchErr == ErrMethodMismatch {
handler = methodNotAllowedHandler()
}

if handler == nil {
handler = http.NotFoundHandler()
}

if !r.KeepContext {
defer contextClear(req)
}

handler.ServeHTTP(w, req)
}

再看看 RouterMatch 方法,遍历 Router 下的所有 route 看看是否匹配:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Match attempts to match the given request against the router's registered routes.
//
// If the request matches a route of this router or one of its subrouters the Route,
// Handler, and Vars fields of the the match argument are filled and this function
// returns true.
//
// If the request does not match any of this router's or its subrouters' routes
// then this function returns false. If available, a reason for the match failure
// will be filled in the match argument's MatchErr field. If the match failure type
// (eg: not found) has a registered handler, the handler is assigned to the Handler
// field of the match argument.
func (r *Router) Match(req *http.Request, match *RouteMatch) bool {
for _, route := range r.routes {
if route.Match(req, match) {
// Build middleware chain if no error was found
if match.MatchErr == nil {
for i := len(r.middlewares) - 1; i >= 0; i-- {
match.Handler = r.middlewares[i].Middleware(match.Handler)
}
}
return true
}
}
// 省略
...
}

在这里我们还不能看出 Router 是怎么实现如此强大的路由的,因为重点是 Route。

route.go

照例看看数据结构,有父路由 parent 、处理器 handler 、匹配器表 matchers 、正则表达式 regexp 以及前面提到的三个 flag 等:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// Route stores information to match a request and build URLs.
type Route struct {
// Parent where the route was registered (a Router).
parent parentRoute
// Request handler for the route.
handler http.Handler
// List of matchers.
matchers []matcher
// Manager for the variables from host and path.
regexp *routeRegexpGroup
// If true, when the path pattern is "/path/", accessing "/path" will
// redirect to the former and vice versa.
strictSlash bool
// If true, when the path pattern is "/path//to", accessing "/path//to"
// will not redirect
skipClean bool
// If true, "/path/foo%2Fbar/to" will match the path "/path/{var}/to"
useEncodedPath bool
// The scheme used when building URLs.
buildScheme string
// If true, this route never matches: it is only used to build URLs.
buildOnly bool
// The name used to build URLs.
name string
// Error resulted from building a route.
err error

buildVarsFunc BuildVarsFunc
}

还记得前面创建 Route 后使用的 Path 方法吧,它能为 URL path 添加一个正则表达式匹配器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Path adds a matcher for the URL path.
// It accepts a template with zero or more URL variables enclosed by {}. The
// template must start with a "/".
// Variables can define an optional regexp pattern to be matched:
//
// - {name} matches anything until the next slash.
//
// - {name:pattern} matches the given regexp pattern.
//
// For example:
//
// r := mux.NewRouter()
// r.Path("/products/").Handler(ProductsHandler)
// r.Path("/products/{key}").Handler(ProductsHandler)
// r.Path("/articles/{category}/{id:[0-9]+}").
// Handler(ArticleHandler)
//
// Variable names must be unique in a given route. They can be retrieved
// calling mux.Vars(request).
func (r *Route) Path(tpl string) *Route {
r.err = r.addRegexpMatcher(tpl, regexpTypePath)
return r
}

这里就不打算再深入下去探究如何添加的啦,一来作者水平有限,二来大家有不同的实现方式,所以我们就且停留到这一层吧。下面看看 RouteMatch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// Match matches the route against the request.
func (r *Route) Match(req *http.Request, match *RouteMatch) bool {
if r.buildOnly || r.err != nil {
return false
}

var matchErr error

// Match everything.
for _, m := range r.matchers {
if matched := m.Match(req, match); !matched {
if _, ok := m.(methodMatcher); ok {
matchErr = ErrMethodMismatch
continue
}
matchErr = nil
return false
}
}

if matchErr != nil {
match.MatchErr = matchErr
return false
}

if match.MatchErr == ErrMethodMismatch {
// We found a route which matches request method, clear MatchErr
match.MatchErr = nil
// Then override the mis-matched handler
match.Handler = r.handler
}

// Yay, we have a match. Let's collect some info about it.
if match.Route == nil {
match.Route = r
}
if match.Handler == nil {
match.Handler = r.handler
}
if match.Vars == nil {
match.Vars = make(map[string]string)
}

// Set variables.
if r.regexp != nil {
r.regexp.setMatch(req, match, r)
}
return true
}

到这里大家可能会疑惑了,// Match everything 指的是什么,其实一个 Route 可以存在多个 matcher,也就是说,你可能设置了一个这样的路由:

1
2
3
4
r.HandleFunc("/products", ProductsHandler).
Host("www.example.com").
Methods("GET").
Schemes("http")

那么这个路由就需要满足上述 4 个 matcher 才能够匹配成功,而 PathHost 都是通过添加 routeRegexp 来实现,MethodsSchemes 则分别实现了各自的 matcher

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Path and Host add RegexpMatcher
func (r *Route) Path(tpl string) *Route {
r.err = r.addRegexpMatcher(tpl, regexpTypePath)
return r
}

func (r *Route) Host(tpl string) *Route {
r.err = r.addRegexpMatcher(tpl, regexpTypeHost)
return r
}

// methodMatcher matches the request against HTTP methods.
type methodMatcher []string

func (m methodMatcher) Match(r *http.Request, match *RouteMatch) bool {
return matchInArray(m, r.Method)
}


// schemeMatcher matches the request against URL schemes.
type schemeMatcher []string

func (m schemeMatcher) Match(r *http.Request, match *RouteMatch) bool {
return matchInArray(m, r.URL.Scheme)
}

总结

你有可能看了我的分析以后更晕了,但是没关系,你只要记住 gorilla/mux 大致是怎么实现路由的就好:

  1. 使用 RouterHandleFuncHostMethodsSchemesHeaders 等方法会创建一个路由并且为其添加相应类型 matcher,当然你也可以使用 MatcherFunc 来创建自己的 matcher。如果你想为一个路由添加多个方法,那么你可以这样:

    1
    2
    3
    4
    r.HandleFunc("/products", ProductsHandler).
    Host("www.example.com").
    Methods("GET").
    Schemes("http)

    因为上面说的每个方法都会返回一个刚创建 Route 的指针,这样你就可以使用 Route 的同名方法来添加 matcher 了。

  2. gorilla/mux 会查找 Router 下的 Route 列表,找到是否有无匹配的路由,而 Route 会遍历其 matchers 列表来看看是否满足所有的 matchers,只要一个不匹配,则失败。