2022-BUAA-SE-第三次作业-结对编程-最长英语单词链

2022-BUAA-SE 第三次作业-结对编程实践

项目 内容
这个作业属于哪个课程 2022春季软件工程(罗杰 任健)
这个作业属于哪个课程 结对编程项目-最长英语单词链-CSDN社区
我在这个课程的目标是 了解并提高自己对软件工程的认识和实践能力
这个作业在哪个具体方面帮助我实现目标 帮助我更好地结对,更好地学会单元测试等具体地软件开发技能

0. 前言

​ 在千呼万唤中,我们终于迎来了结对编程项目,这是被大家称为小OO(面向对象程序设计与构造)的魔鬼单元。看到老师的作业要求容易被吓到,但恐惧来自于未知,所以在开始结对编程之前,我觉得我们必须思考几个问题。

  1. 为什么要结对?一个人写不好吗?
  2. 什么时候该结对?什么时候结对效果就不好?
  3. 自己对自己开发出的软件的期待是怎样的?应该用怎样的态度去实现自己的期待?

​ 对于我们这个层次的结对编程,前两个问题的答案在《构建之法》这本书中。

​ 关于第一个问题的答案,我认为就是如下四点:

  1. 结对编程能增强解决问题的能力
  2. 结对工作能带来更多的信心
  3. 心理上,当面合作时,大家都不好意思摸鱼
  4. 避免了个人英雄主义

​ 结对编程听起来这么好,怎么实现呢,有哪些具体的形式?

  1. 领航员 + 驾驶员:一人侧重开发,另一人侧重设计角度&代码复审,二者轮流交替
  2. 工人 + 工人:两个人都关注具体的问题,提出不同的解决方案,并讨论实现

​ 应该采用哪种形式呢?我认为不必拘泥,只要是发挥出1+1>2的效果,我们就达到了结对编程的目的。

​ 那么什么时候应该结对呢?

  1. 项目进行到了**紧要时刻**。比如关键功能单元、DDL之前等需要紧绷神经同时容易犯错的时刻,对于较为简单和不是那么重要的工作,相对而言就不太需要结对编程。
  2. 二人的环境、心理和安排都支持的时候。没有必要为了结对而结对,应当挑选二人效率最高的时候进行,同时将注意力聚焦在解决具体的问题上`。
  3. 对于项目已经制定了大致的计划,有了具体可操作的目标,已经完成了调研,对任务有了初步的轮廓。如果不加独立思考和钻研的团队合作之不过是两个人一起抱团逃避独立思考的负担罢了,盲目的“开会”“结对”的效率是十分低下的。

​ 所以,具体到我们的这个结对编程项目上,我们打算怎样实现结对编程呢?

  1. 首先,在项目初期应当进行一些独立的准备和思考,我们不妨称之为”预处理“阶段。在这个阶段里,结对编程的两个人应当对于作业的要求进行独立、深刻的思考;同时按照规定的实验要求配置好自己的实验环境;查阅相关资料,考虑可能发挥作用的框架和工具;对于结对的时机和结对的分工进行调研和思考。
  2. 其次,在实践的过程中应当做好串行(结对)和并行(各自个人开发)的调度。我的想法是可以先独自开发一些部分,等到关键功能单元开发时,约定时间地点同时开发。
  3. 最重要的,就是在开发的过程中注意多交流,提升交流的次数和质量。对于项目的想法一定要第一时间记录下来,定期和同伴分享进度和想法,**保持活跃**!

​ 至此,我个人的理解列举在这里啦。

​ 等想好了这几个问题,我们就可以启程开始美妙的结对编程环节了~

1. 项目信息

  • 教学班级:周二班
  • 项目地址:github

2. 项目模块计划

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划 60 40
· Estimate · 估计这个任务需要多少时间 60 40
Development 开发 1510 1710
· Analysis · 需求分析 (包括学习新技术) 80 40
· Design Spec · 生成设计文档 120 240
· Design Review · 设计复审 (和同事审核设计文档) 40 60
· Coding Standard · 代码规范 (为目前的开发制定合适的规范) 10 10
· Design · 具体设计 100 120
· Coding · 具体编码 600 700
· Code Review · 代码复审 200 180
· Test · 测试(自我测试,修改代码,提交修改) 360 360
Reporting 报告 590 520
· Test Report · 测试报告 210 180
· Size Measurement · 计算工作量 60 40
· Postmortem & Process Improvement Plan · 事后总结, 并提出过程改进计划 320 300
合计 2160 2270

3. 接口设计

3.1 Information Hiding

信息隐藏是结构化设计与面向对象设计的基础。在结构化中函数的概念和面向对象的封装思想都来源于信息隐藏。软件业对这个原则的认同也是最近十年的事情。
David Parnas在1972年最早提出信息隐藏的观点。他在其论文中指出:代码模块应该采用定义良好的接口来封装,这些模块的内部结构应该是程序员的私有财产,外部是不可见的。
Fred Brooks在《人月神话》的20周年纪念版中承认了当时自己对Parnas的批评是错误的。他说道:“我确信信息隐藏--现在常常内建于面向对象的编程中--是唯一提高设计水平的途径”。
以下列举了一些信息隐藏原则的应用。
1 多层设计中的层与层之间加入接口层;
2 所有类与类之间都通过接口类访问;
3 类的所有数据成员都是private,所有访问都是通过访问函数实现的;

我们在结对编程中的实践:

  1. 可以从第 5 部分的UML图中看出,我们将每个处理模块内部自己调用的成员变量以及函数都进行了私有化处理,保证没有多余的信息暴露在外;
  2. 除课程组要求的接口外,将其他常用的接口也抽象在core中调用;

3.2 Interface Design

单一职责原则

应该有且仅有一个原因引起类的变更,实际使用时,类很难做到职责单一,但是接口的职责应该尽量单一。

里氏替换原则

1.子类必须完全实现父类的方法

2.子类可以有自己的个性(属性和方法)。

3.覆盖或实现父类的方法时输入参数可以被放大。

4.覆写或实现父类的方法时输出结果可以被缩小。

依赖倒置原则

1.高层模块不应该依赖低层模块,两者都应该依赖其抽象。

2.抽象不应该依赖细节。

3.细节应该依赖抽象。

接口隔离原则

1.接口要尽量小。
2.接口要高内聚。
3.定制服务。
4.接口的设计是有限度的

迪米特法则

一个类应该对自己需要耦合或调用的类知道得最少,你(被耦合或调用的类)的内部是如何复杂都和我没有关系,那是你的事情,我就调用你提供的public方法,其他一概不关心。

开闭原则

一个软件实体如类、模块和函数应该对扩展开放,对修改关闭

  1. 我们gui程序没有调用课程组给定的接口,而是自己重新设计了一个接口。符合接口隔离原则中的定制服务。
  2. 解决单词链问题时使用的多个细节不同的 DFS 算法,我们选择了抽象出一个最底层最基础最普适的DFS算法,然后在其之上封装接口,添加参数来“实现”一个个不同的具体 DFS 算法。

3.3 Loose Coupling

很多年前看过的一本书,有过这么一句话(大意):很多复杂问题的解决都是通过增加中间层来实现的。
比如:应用系统最早是两层架构,数据库的负载往往会很大,通过增加应用服务器来分摊数据库层的压力;
系统集成,点对点的对接,系统间相互的依赖性太大,增加一个中间层(ESB,企业服务总线)实现松耦合性。

开发语言上,想让对象与对象间松耦合,通过增加抽象类(Abstract Class)或者接口来做到。

  1. 我们将单词链问题分为了处理参数、处理输入文本、进行计算、输出结果、异常处理这五个模块,分别构建了五个类,来帮助我们解决这个问题,实现了不同功能之间的解耦。

4. 计算模块接口的设计与实现过程

4.1 代码设计与组织

​ 我们将解决单词链问题的代码简单分为了五部分:命令行参数处理、输入处理、单词链处理、输出处理以及异常处理。各模块之间传递参数即可,彼此之间内部不可见,这样保证了代码的解耦性,同时便于开发调试。

classDiagram class ParamHandler { } class InputsHandler { } class WordListHandler { } class OutputHandler { } class ErrorCodeHandler { } class Main { } class Core { } Main --> OutputHandler Main --> InputsHandler Main --> ParamHandler Main --> ErrorCodeHandler Core --> WordListHandler Core --> ParamHandler Core --> InputsHandler

由于每个类中的方法繁多,为了博客的可读性,就不一一列举在图中了。

4.2 算法核心

算法的核心是 WordListHandler 了。顾名思义,这个模块应当是处理单词链的。我们在该模块中,根据外界传入的信息,进行选择判断采用什么方法处理传入的数据,并应当给OutputHandler发出怎样的数据与信号。

具体的算法流程图可以表示为如下结构。

5. 计算模块各个实体之间的联系

classDiagram class Word { +char first +char last +string content +word() +Word(string content) } class WordListHandler { -ParamHandler paramHandler; -unordered_map~char, listWord~ head2words -unordered_map~char, listWord~ tail2words -vector~Word~ words -unordered_map~string, bool~ visited -vector~int~ chVis -samplePoints(char ch) vector~Word~ -dfsCheck(Word& word, vector~string~& path) void -dfsAllChain(Word& word, vector~string~& path, vector~vectorstring~& ans) void -dfsLongest(Word& word, vector~string~& path, vector~vector string~, char ch) void -dfsLongestNoSameHead(Word& word, vector~string~& path, vector~string~& ans, vector~bool~& heads) void -dfsMaxAlphaNum(Word& word, vector~string~& path, inr& pathSum, vector~string~& ans, int& sum, char ch) void -getRidOfDuplication(vector~Word~& _words) void +WordListHandler(ParamHandler& _paramHandler, vector~Word~ _words) +handle() vector~string~ +genChainsAll() vector~string~ +genLongestChains(char ch1, char ch2) vector~string~ +genLongestChainsNoSameHead() vector~string~ +genMaxAlphaNumChains(char ch1, char ch2) vector~string~ } class core { +gen_chains_word(char* words[], int len, char* result[], char head, char tail, bool enable_loop) int +gen_chains_all(char* words[], int len, char* result[]) int +gen_chains_word_unique(char* words[], int len, char* result[]) int +gen_chain_char(char* words[], int len, char* result[], char head, char tail, bool enable_loop) int +gen_chains_all_python(char* words, char** result, char** error_msg) int +gen_chain_word_python(char* words, char** result, char head, char tail, bool enable_loop, char** error_msg) int +gen_chain_word_unique_python(char* words, char** result, char** error_msg) int +gen_chain_char_python(char* words, char** result, char head, char tail, bool enable_loop, char** error_msg) int +checkHeadAndTail(char head, char tail) bool +checkWord(const char word[]) bool +buildWordList(char* words[], int len, std::vector<Word>& ws) int +buildWordList(std::vector<std::string>& words, std::vector<Word>& ws) int +output2result(std::vector<std::string> words, char* result[]) int +int output2result_python(std::vector<std::string> words, char* result[]) int } core ..> WordListHandler core ..> Word WordListHandler ..> Word Word --o WordListHandler

6. 计算模块接口部分的性能改进

6.1 性能改进思路

​ 性能使用纯纯的 DFS 进行运算,没有想到特别好的思路。只是对冗余的循环运算进行了处理,稍微减轻了系统的负担

6.2性能分析图

​ 可以看出,整个程序中消耗最大的函数仍然是handle中的各种具体计算方法,整体IO开销并不大。

7.Design by Contract,Code Contract

1.目的。契约式设计的主要目的是希望程序员能够在设计程序时明确地规定一个模块单元(具体到面向对象,就是一个类的实例)在调用某个操作前后应当属于何种状态。在我看来Design by contract不是一种编程范型,它是一种设计风格,一种语法规范,甚至是个商标(是的,Bertrand Meyer注册了这么个商标)。

2.思路。契约式设计强调三个概念:前置条件,后置条件和不变式。三个概念在Wiki上都有详细解释,在此我不再啰嗦。前置条件发生在每个操作(方法,或者函数)的最开始,后置条件发生在每个操作的最后,不变式实际上是前置条件和后置条件的交集。违反这些操作会导致程序抛出异常。

repost by (1 封私信 / 80 条消息) 怎样解释 Design by Contract (契约式设计)? - 知乎 (zhihu.com)

7.1 优缺点

  • 优点
    • 规格严格,棱角分明。写出的程序,要么就是对于需求而言绝对正确的,要么直接就无法运行。
    • 模型简单。一段代码执行之前满足哪些条件,执行之后满足哪些条件,中间有哪些东西是不变的,都已经在代码实现之前指出。比较适合作为产品需求提出。
  • 缺点
    • 繁琐。在写代码前需要做大量的设计工作,尤其对于大型项目,严重影响开发效率。
    • 编程门槛高。需要开发者对于架构有着清晰的了解和自己的想法。

7.2 融入结对

与严格要求的 code by contract 不同,我与结对伙伴则是通过口头的 contract 约定了我们的 coding rules.

在开始编写代码之前以及开发过程中,我们约定并遵守了以下 contract :

  1. 变量命名遵守驼峰规则
  2. 代码设计遵循面向对象思想
  3. 开发一个模块时不需要关注其他模块,只需要满足指定的输入输出要求即可
  4. git 提交前牢记先 commit 本地修改并 git pull 拉取最新仓库内容
  5. 使用 gitignore 来防止 visual studio 产生的临时文件被加入仓库
  6. ...

8.单元测试

结对编程的时间紧迫,我们并没有时间正交开发或者寻找小组对拍,因此仅通过手动构造测试样例的方法对代码进行单元测试。

以对gen_chain_word函数的测试为例:

TEST_METHOD(testGenChainWord) {
    // 将每个测试样例抽象化为一个类,批量处理
	class GenChainWordTestCase {
	public:
		char* words[100];
		int len;
		char head;
		char tail;
		bool enable_loop;
		int stdChainNum;
		GenChainWordTestCase(char* words[], int len, char head, char tail, bool enable_loop, int stdChainNum)
			: len(len), head(head), tail(tail), enable_loop(enable_loop), stdChainNum(stdChainNum) {
			for (int i = 0; i < len; i++) {
				this->words[i] = words[i];
			}
		}
	};
	std::vector<GenChainWordTestCase> testCases;
	
	char* words1[] = { "woo", "oom", "moon", "noox" };
	testCases.push_back(GenChainWordTestCase(words1, 4, 0, 0, false, 4));
	testCases.push_back(GenChainWordTestCase(words1, 4, 'w', 0, false, 4));
	testCases.push_back(GenChainWordTestCase(words1, 4, 'o', 0, false, 3));
	testCases.push_back(GenChainWordTestCase(words1, 4, 'n', 0, false, 0));
	testCases.push_back(GenChainWordTestCase(words1, 4, 0, 'x', false, 4));
	testCases.push_back(GenChainWordTestCase(words1, 4, 0, 'n', false, 3));
	testCases.push_back(GenChainWordTestCase(words1, 4, 0, 'o', false, 0));
    
	char* words2[] = { "Algebra", "Apple", "Zoo", "Elephant", "Elephant", "Under", 
                           "Fox", "Dog", "Moon", "Leaf", "Trick", "Pseudopseudohypoparathyroidism" };
	testCases.push_back(GenChainWordTestCase(words2, 12, 0, 0, false, 4));
    
	char* words3[] = { "woo" };
	testCases.push_back(GenChainWordTestCase(words3, 1, 0, 0, false, 0));
	char* words4[1] = {};
	testCases.push_back(GenChainWordTestCase(words4, 0, 0, 0, false, 0));
    
	char* words5[] = { "woo", "oom", "moow" };
	testCases.push_back(GenChainWordTestCase(words5, 3, 0, 0, false, -3));
	testCases.push_back(GenChainWordTestCase(words5, 3, 0, 0, true, 3));
	testCases.push_back(GenChainWordTestCase(words5, 3, 'w', 0, true, 3));
	testCases.push_back(GenChainWordTestCase(words5, 3, 'o', 0, true, 3));
	testCases.push_back(GenChainWordTestCase(words5, 3, 'm', 0, true, 3));
	testCases.push_back(GenChainWordTestCase(words5, 3, 0, 'o', true, 3));
	testCases.push_back(GenChainWordTestCase(words5, 3, 0, 'm', true, 3));
	testCases.push_back(GenChainWordTestCase(words5, 3, 0, 'w', true, 3));
	testCases.push_back(GenChainWordTestCase(words5, 3, 1, 'w', false, -4));
	testCases.push_back(GenChainWordTestCase(words5, 3, 0, 2, false, -4));
	char* words6[] = { "woo", "o", "oon" };
	testCases.push_back(GenChainWordTestCase(words6, 3, 0, 0, false, -1));
	char* words7[] = { "woo", "123", "oon" };
	testCases.push_back(GenChainWordTestCase(words7, 3, 0, 0, false, -1));
	
	auto result = new char* [10000];
    // 仅比较结果的大小,比较返回结果比较困难。
	for (auto& testCase : testCases) {
		int chainNum = gen_chain_word(testCase.words, testCase.len, result, 
                                      testCase.head, testCase.tail, testCase.enable_loop);
		Assert::AreEqual(chainNum, testCase.stdChainNum);
	}
}

8.1 构造思路:

先构造正常数据:输入数据是正常的,简单的规定一下-h, -t参数,看看程序是否能跑出正确结果

再构造不那么正常的数据:只有一个单词、没有单词。

再构造会抛出异常的数据:非法字符、存在单词链、参数异常。

8.2 单元覆盖率截图:

使用Visual Studio 2019企业版自带的覆盖率测试工具进行测试,覆盖率如下:

6e8b5bc471fece55058ea78575e1fd2

有一些点比较零碎,因此没有被覆盖到。比如返回结果超过20000个、文件不存在等等。整体的覆盖率达到93%以上。

9. 计算模块部分异常处理说明

9.1 异常表

异常码 异常描述
0 Everything is alright
1 Illegal word exists.
2 More than 20000 results.
3 EnableLoop is false, but loop is found.
4 Illegal head or tail letter.
others Unexpected exception occured, please contact the developer.

9.2 异常处理过程

动态库:

每个对外暴露的函数都会catch各种抛出的异常,并根据错误代码表返回不同的错误代码。

 0: Everything is alright;
-1: Illegal word exists.
-2: More than 20000 results.
-3: EnableLoop is false, but loop is found.
-4: Illegal head or tail letter.
others: Unexpected exception occured, please contact the developer.

具体的:

  • -1:words中存在非法单词

    • 测试:

    •   char* words6[] = { "woo", "o", "oon" };
        testCases.push_back(GenChainWordTestCase(words6, 3, 0, 0, false, -1));
        char* words7[] = { "woo", "123", "oon" };
        testCases.push_back(GenChainWordTestCase(words7, 3, 0, 0, false, -1));
      
  • -2:超过20000个结果

    • 这一条并没有构建测试用例
  • -3:没有允许隐藏环的情况下出现环

    •   // false代表不允许环,true是允许环
        char* words5[] = { "woo", "oom", "moow" };
        testCases.push_back(GenChainWordTestCase(words5, 3, 0, 0, false, -3));
        testCases.push_back(GenChainWordTestCase(words5, 3, 0, 0, true, 3));
      
  • -4:head或tail不是小写字母

    •   testCases.push_back(GenChainWordTestCase(words5, 3, '*', '@', false, -4));
        testCases.push_back(GenChainWordTestCase(words5, 3, 0, '$', false, -4));
      

命令行应用:

命令行应用是直接调用动态库的,并没有给命令行单独进行单元测试,无法提供测试样例。

  • 输入参数异常:
    • 没有参数
    • 出现不存在的参数
    • -h, -t后没有紧跟一个字母
    • 没有输入文件名
    • ......
  • 输入数据异常:
    • 打不开文件
    • 文件名不以“.txt”结尾
    • 在没有-r参数的情况下出现单词环
  • 动态库给出的异常

当命令行应用发现异常,会以错误信息的形式输出到窗口中,并终止程序。

10.界面模块的详细设计过程

10.1 设计思路

设计思路就是实现课程要求的功能。我采用了较为熟悉的python语言,快速学习了 pyqt5 的用法,敏捷开发出了一个简单但功能齐全的gui界面。

首先,使用QTdesigner软件实现界面元素的设计。

然后,使用 pyui 工具将 qtdesigner 的设计出的 .ui 文件转换成 python 代码。

最后,我们根据自己的需要,为 gui 上的元素添加相应的功能。

10.2 实现过程

  1. 使用 verticalLayout 将元素简洁地约束在一个竖直的框架里

  2. 将文件导入按键、输入文本框、启动选项 checkbox 、运行结果显示等界面元素位置布置合适

  3. 在 python 代码中编写函数,使用 pyqt5 的各种组件,实现启动选项选择、选择文件、导入文件内容、输入单词即时响应、点击运行调用 c++ dll 文件并显示解雇等功能。


    1. 同时,在用户界面编写了一部分异常处理,在运行时就避免了一部分异常。

    2. 使用 python ctypes 库来调用 c++ 编写的 dll 库

    3. 实现运行结果的导出

10.3 效果展示

​ 正常运行

​ 误选提醒

​ 选择文件

导入文件内容

导出结果

11.界面模块与计算模块的对接

11.1 功能对接

使用 python 直接调用 c++ 编写的 dll 库,使用二阶字符指针存储返回信息以及错误信息。

得到返回值以后将计算结果及时展示在 gui 上。

11.2 异常处理对接

由于异常处理在命令行模式、gui模式以及接口模式下都有涉及。主要可以分为 启动参数错误、文件内容错误。

他们由于自身特性,分别散落在参数处理模块、核心计算模块以及 gui 界面。

由于 gui 使用的是接口,课程规定的接口并没有相应的命令输入,所以最好是在 gui 界面设置一些 exe 形式下的参数处理,并阻止异常。

在进入核心计算功能后,无论是 gui 还是 exe 都使用的是核心计算模块中的统一接口。

12. 结对过程

  1. 初见:在一些课程和宿舍里,我认识了李浩宇同学,钦佩他设计思想的先进以及代码能力的强大

  2. 遇见:我找到李浩宇同学,询问他是否想和我一起进行结对编程

  3. 起步:两人一起面对面设计、编写代码,理念有着很大冲突

  4. 入境:直到时间不太够用,决定求真务实,求同存异,追赶一致的敌人 —— ddl

  5. 完结:以较为明确的分工,以及贯穿始终的合作,完成了我们的第一次——结对编程

    ​ 世界名画 《xjl在指导lhy编程时的讲话》

13. 结对编程的优点和缺点

13.1 结对成员的优缺点

李浩宇 徐家乐
优:面向对象思想十分先进 优:十分喜欢面向过程编程
优:十分讨厌c++语言 优:比较喜欢c++语言
优:喜欢良好的设计 缺:做事比较急功近利
缺:容易陷入个性的设计 优:python gui 开发较快
优:单元测试能力 very good 优:会不断督促项目进度
缺:菜,算法纯纯白给

13.2 结对编程的优缺点

优:

  1. 让两个人一起为一个共同的目标努力
  2. 确实能够实现优势互补,互相分工
  3. 能够培养与人合作的能力

缺:

  1. 开发效率较个人开发低
  2. 如何处置分歧与争议没有通用的解决流程
  3. 只要有一个人开摆,另外一个人会很痛苦
posted @ 2022-04-05 20:37  WuhuAirforce  阅读(149)  评论(0)    收藏  举报