项目 内容
课程班级博客链接 班级博客
这个作业要求链接 作业要求
我的课程学习目标 学习软件工程的理论和知识,掌握软件开发流程,增强实践能力
这个作业在哪些方面帮助我实现学习目标 体验软件项目开发中的两人合作,练习结对编程(Pair programming), 掌握Github协作开发程序的操作方法
结对方学号-姓名 201871010202-曹盼盼
结对方本次博客作业链接 结对方博客链接
本项目Github的仓库链接地址 Github仓库地址

1、实验目的与要求

(1)体验软件项目开发中的两人合作,练习结对编程(Pair programming)。
(2)掌握Github协作开发程序的操作方法。

2、实验内容

  • 任务1:阅读《现代软件工程—构建之法》第3-4章内容,理解并掌握代码风格规范、代码设计规范、代码复审、结对编程概念;

  • 第3章(软件工程师的成长)的理论和知识点主要有:评价软件工程师水平的主要方法、技能的反面、TSP对个人的要求、软件工程师的思维误区等;第4章(两人合作)的理论和知识点主要有:代码规范、极限编程、两人合作的不同阶段、影响他人的技巧。

  • 代码风格规范

    • 代码风格规范。主要是文字上的规定,看似表面文章,实际上非常重要。代码风格的原则是:简明,易读,无二义性。
    • 缩进
        是用Tab键好,还是2、4、8个空格?
        结论:4个空格,在VS2005和其他的一些编辑工具中都可以定义Tab键扩展成为几个空格键。不用 Tab键的理由是Tab键在不同的情况下会显示不同的长度。4个空格的距离从可读性来说正好。
    • 行宽
        行宽必须限制,但是以前有些文档规定的80字符行宽太小了(以前的计算机/打字机显示行宽为80字符),现在时代不同了,可为100字符。
    • 括号
        在复杂的条件表达式中,用括号清楚地表示逻辑优先级。
    • 断行与空白的{ }行
        每个“{”和“}”都独占一行
    • 分行
        不要把多行语句放在一行上,更严格地说,不要把不同的变量定义在一行上。
    • 命名
        在变量面前加上有意义的前缀,就可以让程序员一眼看出变量的类型及相应的语义。这就是“匈牙利命名法”的用处。
      还有一些地方不适合用“匈牙利命名法”,比如,在一些强类型的语言(如C#)中,不同类型的值是不能做运算的,对类型有严格的要求,例如C# 中,if()语句只能接受bool值的表达式,这样就大大地防止了以上问题的发生。在这样的语言中,前缀就不是很必要的,匈牙利命名法则不适用了。Microsoft .Net Framework就不主张用这样的法则。
    • 下划线问题
        下划线用来分隔变量名字中的作用域标注和变量的语义,如:一个类型的成员变量通常用m_来表示。移山公司规定下划线一般不用在其他方面。
    • 大小写问题
        由多个单词组成的变量名,如果全部都是小写,很不易读,一个简单的解决方案就是用大小写区分它们。Pascal——所有单词的第一个字母都大写;Camel——第一个单词全部小写,随后单词随Pascal格式,这种方式也叫lowerCamel。一个通用的做法是:所有的类型/类/函数名都用Pascal形式,所有的变量都用Camel形式。类/类型/变量:名词或组合名词,如Member、ProductInfo等。函数则用动词或动宾组合词来表示,如get/set; RenderPage()。
    • 注释
        复杂的注释应该放在函数头,很多函数头的注释都是解释参数的类型等的,如果程序正文已经能够说明参数的类型in/out等,就不要重复!注释也要随着程序的修改而不断更新,一个误导(Misleading)注释往往比没有注释更糟糕。另外,注释(包括所有源代码)应只用ASCII字符,不要用中文或其他特殊字符,它们会极大地影响程序的可移植性。
  • 代码设计规范

    • 代码设计规范。牵涉到程序设计、模块之间的关系、设计模式等方方面面,这里有不少与具体程序设计语言息息相关的内容(如C/C++/Java/C#),但是也有通用的原则,这里主要讨论通用的原则。
    • 函数
        现代程序设计语言中的绝大部分功能,都在程序的函数(Function, Method)中实现,关于函数最重要的原则是:只做一件事,但是要做好。
    • goto
        函数最好有单一的出口,为了达到这一目的,可以使用goto。只要有助于程序逻辑的清晰体现,什么方法都可以使用,包括goto。
    • 错误处理
      1.参数处理
        在DeBug版本中,所有的参数都要验证其正确性。在正式版本中,从外部(用户或别的模块)传递过来的参数要验证其正确性。
      2.断言
        如何验证正确性?那就要用Assert(断言)。断言和错误处理是什么关系?当你觉得某事肯定如何,你可以用断言。Assert (p != NULL);然后可以直接使用变量p;如果你认为某事可能会发生,这时就要用错误处理。
    • 如何处理C++中的类
      注意,除了关于异常(Exception)的部分,大部分其他原则对C#也适用。
      1.类
      (1)使用类来封装面向对象的概念和多态(Polymorphism)。
      (2)避免传递类型实体的值,应该用指针传递。换句话说,对于简单的数据类型,没有必要用类来实现。
      (3)对于有显式的构造和析构函数,不要建立全局的实体,因为你不知道它们在何时创建和消除。
      (4)只有在必要的时候,才使用“类”。
      2.Class vs. Struct
        如果只是数据的封装,用Struct即可。
      3.公共/保护/私有成员Public、Private和Protected
        按照这样的次序来说明类中的成员:public、protected、private
      4.数据成员
      (1)数据类型的成员用m_name说明。
      (2)不要使用公共的数据成员,要用inline访问函数,这样可同时兼顾封装和效率。
      5.虚函数Virtual Functions
      (1)使用虚函数来实现多态(Polymorphism)。
      (2)只有在非常必要的时候,才使用虚函数。
      (3)如果一个类型要实现多态,在基类(Base Class)中的析构函数应该是虚函数。
      6.构造函数Constructors
      (1)不要在构造函数中做复杂的操作,简单初始化所有数据成员即可。
      (2)构造函数不应该返回错误(事实上也无法返回)。把可能出错的操作放到HrInit()或FInit()中。
      7.析构函数
      (1)把所有的清理工作都放在析构函数中。如果有些资源在析构函数之前就释放了,记住要重置这些成员为0或NULL。
      (2)析构函数也不应该出错。
      8.New和Delete
      (1)如果可能,实现自己的New/Delete,这样可以方便地加上自己的跟踪和管理机制。自己的New/Delete可以包装系统提供的New/Delete。
      (2)检查New的返回值。New不一定都成功。
      (3)释放指针时不用检查NULL。
      9.运算符(Operators)
      (1)在理想状态下,我们定义的类不需要自定义操作符。只有当操作符的确需要时。
      (2)运算符不要做标准语义之外的任何动作。例如,“==”的判断不能改变被比较实体的状态。
      (3)运算符的实现必须非常有效率,如果有复杂的操作,应定义一个单独的函数。
      (4)当你拿不定主意的时候,用成员函数,不要用运算符。
      10.异常(Exceptions)
      (1)异常是在“异乎寻常”的情况下出现的,它的设置和处理都要花费“异乎寻常”的开销,所以不要用异常作为逻辑控制来处理程序的主要流程。
      (2)了解异常及处理异常的花销,在C++语言中,这是不可忽视的开销。
      (3)当使用异常时,要注意在什么地方清理数据。
      (4)异常不能跨过DLL或进程的边界来传递信息,所以异常不是万能的。
      11.类型继承(Class Inheritance)
      (1)当有必要的时候,才使用类型继承。
      (2)用Const标注只读的参数(参数指向的数据是只读的,而不是参数本身)。
      (3)用Const标注不改变数据的函数。
  • 代码复审

    • 代码复审的正确定义:看代码是否在“代码规范”的框架内正确地解决了问题
名 称 形 式 目 的
自我复审 自己 vs. 自己 用同伴复审的标准来要求自己。不一定最有效,因为开发者对自己总是过于自信。如果能持之以恒,则对个人有很大好处
同伴复审 复审者 vs. 开发者 简便易行
团队复审 团队 vs. 开发者 有比较严格的规定和流程,用于关键的代码,以及复审后不再更新的代码。 覆盖率高——有很多双眼睛盯着程序。但是有可能效率不高(全体人员都要到会)
  复审的目的在于:
  (1)找出代码的错误。如:
    a. 编码错误,比如一些能碰巧骗过编译器的错误。
    b. 不符合项目组的代码规范的地方。
  (2)发现逻辑错误,程序可以编译通过,但是代码的逻辑是错的。
  (3)发现算法错误,比如使用的算法不够优化。
  (4)发现潜在的错误和回归性错误——当前的修改导致以前修复的缺陷又重新出现。
  (5)发现可能改进的地方。
  (6)教育(互相教育)开发人员,传授经验,让更多的成员熟悉项目各部分的代码,同时熟悉和应用领域相关的实际知识。
  • 4 结对编程

    • 结对编程(Pair programming)是一种敏捷软件开发的方法,两个程序员在一个计算机上共同工作,是极限编程的组成部分。一个人输入代码,称作驾驶员;另一个人负责审查工作,称作观察员(或导航员)。两人常互换角色。在结对编程中,观察员同时考虑工作的战略性方向,提出改进的意见,或将来可能出现的问题以便处理。
    • 首先,结对编程可以促进参与项目的程序员自身的提高,一对程序员工作的时候,水平较低的一方会潜移默化地受水平略高的程序员影响,学到一些新的东西。而水平高的一方同样因为不断地把自己的想法说出来而整理了自己的思路。其次,一定时间周期地打乱配对,让参与项目的人员相互转换位置,使得维护繁杂的文档变得不那么重要。大家分组打乱后,口头的交流很容易让所有人都熟悉每个模块,这样对于公司也很有好处,项目中万一有人离开,也不至于影响到整个项目。最后,开发过程变得更为有趣,任何人的交流变得很多,大家关系更为融洽。
  • 任务2:两两自由结对,对结对方《实验二 软件工程个人项目》的项目成果进行评价,具体要求如下:

  • (1)对项目博文作业进行阅读并进行评论,评论要点包括:博文结构、博文内容、博文结构与PSP中“任务内容”列的关系、PSP中“计划共完成需要的时间”与“实际完成需要的时间”两列数据的差异化分析与原因探究,将以上评论内容发布到博客评论区。

  • (2)克隆结对方项目源码到本地机器,阅读并测试运行代码,参照《现代软件工程—构建之法》4.4.3节核查表复审同伴项目代码并记录。

    • 概要部分
      1)代码符合需求和规格说明么?基本符合
      2)代码设计是否考虑周全?排序功能不完善
      3)代码可读性如何?可读性好
      4)代码容易维护么?容易
      5)代码的每一行都执行并检查过了吗?是
    • 设计规范部分
      1)设计是否遵从已知的设计模式或项目中常用的模式?是
      2)有没有硬编码或字符串/数字等存在?没有
      3)代码有没有依赖于某一平台,是否会影响将来的移植(如Win32到 Win64 ) ?没有
      4)开发者新写的代码能否用已有的Library/SDK/Framework中的功能实现?在本项目中是否存在类似的功能可以调用而不用全部重新实现?没有
      5)有没有无用的代码可以清除?没有
    • 代码规范部分
      修改的部分符合代码标准和风格吗? 修改的部分符合代码标准和风格
      具体代码部分
      1)有没有对错误进行处理?对于调用的外部函数,是否检查了返回值或处理了异常?没有
      2)参数传递有无错误,字符串的长度是字节的长度还是字符(可能是单/双字节)的长度,是以0开始计数还是以1开始计数?没有
      3)边界条件是如何处理的?switch语句的default分支是如何处理的?循环有没有可能出现死循环?没有出现死循环
      4)有没有使用断言(Assert)来保证我们认为不变的条件真的得到满足?没有使用
      5)对资源的利用,是在哪里申请,在哪里释放的?有无可能存在资源泄漏(内存、文件、各种GUI资源、数据库访问的连接,等等)?有没有优化的空间?对空间的优化较好,无数据库的访问链接
      6)数据结构中有没有用不到的元素? 没有
  • (3)依据复审结果尝试利用github的Fork、Clone、Push、Pull request、Merge pull request等操作对同伴个人项目仓库的源码进行合作修改。

    • fork:从别人发布的项目上复制一个过来,相当于一个分支;项目复制到自己的个github中,于是本地就有了一个仓库,假设名字为A;
    • clone: 从自己的github上把fork过来的复制到本地,这样本地就有了一个项目A1;
    • push:当你在A1中进行修改进行开发后,最后同步到你的github上的仓库中;
    • pull request:你把自己github中的已经修改的内容申请同步到最初那个开发者的项目中;
  • 任务3:采用两人结对编程方式,设计开发一款D{0-1}KP 实例数据集算法实验平台,使之具有以下功能:

  • (1)平台基础功能:实验二 任务3;
    (2)D{0-1}KP 实例数据集需存储在数据库;
    (3)平台可动态嵌入任何一个有效的D{0-1}KP 实例求解算法,并保存算法实验日志数据;
    (4)人机交互界面要求为GUI界面(WEB页面、APP页面都可);
    (5)查阅资料,设计遗传算法求解D{0-1}KP,并利用此算法测试要求(3);
    (6)附加功能:除(1)-(5)外的任意有效平台功能实现。

  • 需求分析陈述:
      设计开发一款D{0-1}KP 实例数据集算法实验平台,平台基础功能:正确读入实验数据文件的有效D{0-1}KP数据;能够绘制任意一组D{0-1}KP数据以重量为横轴、价值为纵轴的数据散点图; 能够对一组D{0-1}KP数据按项集第三项的价值:重量比进行非递增排序;用户能够自主选择动态规划算法、回溯算法求解指定D{0-1} KP数据的最优解和求解时间(以秒为单位);任意一组D{0-1} KP数据的最优解、求解时间和解向量可保存为txt文件或导出EXCEL文件。

  • 软件设计说明:
      提供人机交互界面GUI;输入输出文件;能够绘制任意一组D{0-1}KP数据以重量为横轴、价值为纵轴的数据散点图; 对一组D{0-1}KP数据按项集第三项的价值:重量比进行非递增排序;用户能够自主选择动态规划算法、回溯算法求解指定D{0-1} KP数据的最优解和求解时间(以秒为单位);遗传算法求解D{0-1}KP。

  • 软件实现及核心功能代码展示:

    • 遗传算法(Genetic algorithm)属于演化计算( evolutionary computing),是随着人工智能领域发展而来的一种智能算法。正如它的名字所示,遗传算法是受达尔文进化论启发。简单来说,它是一种通过模拟自然进化过程搜索最优解的方法。设Pr、Pc和Pm分别表示繁殖、杂交和变异三种操作的概率,遗传算法的一般过程如下
      1.设置种群个数N,随机初始化种群P(0),t=0;
      2.计算种群P(0)中个体的适应值;
      3.重复执行下述操作,直到满足终止条件
        3.1 根据个体适应值及选择策略确定P(内每个个体的选择概率P、
        3.2 对种群P()中的个体执行下述操作,直到取完种群中的所有个体;
          3.2.1 根据选择概率在P(t)中选择两个父体;
          3.2.2 在[0,1]区间随机确定一个随机数r;
          3.2.3 若r≤pr,则执行繁殖操作,将两个父体直接插到种群P(t+1)中;
            若r≤Pr十Pc,则执行杂交操作,并将两后代插到种群P(t+1)中
            否则,对两父体分别执行变异操作,并将两后代插到种群P(t+1)中;
        3.3 计算种群P(t+1)中个体的适应值,t=t+1;
      4.返回种群中适应值最大的个体。
    • 动态规划算法、回溯算法求解指定D{0-1} KP数据的最优解和求解时间(以秒为单位),主要设计函数有动态规划求解,回溯法求解,以及输出最优解;动态规划,对每一件物品遍历背包容量,当背包可容纳值大于等于当前物品,与之前已放进去的物品所得价值进行对比,考虑是否需要置换;用回溯法求解需要构造解的子集树。对于每一个物品i,对于该物品只有选与不选2个决策,总共有n个物品,可以顺序依次考虑每个物品,这样就形成了一棵解空间树:基本思想就是遍历这棵树,以枚举所有情况,最后进行判断,如果重量不超过背包容量,且价值最大的话,该方案就是最后的答案。在搜索状态空间树时,只要左子节点是可一个可行结点,搜索就进入其左子树。对于右子树时,先计算上界函数,以判断是否将其减去(剪枝)。
  • 测试运行:

  • 代码片段:

遗传算法:
import numpy as np
import random
import matplotlib.pyplot as plt
##初始化,N为种群规模,n为染色体长度
def init(N,n):
    C = []
    for i in range(N):
        c = []
        for j in range(n):
            a = np.random.randint(0,2)
            c.append(a)
        C.append(c)
    return C
##评估函数
# x(i)取值为1表示被选中,取值为0表示未被选中
# w(i)表示各个分量的重量,v(i)表示各个分量的价值,w表示最大承受重量
def fitness(C,N,n,W,V,w):
    S = []##用于存储被选中的下标
    F = []## 用于存放当前该个体的最大价值
    for i in range(N):
        s = []
        h = 0  # 重量
        f = 0  # 价值
        for j in range(n):
            if C[i][j]==1:
                if h+W[j]<=w:
                    h=h+W[j]
                    f = f+V[j]
                    s.append(j)
        S.append(s)
        F.append(f)
    return S,F
##适应值函数,B位返回的种族的基因下标,y为返回的最大值
def best_x(F,S,N):
    y = 0
    x = 0
    B = [0]*N
    for i in range(N):
        if y<F[i]:
            x = i
        y = F[x]
        B = S[x]
    return B,y
## 计算比率
def rate(x):
    p = [0] * len(x)
    s = 0
    for i in x:
        s += i
    for i in range(len(x)):
        p[i] = x[i] / s
    return p
## 选择
def chose(p, X, m, n):
    X1 = X
    r = np.random.rand(m)
    for i in range(m):
        k = 0
        for j in range(n):
            k = k + p[j]
            if r[i] <= k:
                X1[i] = X[j]
                break
    return X1
##交配
def match(X, m, n, p):
    r = np.random.rand(m)
    k = [0] * m
    for i in range(m):
        if r[i] < p:
            k[i] = 1
    u = v = 0
    k[0] = k[0] = 0
    for i in range(m):
        if k[i]:
            if k[u] == 0:
                u = i
            elif k[v] == 0:
                v = i
        if k[u] and k[v]:
            # print(u,v)
            q = np.random.randint(n - 1)
            # print(q)
            for i in range(q + 1, n):
                X[u][i], X[v][i] = X[v][i], X[u][i]
            k[u] = 0
            k[v] = 0
    return X
##变异
def vari(X, m, n, p):
    for i in range(m):
        for j in range(n):
            q = np.random.rand()
            if q < p:
                X[i][j] = np.random.randint(0,2)

    return X
动态规划:
def bag(n,c,w,v):
	res=[[-1 for j in range(c+1)] for i in range(n+1)]
	for j in range(c+1):
		res[0][j]=0
	for i in range(1,n+1):
		for j in range(1,c+1):
			res[i][j]=res[i-1][j]
			if j>=w[i-1] and res[i][j]<res[i-1][j-w[i-1]]+v[i-1]:
				res[i][j]=res[i-1][j-w[i-1]]+v[i-1]
	return res
回溯法:
def backtrack(i):
	global bestV,curW,curV,x,bestx
	if i>=n:
		if bestV<curV:
			bestV=curV
			bestx=x[:]
	else:
		if curW+w[i]<=c:
			x[i]=True
			curW+=w[i]
			curV+=v[i]
			backtrack(i+1)
			curW-=w[i]
			curV-=v[i]
		x[i]=False
		backtrack(i+1)
  • 程序运行


  • 结对过程
      由于我和结对同伴曹盼盼同学宿舍距离较近,所以我们的共同工作基本上都是在宿舍完成的,也通过社交软件如QQ、微信等工具互相分享了一些学习资料和心得。两个人结对编程完成项目也避免和及时发现了很多错误,提高了学习效率。

  • 结对作业PSP展示:

PSP2.1 任务内容 计划共完成需要的时间(min) 实际完成需要的时间(min)
lanning 计划 10 20
· Estimate · 估计这个任务需要多少时间,并规划大致工作步骤 10 20
Development 开发 330 480
· Analysis · 需求分析 (包括学习新技术) 30 30
· Design Spec · 生成设计文档 30 30
· Design Review · 设计复审 (和同事审核设计文档) 30 30
· Coding Standard · 代码规范 (为目前的开发制定合适的规范) 30 30
· Design · 具体设计 30 50
· Coding · 具体编码 120 180
· Code Review · 代码复审 30 50
· Test · 测试(自我测试,修改代码,提交修改) 30 80
Reporting 报告 40 60
· Test Report · 测试报告 15 20
· Size Measurement · 计算工作量 10 10
· Postmortem & Process Improvement Plan · 事后总结 ,并提出过程改进计划 15 30
  通过分析估计和实际消耗时间,发现在具体设计环节耗时最多,需求分析和具体设计阶段的实际用时比预期长了许多,主要是因为需求分析阶段需要完成查阅资料等工作,比较花费时间,而具体编码阶段需要复学学习算法等内容,并且还要学习项目中需要用到的以前未掌握的知识和技术。
  • 任务4:完成结对项目报告博文作业

3、实验总结

  本次实验主要是通过完成软件工程结对项目,体验软件项目开发中的两人合作,练习结对编程(Pair programming),并掌握Github协作开发程序的操作方法。对于此次的项目设计,我有很多地方还很不完善,本次结对编程项目是基于实验二个人项目的基础上完成,还需要查阅大量资料来学习遗传算法,以及熟悉web界面的设计。本次实验中,通过阅读和学习《构建之法—现代软件工程》第3-4章内容,理解和掌握了代码风格规范、代码设计规范、代码复审、结对编程概念;并在实验过程中通过实践,掌握了Github的fork、 clone、push、pull request 、merge pull request等的相关操作。此次结对编程项目使我初步感受到了结对编程的一些优势,但是毕竟是初次尝试,过程中也出现了一些小问题。结对编程减轻了个人的一部分工作量,而且让我深刻感受到在开发过程中思路更加开阔,也更不容易出错,也提高了相互的合作交流,虽然本次项目完成结果不完善,但是我认为还是优于一个人的,达到了一定的效果(1+1>2)。