201971020107-郭清华 实验二 个人项目—— 《KP Project》项目报告


项目 内容
班级博客 2022年春软件工程课程班
作业要求 实验二 软件工程个人项目
学习目标 掌握软件项目个人开发流程;掌握Github发布软件项目的操作方法
目标实现 已掌握Git及GitHub的基本操作;对软件开发中项目代码的规范要求有了新的认识;对PSP进行了应用,对个人开发进行实践;
项目地址 KPProject

任务1

  • 点评链接1:
  • 点评链接2:
  • 点评链接3:

任务2

构建之法第一章——概论

  1. 软件=程序+软件工程 软件企业=软件+商业模式
  2. 程序(算法、数据结构)是基本功,但是在算法和数据结构之上,软件工程决定了软件的质量;商业模式决定了一个软件企业的成败。软件从业人员和软件企业的道德操守会极大地影响软件用户的利益。
  3. 软件工程
    • 是把系统的、有序的、可量化的方法应用到软件的开发、运营和维护上的过程。
    • 软件工程包括下列领域:软件需求分析、软件设计、软件构建、软件测试和软件维护。
    • 软件工程和下列的学科相关:计算机科学、计算机工程、管理学、数学、项目管理学、质量管理、软件人体工学、系统工程、工业设计和用户体验设计。
  4. 软件的特性
    • 复杂性
    • 不可见性
    • 易变性
    • 服从性
    • 非连续性
  5. 软件工程与计算机科学的关系:计算机理论的进展会帮助软件工程(例如对程序正确性的分析);软件工程的进展(更好的工具,更多的应用领域)会帮助计算机科学家更有效地进行实验和探索。

构建之法第二章——个人技术和流程

  1. 好的单元测试的标准:
    • 单元测试应该在最基本的功能/参数上验证程序的正确性。
    • 单元测试必须由最熟悉代码的人来写。
    • 单元测试过后,机器状态保持不变。
    • 单元测试要快。
    • 单元测试应该产生可重复、一致的结果。
    • 独立性——单元测试的运行/通过/失败不依赖于别的测试,可以人为构造数据,以保持单元测试的独立性。
    • 单元测试应该覆盖所有代码路径。
    • 单元测试应该集成到自动测试的框架中。
    • 单元测试必须和产品代码一起保存和维护。
  2. 回归测试的目的:
    • 验证新的代码的确改正了缺陷
    • 同时要验证新的代码有没有破坏模块的现有功能,有没有Regression
  3. 效能分析:
    • 抽样(Sampling),抽样就是当程序运行时,Visual Studio时不时看一看这个程序运行在哪一个函数内,并记录下来。
    • 代码注入(Instrumentation),代码注人就是将检测的代码加入到每一个函数中,这样程序的一举一动都被记录在案,程序的各个效能数据都可以被精准地测量。
  4. 个人开发流程(Personal Software Process)的特点:
    • 不局限于某一种软件技术,而是着眼于软件开发的流程。
    • 不依赖于考试,而主要靠工程师自己收集数据,然后分析,提高。
    • 在小型、初创的团队中,很难找到高质量的项目需求,这意味着给程序员的输入质量不高。在这种情况下,程序员的输出(程序/软件)往往质量也不高,然而这并不能全部由程序员负责。
    • PSP依赖于数据。
    • PSP的目的是记录工程师如何实现需求的效率,而不是记录顾客对产品的满意度。

任务3

需求分析

  • 正确读入实验数据文件的有效{0-1}KP数据
  • 能够绘制任意一组{0-1}KP数据以价值重量为横轴、价值为纵轴的数据散点图
  • 能够对一组{0-1}KP数据按价值重量比进行非递增排序
  • 用户能够自主选择贪心算法、动态规划算法、回溯算法求解指定{0-1} KP数据的最优解和求解时间(以秒为单位)
  • 任意一组{0-1} KP数据的最优解、求解时间和解向量可保存为txt文件或导出EXCEL文件

功能设计

  • 数据读入:将用户选择的数据导入
  • 散点图绘制:将用户选择的数据进行散点图的绘制,横轴为重量,纵轴为价值
  • 数据排序:将用户选择的数据按照价值重量比非递增排序
  • 01背包问题求解:将用户选择的数据及方法进行问题求解,有以下三种方法:
    • 动态规划
    • 贪心
    • 回溯
  • 01背包问题求解得到的最优解,解向量,求解时间保存到相应的txt文件里
  • 扩展功能:将散点图绘制得到的散点图进行保存(已实现)

设计实现

总体设计

  该系统运行时会提示用户进行输入从而选择对应的功能,如果输入为“0”,则退出该系统;如果输入为1—4,则会转到对应的模块来实现用户的功能;当执行完对应功能后便会回到起始,即——提示用户选取相应的功能,直到用户输入“0”退出该系统为止。

工程结构

  • data:存放01背包实验数据

  • lib:存放引入的JFreeChart包

  • pic:存放导出的散点图

  • res:存放导出的解

  • src:源码

    • algorithm:该包中分别存放算法代码

      • BT.class 回溯法对应的代码
      • DP.class 动态规划法对应的代码
      • Greedy.class 贪心法对应的代码
    • entity:实体类包

      • Data.class 数据文件对应的实体类
    • utils:存放所用到的各种工具类

      • DataUtils.class 与数据处理有关的类
      • PicUtils.class 与散点图绘制的类
    • Main.class 程序的入口类

      类之间的关系

模块实现

实验数据查看

  数据查看的思路是通过Java的IO流中的FileReader来将文件按行读取并将数据封装在Data类中。

绘制散点图并导出

  散点图的绘制是通过JFreeChart这个图表绘制类库来绘制的,通过将数据保存在一个二维数组中,然后通过JFreeChart的DefaultXYDataset和ChartFactory.createScatterPlot()来进行散点图的绘制;

  散点图的导出是通过JFreeChart的工具类ChartUtilities.writeChartAsPNG()来将绘制的图像保存为一个png格式的图片。

对数据进行排序

  该任务的实现是通过冒泡排序的方式进行。

问题求解并导出

  • 动态规划法

  \(0/1\)背包问题满足最优性原理,因此可以通过动态规划法进行求解。

  该问题可以看作为一个决策序列\((x_1, x_2, ...,x_n)\) ,对任意一个变量 \(x_i\) 的决策是决定\(x_i\)\(1\)还是\(0\)。设 \(V(n,C)\) 表示将\(n\)个物品装入容量为\(C\)背包获得的最大价值,则初始子问题为把前\(i\)个物品装入容量为\(0\)的背包和把\(0\)个物品装入容量为\(j\)的背包中。即:

\[V(i, 0) = V(0, j) = 0 \qquad0 \leq i\leq n, 0\leq j\leq C \]

  设\(V(i,j)\)表示将前\(i(1≤i个物品装入容量为\(j(1≤j≤0)\)的背包获得的最大价值,在决策\(x_i\)时,已确定了\((x_1, x_2, ...,x_{i-1})\),则问题处于下列两种状态之一:

  • 背包容量不足以装入物品\(i\),则装入前i个物品和前\(i-1\)个物品的价值是一样的;

  • 背包容量可以装入物品\(i\),则选择装入前i个物品和前\(i-1\)个物品中价值大的哪个。

    有如下递推式:

    \[V(i,j)= \left\{ \begin{aligned} &V(i-1,\quad j) &j

    流程图

  • 贪心法

  先将物品按照价值重量比非递增排序,然后依次选取物品装入背包,直到无法装入为止。\(0/1\)背包问题不适合用贪心法,因为物品不能够被分割,背包可能无法装满,从而导致闲置的背包容量使得物品的单位重量价值降低。

  • 回溯法

  回溯法也就是从解空间树根节点出发,按照深度优先的策略对满足条件的解进行搜索。在该问题中,如果没有剪枝,则会导致程序运行时间过长,导致部分数据无法得到结果。

流程图

  • 结果导出

  通过Java IO流的FileWriter的writer方法将求得的结果存入对应的txt文件中

测试运行

  • 文件读入

  • 绘制散点图并导出

  • 拓展功能——将图片导出

  • 对数据进行排序

  • 问题求解及导出

  • 动态规划

  • 贪心

  • 回溯

  • 结果导出

代码片段

项目代码规范说明

方面 规范
缩进 缩进采用4个空格,禁止使用tab字符。
变量命名 使用lowerCamelCase风格,必须遵从驼峰形式。
每行最多字符数 单行字符数限制不超过 120个,超出需要换行。
函数、类命名 类名使用UpperCamelCase风格,必须遵从驼峰形式;方法名使用lowerCamelCase风格,必须遵从驼峰形式。
常量 常量命名全部大写,单词间用下划线隔开。
空行规则 方法体内的执行语句组、变量的定义语句组、不同的业务逻辑之间或者不同的语义之间插入一个空行。相同业务逻辑和语义之间不需要插入空行。
注释规则 类、类属性、类方法的注释必须使用Javadoc规范;所有的类都必须添加创建者信息;方法内部单行注释,在被注释语句上方另起一行,使用//注释。方法内部多行注释使用/* */注释,注意与代码对齐。
操作符前后空格 任何运算符左右必须加一个空格;if/for/while/switch/do等保留字与左右括号之间都必须加空格;方法参数在定义和传入时,多个参数逗号后边必须加空格。
包名 包名统一使用小写,点分隔符之间有且仅有一个自然语义的英语单词。
  • 散点图的绘制及导出
    /**
     * 画散点图
     *
     * @param data      数据
     * @param dataIndex 文件序号
     * @throws IOException
     */
    public static void scatterPlotPaint(Data data, int dataIndex) throws IOException {
        double[][] tempData = new double[2][data.getN()];
        //将Integer转换为int
        for (int i = 0; i < data.getN(); i++) {
            tempData[0][i] = Integer.parseInt(data.getW().get(i).toString());
        }
        for (int i = 0; i < data.getN(); i++) {
            tempData[1][i] = Integer.parseInt(data.getV().get(i).toString());
        }

        DefaultXYDataset defaultXYDataset = new DefaultXYDataset();
        //添加数据
        defaultXYDataset.addSeries(" ", tempData);
        //设置表头,x轴,y轴
        JFreeChart chart = ChartFactory.createScatterPlot("", "Weight", "Value", 		 defaultXYDataset, PlotOrientation.VERTICAL, true, true, false);
        XYPlot xyplot = (XYPlot) chart.getPlot();
        //设置背景面板颜色
        xyplot.setBackgroundPaint(Color.white);
        ValueAxis valueAxis = xyplot.getDomainAxis();
        //设置坐标轴粗细
        valueAxis.setAxisLineStroke(new BasicStroke(1.0f));

        //输出PNG文件
        OutputStream os_png = new FileOutputStream("pic/beibao" + dataIndex + ".png");
        ChartUtilities.writeChartAsPNG(os_png, chart, 500, 500);
        System.out.println("文件已导出到KP/pic/beibao" + dataIndex + ".png");

        //以面板显示
        ChartPanel chartPanel = new ChartPanel(chart);
        chartPanel.setPreferredSize(new java.awt.Dimension(560, 400));
        
        //创建一个主窗口来显示面板
        JFrame frame = new JFrame("散点图");
        frame.setLocation(500, 400);
        frame.setSize(600, 500);

        //将主窗口的内容面板设置为图表面板
        frame.setContentPane(chartPanel);
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        frame.setVisible(true);
    }
  • 动态规划
 /**
     * 动态规划
     *
     * @param w 物品重量
     * @param v 物品价值
     * @param n 物品个数
     * @param C 背包容量
     * @param x 解向量
     * @return 最大价值
     */
    public static int KnapsackDP(int[] w, int[] v, int n, int C, int[] x) {
        int[][] V = new int[n + 1][C + 1];
        //初始化第0列
        for (int i = 0; i <= n; i++)
            V[i][0] = 0;
        //初始化第0行
        for (int j = 0; j <= C; j++)
            V[0][j] = 0;
        //计算第i行,进行第i次迭代
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= C; j++) {
                if (j < w[i - 1]) {
                    V[i][j] = V[i - 1][j];
                } else {
                    V[i][j] = Math.max(V[i - 1][j], V[i - 1][j - w[i - 1]] + v[i - 1]);
                }
            }
        }
        //求装入背包的物品
        for (int j = C, i = n; i > 0; i--) {
            if (V[i][j] > V[i - 1][j]) {
                x[i - 1] = 1;
                j = j - w[i - 1];
            } else {
                x[i - 1] = 0;
            }
        }
        //返回背包取得的最大价值
        return V[n][C];
    }

GitHub的使用

  • 仓库
  • commit
  • release
  • branch master为主分支,dev为开发分支

总结

  该系统完全符合模块化设计,该项目将不同的功能划分为不同的模块,每个模块可以单独解决一个问题,所有的模块是通过Main类来进行调用从而完整的实现项目要求的功能,而且如果以后要添加,删除或修改功能均可快速找到对应的模块来进行修改,并且不影响其他模块的执行。

PSP

PSP2.1 计划共完成需要的时间(min) 实际完成需要的时间(min)
Planning(计划) 20 20
Estimate(估计任务时间,规划大致步骤) 20 20
Development(开发) 290 1030
Analysis(需求分析) 15 10
Design Spec(生成设计文档) 15 10
Design Review(设计复审) 10 10
Coding Standard(代码规范) 5 5
Design(具体设计) 30 60
Coding(具体编码) 180 900
Code Review(代码复审) 30 30
Test(测试) 5 5
Reporting(报告) 15 154
Test Report(测试报告) 5 5
Size Measurement(计算工作量) 5 5
Postmortem & Process Improvement Plan(事后总结,提出过程改进计划) 5 5

  开发环节耗时最多,并且估计和实践相差巨大。

原因:

  • 在设计的时候没有考虑全面,导致在开发过程中有些方法之间的调用出现问题
  • 部分知识欠缺,在使用的时候不熟练

实践总结

  对于本次实践,我使用了PSP来进行整体活动的安排,因为是第一次使用,部分环节的时间估计与实际差异很大。本次实践活动相比之前的课程设计之类的开发更具有条理性,活动更加科学,因此开发起来也是比较顺利的。通过此次实践,我也发现了我在开发方面的不足之处,比如:在根据需求来设计软件整体结构的时候考虑不全面,导致开发速度大幅降低。在以后的开发时,一定要重视前期的规划工作,这将会为之后的编码等工作带来极大的好处。