软件工程基础课-结对项目-地铁


一、项目地址

结对项目的GitHub:PairProject_Subway
结对项目纪实:软件工程基础课-结对项目纪实
结对伙伴的博客:Frankin
结对伙伴的GitHub:DomiAbraham


二、PSP

PSP2.1Personal Software Process Stages预估耗时(分钟)实际耗时(分钟)
Planning计划
·Estimate·估计这个任务需要多长时间1015
Development开发
·Analysis·需求分析(包括学习新技术)60120
·Design Spec·生成设计文档300200
·Design Review·设计复审(和同事审核设计文档)6020
·Coding Standard·代码规范(为目前的开发制定合适的规范)6060
·Design·具体设计120120
·Coding·具体编码12002400
·Code Review·代码复审20030
·Test·测试(自我测试,修改代码,提交修改)300100
Reporting报告
·Test Report·测试报告6030
·Size Measurement·计算工作量3030
·Postmortem & Process Improvement Plan·事后总结,并提出过程改进计划6030
<script id="MathJax-Element-2" type="math/tex"> </script>合计24603155

三、解题思路

本次项目的难度继续上升,需要编写界面了,同时引入结对编程,开始向团队化过渡。距前个人项目的完成已有一个多月了,在这期间,学习了面向对象设计及编程的方法概念,这也是本次项目需要终点引入与运用的。

3.1 面向对象设计与分析

本问题分两个模块,一个是控制台程序,一个是界面程序,由于界面程序主要是显示路径,所以主体功能是与控制台程序相同的,在这里我们可以考虑对控制台程序进行打包封装成dll文件,也可以重写代码。

由题可知,首先可以先建一个Subway类,不同的参数功能即是不同的方法。对于输入操作,则可以建一个Input类,对输入参数的一些分析操作的方法即在这个类中。

3.2 关键算法的实现

3.2.1 Dijkstra算法

由于需要计算两点间最短路径,而Dijkstra算法是目前比较稳定和快速的求最短路径问题的算法。同时在上个学期的数据结构课上,也有过相关练习(参见16. 求两点之间的最短路径)。在本程序中使用的Dijkstra算法就是在其基础上适当修改而来的。

void Subway::Dijkstra()
{
    int book[STATION_NUM];  // book[]节点是否被访问
    int dis[STATION_NUM];  // dis[i]起始点到i的最短距离

    memset(book, 0, sizeof(book));  // 一开始每个点都没被访问
    for (int i = 0; i < STATION_NUM; i++)
    {
        dis[i] = this->station_link[this->start_station_][i].value;
        if (dis[i] < INF)  // start_station_到i有直接路径
        {
            this->station_path[i][0] = this->start_station_;  // start_station_到i最短路径经过的第一个顶点
            this->station_path[i][1] = i;  // start_station到i最短路径经过的第二个顶点
        }
    }

    /* 核心语句 */
    for (int i = 0; i < STATION_NUM - 1; i++)
    {
        int min = INF;  // 记录最小dis[i]
        int u;  // 记录小dis[i]的点
        for (int j = 0; j < STATION_NUM; j++)
        {
            if (book[j] == 0 && dis[j] < min)
            {
                min = dis[j];
                u = j;
            }
        }
        book[u] = 1;
        if (u == this->start_station_)
            continue;
        for (int v = 0; v < STATION_NUM; v++)
        {
            if (v == this->start_station_)
                continue;
            if (!book[v] && this->station_link[u][v].value < INF
                && dis[v] > dis[u] + this->station_link[u][v].value)
            {
                dis[v] = dis[u] + this->station_link[u][v].value;

                for (int i = 0; i < STATION_NUM; i++)
                {
                    this->station_path[v][i] = this->station_path[u][i];
                    if (this->station_path[v][i] == -1)
                    {
                        this->station_path[v][i] = v;
                        break;
                    }
                }

                /* 是否为换乘优化模式 */
                if (this->transfer_par)
                {
                    dis[u] = 0;
                    for (int i = 0; i < STATION_NUM; i++)
                    {
                        if (i != 0 && this->station_path[u][i - 1] > -1
                            && this->station_path[u][i + 1] > -1)
                        {
                            dis[u] += this->station_link[this->station_path[u][i]][this->station_path[u][i]].value;
                            if (this->station_link[this->station_path[u][i - 1]]
                                [this->station_path[u][i]].line_name
                                != this->station_link[this->station_path[u][i]]
                                [this->station_path[u][i + 1]].line_name)
                                dis[u] += this->transfer_par;
                        }
                    }
                    dis[v] = dis[u] + this->station_link[u][v].value;
                }
            }
        }
    }
}

在上面的代码中我们还可以看到,在换乘优化模式下,还进行了权值更新,这是因为在原题中认为换乘是可以看作经过3个站,即下车和又上车相当于又多增加了2个站。于是我们在实现这个换乘功能时就可以依据此原理更新权值就好。

3.2.2 鸡肋的全遍历

/a参数功能是从A站出发遍历所有站点后再回到A站。这构成了一个回路。在之前我们遇到过的回路问题有两个,一个是欧拉回路,一个是哈密顿回路。欧拉回路强调的是对所有的边的遍历,哈密顿回路强调的是对所有的节点一次遍历。而实际上,本题的回路问题与这两个著名的回路都不同,本题强调要遍历所有站点,对于边并不做要求,只要点遍历到了,有的边甚至是可以忽略的。不过使用欧拉回路解法应该是可以的,只是不会真正得到最完美解。在实际操作中,由于没有搞懂求欧拉回路算法的实现,所以只能挥泪再见。由于没有找到较好的解法,只能蛮干了,检索未遍历的点然后使用Dijkstra算法求到那个点的最短路,然后再把那个点做起点再求,直到所有点都遍历然后回到最开始的起点。

void Subway::Traverse()
{
    ..

    while (this->visit_num_ > 0)
    {
        for (int i = 1; i <= this->station_num_; i++)
        {
            if (this->visit_station[i] != 1)
            {
                this->start_station_ = from_station;
                this->end_station_ = i;
                this->AddPath();  // 在添加路径时注意记录访问点数
                from_station = i;
                break;
            }
        }
    }

    ..
}

上面的算法就是上面的描述的实现。当我们知道起点和要去的终点,就可以求路径了,使用AddPath()方法求最短路并记录站点的访问情况。在AddPath()方法里,由于它是使用了之前求最短路的方法,所以在使用保存最短路径的数组时会记录每次的出发点,所以在把这路径加入大遍历路径数组时,要忽略出发站点,避免产生站点重复。


四、设计实现过程

4.1 代码风格规范

原本设计好了本次项目的代码风格规范软件工程基础课-结对项目-代码风格规范 ,后来在实际操作中发现很难完全遵循,因为我的命名有时很长,所以经常每列超过规范中要求的80列。只能在大部分情况下遵循此规范。

4.2 函数关系图

  1. Project_Subway_Console解决方案的函数关系图
    这里写图片描述
  2. DLLCS解决方案的函数关系图
    这里写图片描述
  3. CS_Project_Console解决方案的函数关系图
    这里写图片描述

五、程序性能分析及改进

由于DLLCS.dll不是可执行程序,且此动态依赖库是Project_Subway_Console的封装,主体代码及功能没有变化,所以就没有对DLLCS解决方案做性能分析。

5.1 Project_Subway_Console解决方案

  1. /a参数的性能分析 以/a 郭公庄为例
    这里写图片描述
  2. /b参数的性能分析 以/b 巴沟 十里河为例
    这里写图片描述
  3. /c参数的性能分析 以/c 1号线为例
    这里写图片描述
  4. /d参数的性能分析 以/d 巴沟 十里河为例
    这里写图片描述
  5. /z参数的性能分析 以在/a 郭公庄的结果为例
    这里写图片描述

5.2 CS_Project_Console解决方案

由于在控制台操作基本与Project_Subway_Console的相同,所以仅分析在调出界面端的性能。
1. /a 模式性能分析 以郭公庄为例
这里写图片描述
2. /b 模式性能分析 以巴沟十里河为例
这里写图片描述
3. /c 模式性能分析 以1号线为例
这里写图片描述
4. /d 模式性能分析 以巴沟十里河为例
这里写图片描述
5. 站点显示性能分析 以郭公庄为例
这里写图片描述

5.3 关于改进

由于当前的程序基本符合项目性能要求,所以就没有做改进分析。有些细节的改进在具体编程中已经经过优化。


六、代码说明

控制台部分的一些重要代码在前文已有所介绍,这里就不再赘述。
下面着重介绍界面部分使用的代码,本程序的界面实现是基于WinForm和C#。

首先是关键的调用动态依赖库的代码

/// <summary>
/// 控制台使用的DLL调用函数
/// </summary>
[DllImport("DLLCS.dll", EntryPoint = "ConsoleInterface")]
public static extern void InterFace();

ConsoleInterface是依赖库本身对外开放的函数,InterFace()则是你对其重新取的名字。

其次关键的部分就是绘图,这里参考了github.com/jisuozhao/SE2 中的写法。
首先是对站点和两站点之间的线路的绘制:

/// <summary>
/// 绘制指定颜色粗细带箭头的线
/// </summary>
public static void DrawArrowLine(Graphics g, float x1, float y1, float x2, float y2, float width)
{
    Pen p = new Pen(DrawTool.line_brush_color, width);
    p.EndCap = LineCap.ArrowAnchor;  // 定义线尾的样式为箭头
    g.DrawLine(p, x1, y1, x2, y2);
}

/// <summary>
/// 绘制圆心(x,y),半径r,宽度为width的空心圆
/// </summary>
public static void DrawCircle(Graphics g, float x, float y, float r, float width)
{
    Pen p = new Pen(DrawTool.circle_brush_color, width);
    g.DrawEllipse(p, (int)(x - r), (int)(y - r), (int)(2 * r), (int)(2 * r));
}

当然从代码可以看出最核心的绘制功能是C#本身就有的,这也是选择C#编写界面的原因,确实是比较方便。

在绘制之后就需要考虑清除,这里使用的策略是把原地图再次覆盖。Bitmap是取资源里的地图,Rectangle是取覆盖大小,这里当然是把pictureBox全覆盖。

/// <summary>
/// 地图复原,使用原地图覆盖
/// </summary>
public void ResetMap()
{
    Bitmap bitmap = new Bitmap(Resources.subway_map);
    Rectangle r = new Rectangle(0, 0,
        this.pictureBox_Map.Size.Width, this.pictureBox_Map.Size.Height);
    MainForm.graphics.DrawImage(bitmap, r);  // 使用原地图覆盖
}

七、文本数据格式说明及优劣

这里主要是介绍beijing-subway.txt的格式

在有数据的每行开头都有标志符,分别为&#

&开头,表示这一行是关于站点的信息,分别为站点的编号、名称、在界面显示所需的坐标。

#开头,表示这一行是关于量相邻可达站点的消息,分别为出发站、到达站、权值、线路名。这里也易知本信息记录的图是有向图,这是考虑到像机场线这种存在单向行驶的情况。权值目前都设置为1,因为题目要求以站的数目为依据,也就忽略了站之间的距离大小,也就默认任意量相邻可达站的距离相等,在实际情况中可以根据地铁运行时间做权值。然后是线路名,因为北京地铁的特殊性,任意两相邻可达站之间往往只有一条地铁线,仅仅只有四惠四惠东是个例外。所以这里是个小瑕疵。

用文本保存地铁的信息数据具有方便简单易行的特点,在这类项目中具有较好的可操作性,是推荐使用的。用上述方法存储数据,在配合Dijkstra算法求最短路方面是比较简单的,因为能够轻松的构建邻接矩阵进行运算。但是在表示换乘信息方面具有一定的劣势,因为就如上述的特例一样,如果相邻可达站之间存在多条地铁线就会产生冲突,因为只能记录一条。像遇到上海地铁就无法使用这种表示方法,因为存在大量重复线。说明此方法不具有良好的可移植性。

此方法的另一个缺陷就是在打印一条线路上的所有站点时不易根据文本内容打印,在本程序实现里使用的是打表方法,即并不是根据文本信息得到线路的,而是程序本身存储了每条线路要经过哪些站,这又是对可移植性的重创。最好的方法应是完全根据文本信息的,程序代码只能有操作逻辑,而不应包含地铁信息。

本方法唯一的好处似乎就是比较适合Dijkstra算法吧!不过也确实是没有想到其他比较好的方法。

八、黑盒测试

本次没有采取单元测试,首先是考虑到在上一次的单元测试仅仅只是样子,只要程序逻辑正确,在大部分情况,尤其就本题而言没有做单元测试的必要。于是本次采取黑盒测试,选取部分具有代表性的测试样例进行测试。

由于subway_gui.exe集成了控制台操作功能和界面操作功能,所以黑盒测试只对subway_gui.exe进行。

8.1 控制台测试

  • /b 良乡大学城北 魏公村
    选取理由,本校经典路线。
    输出结果:
20
良乡大学城北
广阳城
篱笆房
长阳
稻田
大葆台
郭公庄 换乘9号线
丰台科技园
科怡路
丰台南路
丰台东大街
七里庄
六里桥 换乘10号线
莲花桥
公主坟 换乘1号线
军事博物馆 换乘9号线
白堆子
白石桥南
国家图书馆 换乘4号线/大兴线
魏公村

由于是测试最短路,所以只取了某个可行解。但实际情况下在公主坟是不换乘的。

  • /d 良乡大学城北 魏公村
    选取理由:在实际情况下应走房山线->9号线->4号线。
    输出结果:
20
良乡大学城北
广阳城
篱笆房
长阳
稻田
大葆台
郭公庄 换乘9号线
丰台科技园
科怡路
丰台南路
丰台东大街
七里庄
六里桥
六里桥东
北京西站
军事博物馆
白堆子
白石桥南
国家图书馆 换乘4号线/大兴线
魏公村

与实际情况一致,为正确解。

  • /d 良乡大学城北 可可西里
    选取理由:可可西里是不可能到的。
    输出结果:
Error: 站名错误!

能够正确判别站点出错。

  • /b 魏公村 魏公村
    选取理由:当起点与终点相同。
    输出结果:
0.

判断出距离为0,说明为相同站。

  • /b 2号航站楼 3号航站楼
    选取理由:机场线为单行线。
    输出结果:
3
2号航站楼
三元桥
3号航站楼

符合实际情况,得到正确解。

8.2 界面测试

  1. /b 良乡大学城北 魏公村
    这里写图片描述
    符合预期。
  2. /d 良乡大学城北 魏公村
    这里写图片描述
    符合预期。
  3. /a 郭公庄
    这里写图片描述
    缺陷,很难看出遍历过程。
  4. /b 良乡大学城北 可可西里
    这里写图片描述
    成功报错。
  5. /b 魏公村 魏公村
    直接退出,这里应设计有提示。
  6. /b 2号航站楼 3号航站楼
    这里写图片描述
    符合预期。
  7. /c 10号线
    这里写图片描述
    符合预期
  8. 站点显示 郭公庄
    这里写图片描述
    符合预期,不过画的圈有点小且颜色较淡,不太明显。

8.3 其他说明

由于控制台程序/z参数本身能够检查/a,所以就没有测试控制台的/a。对/z,测试本身的测试,在代码实现时已经测试为正确,这里就不再测试了。

结果上述测试,本程序的运算逻辑情况良好,可能存在部分瑕疵,但总体较优。


九、项目总结

这次项目耗时较长,从GitHub上的推送记录也可以看出来。相比与上一次的个人项目,这次结对项目的要求更多了,上一次的数独只要生成和解题即可,且策略基本相同,使用递归回溯即可,而这一次不同的参数功能下的算法就有一定的差异,同时在算法实现上也更有难度。这次还需要制作界面端,又是一个新要求。在制作界面方面本身经验不足,但最终还是能够完成,这点还是比较好的。

从算法角度来说,主要使用的是Dijkstra算法,没有使用什么比较新的算法,感觉在算法方面本次项目对我而言并没有太多帮助与提高。

从工程角度来说,这一次有着许多新东西。首先是对C++程序封装成DLL动态依赖库,之前一直不知道怎么使用动态依赖库,这一次则有了使用经验,非常有意义,对以后的项目开发也很有帮助。然后是界面,这次更加熟悉了使用C#制作界面程序,对以后制作界面程序积累了一定的经验。在使用GitHub方面,第一次使用releases发布版本。


致谢

  1. 本次界面端的编写是一个难点,怎么画点、画线是个问题。github.com/jisuozhao/SE2他们的结对项目给我了相关启发,他们封装的DrawTool类中的绘图方法着实巧妙,如果没见到代码,确实是想不到还有这些操作,这么方便的方法。
  2. 因为是第一次封装dll,没有什么经验,Lib和Dll的那点事这篇博文给了比较完整的方法。
  3. 还有其他好多大佬分享的经验!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值