(对对碰)软工结对做业

最长单词链问题

一、项目github连接

传送门c++

二、计划(梦想)中的PSP时间分配和实际(现实)的PSP时间分配

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划 60 80
· Estimate · 估计这个任务须要多少时间 14天 12天
Development 开发 9天 7天
· Analysis · 需求分析 (包括学习新技术) 0.5天 1天
· Design Spec · 生成设计文档 120 150
· Design Review · 设计复审 (和同事审核设计文档) 60 60
· Coding Standard · 代码规范 (为目前的开发制定合适的规范) 30 50
· Design · 具体设计 1天 1天
· Coding · 具体编码 5天 5天
· Code Review · 代码复审 1.5天 1天
· Test · 测试(自我测试,修改代码,提交修改) 1.5天 1天
Reporting 报告 1天 1天
· Test Report · 测试报告 2小时 4小时
· Size Measurement · 计算工做量 1小时 0.5小时
· Postmortem & Process Improvement Plan · 过后总结, 并提出过程改进计划 0.5天 0.5天
合计 10天 12天

三、看教科书和其它资料中关于Information Hiding, Interface Design, Loose Coupling的章节,说明大家在结对编程中是如何利用这些方法对接口进行设计的

  • 信息隐藏
    咱们的设计在多个层面作了很好的封装,很好的保证了各个模块间的信息隐藏性,将搜索算法逻辑封装在具体实现内部,向外给出统一的公告接口,将计算逻辑封装在calculate这一个接口函数中。git

  • 接口设计
    接口设计上首先是一个全局对外接口calculate,咱们详细分析了实际需求的本质,设计了一个通用接口来知足各类各样的需求,在数据结构封装上咱们提供了数据访问和修改专用的接口,规范数据结构的使用,防止出现隐藏bug。没有使用任何全局变量,只使用了一些全局宏来输出log进行debug。对于可能出现的各种优化算法,咱们设计了公共的接口类,规范了统一的算法接口。github

  • 松耦合
    在计算逻辑和IO之间松耦合,计算逻辑和具体的IO方式无关,设置了规范的数据类型对象和错误信息提示对象,很好的完成了松耦合。算法

四、计算模块接口的设计与实现过程

咱们在设计计算模块时使用了自定义的对外接口,接口形式以下编程

se_errcode Calculate(const string& input_text, string& output_text, LongestWordChainType& longest_type, const char& head, const char& tail, bool enable_circle, WordChainError& handle_error);
对于该接口的说明以下:数组

咱们将整个运算逻辑抽象在一个接口内,不一样的“最长”计算方式使用枚举类型LongestWordChainType进行描述,head和tail表示单词链的首尾字母,enable_circle表示是否容许输入中含有环,这样因此的需求选项均可以被集中在一个接口中,代码的复用性很是高,且很简洁。数据结构

咱们将输入输出抽象为string,具体的IO逻辑可能涉及读取文件,与gui协同,输入输出格式等问题,这一部分相对于计算逻辑十分独立,且较容易发生需求上的变化,故独立在计算模块以外,给计算模块的一概整理成string形式,并用非字母符号来分割单词,完成了计算和IO的解耦,同时string对象不须要手动维护char*数组,增长了程序的鲁棒性。架构

对于异常处理,通常c++编程中是不使用异常的,由于其会对运行效率带来巨大的影响,一旦抛出异常整个程序的运行时间将大大增长,因此在程序逻辑上,咱们使用自定义的错误码返回值来完成逻辑上的处理,同时创建专门的资源和错误信息管理对象handle_error,负责管理资源和错误信息记录app

关于计算逻辑的实现,咱们认为这里主要的两部分是数据结构和算法,接下来咱们会对这两方面分开进行说明。框架

数据结构方面:

咱们将该问题抽象为一个有向图,该图中的结点是26个字母,一个单词即可以表示为从首字母到尾字母的一条边,由问题的特性咱们知道,这张图中是有自圈(例如awa),多重边(例如awwb, awb),简单的使用邻接矩阵,邻接表是很难应对这种数据的。咱们抽象了新的“边”元素,使用WordMapElement类去描述它,对于首字母和尾字母相同的边,存储在这一个元素之中,并按照单词含有的字母数量降序进行排列存储。而首字母,尾字母根据问题去建立结点,使用特殊的“边”元素,构建出咱们所用的数据结构。

在具体实现上,咱们使用unordered_map<char, unordered_map<char, WordMapElement> >这样的c++容器结构去存储,避免了定长数组带来的维护性差,可扩展性差的问题,同时又具备直接根据key-value来访问元素的方便性。

在搜索时,咱们须要存储当前搜到的最长单词链的相关信息,咱们仿照上文中相似的模式创建相似的数据结构。

算法方面:

因为该问题自己是一个NP-Complete问题,因此会有不少的剪枝优化和启发式搜索算法,那么从设计框架角度,咱们须要一个可扩展性很高的搜索框架,而不是把算法耦合在总体逻辑中,因此,咱们设计一个通用的搜索接口SearchInterface,定义了公共的接口方法Search和LookUp,任何搜索算法只要继承接口类重写这两个方法便可,其内部的算法优化逻辑将封装在方法内部,与外部的逻辑无关,这样能够很方便的添加优化算法,同时保证架构设计的完整性。

另外一独特之处:

在做业文档给出的接口中,对于单词数最长和字母数最长,分别设计了两个接口,可是咱们认为本质上来说,这只是两种不一样的“长度”度量而已,从实现来讲只有计算长度时,每条边对应的长度不一样这一点差别,因此咱们的设计将其统一在一块儿,若是将来有新的需求,好比说“其中含字母a的个数最多”,只须要添加新的长度计算方式便可,而不用改动总体逻辑。

五、各个实体间关系的UML图

六、计算模块接口部分的性能改进

  • 计算模块的性能上,通过visual studio 2017自带的性能探查器分析,咱们获得了以下的效能分析结果:

  • 能够发现,效能瓶颈主要存在于ChainSearch方法中,new一个对象时分配内存进行初始化,占用了大量cpu资源。另外在Search和LookUp方法中大量的搜索等操做也是效能瓶颈之一。

  • 一样的,在CheckCircle方法中,变量的声明也占用了大量cpu资源。说明为了提高计算模块的性能,咱们须要在这几个方面加以改进。

  • 须要注意的是,上述效能分析使用的数据规模并不大,因此咱们猜想随着数据规模的增大,DFS方法的cpu使用率应该会显著增长,因此咱们又进行了大数据规模(约9000词)的测试,结果以下:

  • 能够发现,当数据规模增长,暴力DFS的时间成本和cpu使用率大幅增长,由此咱们能够获得结论,在数据规模小的时候,须要减小变量声明初始化以及内存分配的相关代码。而当数据规模很大时,则须要从算法出发减少复杂度,提高性能。
  • 此外, 从算法角度来讲,可使用更好的剪枝策略,好比只使用入度为0的点和环上的点进行搜索,或者采用启发式搜索策略
    七、看Design by Contract, Code Contract的内容,描述这些作法的优缺点, 说明你是如何把它们融入结对做业中的
    ----------
    契约编程 Design by Contract

这种编程方式的特色是严格规定前置条件,后置条件,不变项,好比大二oo课程中就有相似的训练(JSF),这样作的好处是严格限制了输入输出条件,函数可能产生的反作用,从而能够很好的规范程序接口,同时,基于这种规定,能够更好地进行单元测试,覆盖到每个函数和方法的具体细节,使得程序的正确性获得了更大的保证, 可是与之相对的,就须要开发者付出大量的时间和精力,作很是详尽的测试,对于工期很紧的项目可能没法实际操做。我认为在时间中,能够选择部分绝对不能出现问题的核心模块,使用这种方式进行开发,对于比较边缘的模块,并不须要作这么多,从而让开发兼顾开发效率和正确性。

八、计算模块部分单元测试展现

  • 计算模块部分的单元测试,咱们的测试思路是从基本类开始测试,而后测试基本类的方法,接着测试使用到这个类的方法,由小到大以保证单元测试的正确性。

  • 处理I/O的函数之一ExtractWord(),测试数据构造思路在于构造出由不一样的字符分割的单词,包括没有单词的状况,将分离结果与预设结果逐个对拍。
TEST_METHOD(Test_ExtractWord)
        {
            //TEST  ExtractWord
            WordChainError error1;
            string input_text1 = "_this is      a!@#$test of0extract!word...... ";
            vector<string>  input_buffer1;
            vector<string> result1 = { "this","is","a","test","of","extract","word" };
            ExtractWord(input_text1, input_buffer1,error1);
            Assert::AreEqual(result1.size(), input_buffer1.size());
            for (int i = 0; i < result1.size(); i++) {
                Assert::AreEqual(result1[i], input_buffer1[i]);
                }

            WordChainError error2;
            string input_text2 = "_this___is___another======test of extract[][][]word. ";
            vector<string>  input_buffer2;
            vector<string> result2 = { "this","is","another","test","of","extract","word" };
            ExtractWord(input_text2, input_buffer2,error2);
            Assert::AreEqual(result2.size(), input_buffer2.size());
            for (int i = 0; i < result2.size(); i++) {
                Assert::AreEqual(result2[i], input_buffer2[i]);
            }

            WordChainError error3;
            string input_text3 = "_[][][]....";
            vector<string>  input_buffer3;
            vector<string> result3 = { };
            ExtractWord(input_text3, input_buffer3,error3);
            Assert::AreEqual(result3.size(), input_buffer3.size());
            for (int i = 0; i < result3.size(); i++) {
                Assert::AreEqual(result3[i], input_buffer3[i]);
            }
        }
  • Word类的基本单元测试,主要验证其构建方法和其内部方法的正确性,构造数据包括单个字母的状况和多个字母的状况。
TEST_METHOD(Test_Class_Word)
        {
            //TEST  Class_Word
            Word test1 = Word("a");
            Assert::AreEqual(test1.GetHead(), 'a');
            Assert::AreEqual(test1.GetTail(), 'a');
            Assert::AreEqual(test1.GetWord(), string("a"));
            Assert::AreEqual(test1.GetKey(), string("aa"));

            Word test2 = Word("phycho");
            Assert::AreEqual(test2.GetHead(), 'p');
            Assert::AreEqual(test2.GetTail(), 'o');
            Assert::AreEqual(test2.GetWord(), string("phycho"));
            Assert::AreEqual(test2.GetKey(), string("po"));
        }
  • DistanceElement类内部方法的单元测试,构造数据中验证对其操做前后顺序的影响是否知足需求,以及基本方法的正确性验证。
TEST_METHOD(Test_Class_DistanceElement_Method)
        {
            //TEST  Class_DistanceElement_Method: SetDistance/GetDistance/SetWordChain/CopyWordBuffer/ToString
            LongestWordChainType type1 = letter_longest;
            DistanceElement testElement1 = DistanceElement(type1);
            Assert::AreEqual(testElement1.GetDistance(), 0);
            vector<string> input1 = { "a","test","of","it" };
            vector<string> output1;
            testElement1.SetWordChain(input1);
            testElement1.CopyWordBuffer(output1);
            for (int i = 0; i < input1.size(); i++) {
                Assert::AreEqual(output1[i], input1[i]);
            }
            testElement1.SetDistance(6);
            Assert::AreEqual(testElement1.GetDistance(), 6);
            Assert::AreEqual(testElement1.ToString(), string("a-test-of-it"));

            LongestWordChainType type2 = word_longest;
            DistanceElement testElement2 = DistanceElement(type2);
            Assert::AreEqual(testElement2.GetDistance(), 0);
            vector<string> input2 = { "another","test","of","it" };
            vector<string> output2;
            testElement2.SetWordChain(input2);
            testElement2.CopyWordBuffer(output2);
            for (int i = 0; i < input2.size(); i++) {
                Assert::AreEqual(output2[i], input2[i]);
            }
            testElement2.SetDistance(2);
            Assert::AreEqual(testElement2.GetDistance(), 2);
            Assert::AreEqual(testElement2.ToString(), string("another-test-of-it"));
        }
  • 将某个方法内部的方法所有验证事后,就能够对调用其的方法进行单元测试,下面展现的是计算模块的总体调用,返回值为计算结果,数据构造上考虑到了是否有环,是否有头尾字母的要求等,将计算方法返回结果与正确结果比对。
TEST_METHOD(Test_Calculate)
        {
            //Test Calculate: include CalculateLongestChain/ChainSearch
            WordChainError error;
            string input_text ="Algebra))Apple 123Zoo Elephant  Under  Fox_Dog-Moon Leaf`;;Trick Pseudopseudohypoparathyroidism";
            string output_text1 = "";
            LongestWordChainType type1 = word_longest;
            Calculate(input_text, output_text1, type1, NO_ASSIGN_HEAD, NO_ASSIGN_TAIL, false,error);
            string result1 = "algebra\napple\nelephant\ntrick\n";
            Assert::AreEqual(result1, output_text1);
            string output_text2 = "";
            LongestWordChainType type2 = letter_longest;
            Calculate(input_text, output_text2, type2, NO_ASSIGN_HEAD, NO_ASSIGN_TAIL, false,error);
            string result2 = "pseudopseudohypoparathyroidism\nmoon\n";
            Assert::AreEqual(result2, output_text2);
            string input_text_ring = "Algebra))Apple aaaaa 123Zoo Elephant  Under  Fox_Dog-Moon Leaf`;;Trick Pseudopseudohypoparathyroidism";
            string output_text3 = "";
            string result3 = "algebra\naaaaa\napple\nelephant\ntrick\n";
            LongestWordChainType type3 = word_longest;
            Calculate(input_text_ring, output_text3, type3, NO_ASSIGN_HEAD, NO_ASSIGN_TAIL, true, error);
            Assert::AreEqual(result3, output_text3);
        }

九、计算模块部分异常处理说明

  • 异常设计上,咱们设计了7种异常
    • 重复单词异常
    • 环异常
    • 输入文件异常
    • 输出文件异常
    • 命令行参数异常
    • 计算模式异常
    • 无结果异常
  • 其中重复单词异常会在命令行输出,可是不会影响程序的进行,计算模式异常和命令行参数异常均为在对命令行进行解析时发生的异常,咱们并无单独为其写一个方法,因此难以在单元测试中验证,仅会在命令行输出错误信息。

  • 其他四种异常均在单元测试中进行了验证。

  • 输入文件异常,对应找不到输入文件的场景等:

std::ifstream in("notexist.txt");
            std::stringstream buffer1;
            WordChainError error3;
            if (!in.is_open()) {
                char buffer1[MAX_BUFFER_SIZE];
                sprintf(buffer1, "Error Type: can't open input file\n");
                string error_content(buffer1);
                int error_code = SE_ERROR_OPENING_INPUT_FILE;
                error3.AppendInfo(error_code, error_content);
            }
            string errortext3 = error3.ToString();
            Assert::AreEqual(errortext3, string("Error Type: can't open input file\nError Content: Error Type: can't open input file\n"));
  • 输出文件异常,对应输出文件被意外关闭的场景等:
std::stringstream buffer2;
            std::ofstream out("close.txt");
            WordChainError error4;
            out.close();
            if (!out.is_open()) {
                char buffer2[MAX_BUFFER_SIZE];
                sprintf(buffer2, "Error Type: can't open output file\n");
                string error_content(buffer2);
                int error_code = SE_ERROR_OPENING_OUTPUT_FILE;
                error4.AppendInfo(error_code, error_content);
            }
            string errortext4 = error4.ToString();
            Assert::AreEqual(errortext4, string("Error Type: can't open output file\nError Content: Error Type: can't open output file\n"));
  • 环异常,对应未选择-r选项可是输入单词可成环的场景:
WordChainError error1;
            string input_text1 = "Algebra))Apple aaaaa 123Zoo Elephant  Under  Fox_Dog-Moon Leaf`;;Trick Pseudopseudohypoparathyroidism";
            string output_text1 = "";
            string errortext1;
            LongestWordChainType type1 = word_longest;
            Calculate(input_text1, output_text1, type1, NO_ASSIGN_HEAD, NO_ASSIGN_TAIL,false, error1);
            errortext1 = error1.ToString();
            Assert::AreEqual(errortext1,string("Error Type: input has circle but not enable circle\nError Content: Error Type: input has circle but not enable circle\n"));
  • 无结果异常,对应根据选择的计算模式和头尾字母要求,没有对应结果的场景:
WordChainError error2;
            string input_text2 = "Algebra Zoo";
            string output_text2 = "";
            string errortext2;
            LongestWordChainType type2 = word_longest;
            Calculate(input_text2, output_text2, type2, 'i', NO_ASSIGN_TAIL, false, error2);
            errortext2 = error2.ToString();
            Assert::AreEqual(errortext2, string("Error Type: no available word chain\nError Content: no available word chain for head(i) and tail(0)\n"));

十、界面模块的详细设计过程

  • 界面模块咱们使用了Qt的库进行了设计。编码上仍然是c++语言,ui设计上使用了Qt Creator进行设计。

  • 首先进行界面组件需求分析:
  • 两种导入文本的方式
  • 交互式按钮,分别是五个功能选项
  • 异常状况界面提示
  • 正确结果界面显示
  • 导出结果,保存到文件
  • 使用说明

  • 以上的需求能够大概代表咱们的用户界面须要至少五个参数选择的交互按钮,两个界面,其中一个负责写入文本,一个负责显示正确结果和错误信息。另外须要四个按钮,分别对应导入文本,运行程序,导出结果和显示使用说明。

  • 明确了以上需求以后,咱们在Qt Creator中设计了大概的用户界面(macOS下):

  • 其中使用radiobutton选择两种计算方式,checkbox选择是否容许单词环,下拉框选择是否有开头和结尾字母的要求,这些设计都是为了方便用户的使用。

  • 如下为部分用户界面的代码:

//按钮触发事件(引入文件以及显示帮助信息)
void MainWindow::on_pushButton_import_clicked()
{
QString fileName=QFileDialog::getOpenFileName(this,tr("Choose File"),"",tr("text(*.txt)"));
QFile file(fileName);
if(!file.open(QFile::ReadOnly|QFile::Text)){
QString errMsg="error when import file";
ui->outputArea->setText(errMsg);
return;
}
QTextStream in(&file);
ui->inputArea->clear();
ui->inputArea->setText(in.readAll());
}

void MainWindow::on_pushButton_help_clicked()
{
dialog = new Dialog(this);
dialog->setModal(false);
QString helpMsg="test help";
dialog->ui->textBrowser->setPlainText(helpMsg);
dialog->show();
}

十一、界面模块与计算模块的对接

  • 模块对接方面,主要是经过接口函数(做业要求中的Core)进行计算,其中各个参数的值是经过界面模块的控件传入的,例如radiobutton控制的值为-w选项或-c选项,checkbox传入单词环的布尔值,combobox传入是否有-h,-t选项以及对应的字符,输入框传入文本或者从文件读入的内容,界面模块以下(window下)。

  • 对接的过程主要体如今运行程序按钮上,咱们为其绑定了事件调用core的对应函数,即Calculate(content, output,type,head,tail,ring),代码以下:
void MainWindow::on_pushButton_run_clicked()
{
int para=ui->radioButton_w->isChecked()?1:2;
bool ring=ui->checkBox_loop->isChecked();
string content = ui->inputArea->toPlainText().toStdString();
char head, tail;
if (ui->comboBox_h->currentIndex() == 0) {
head = '\0';
}
else {
head = 'a' + ui->comboBox_h->currentIndex() - 1;
}
if (ui->comboBox_t->currentIndex() == 0) {
tail = '\0';
}
else {
tail = 'a' + ui->comboBox_t->currentIndex() - 1;
}
if(content.size()==0){
QString errMsg="empty input!";
ui->outputArea->setPlainText(errMsg);
}
else{
//call corresponding function
string output;
LongestWordChainType type;
se_errcode code;
QString s = "fin";
WordChainError error;
if (para == 1) {
type = word_longest;
code=Calculate(content, output,type,head,tail,ring,error);
}
else {
type = letter_longest;
code=Calculate(content, output, type, head, tail, ring,error);
}
if (code == SE_OK) {
QString result = QString::fromStdString(output);
ui->outputArea->setPlainText(result);
}
else {
string result = error.ToString();
QString error= QString::fromStdString(result);
ui->outputArea->setPlainText(error);
}
}
//cout<<"onclick_run"<<endl;
}

功能运行结果以下:

  • 导入文件:

  • 参数选择1:

  • 运行结果1:

  • 参数选择2及运行结果:

  • 导出结果:

  • 错误提示:

十二、结对的过程

1三、结对编程的优缺点

优势:

  • 能够两我的交替负责开发和设计,可以有不少讨论问题的机会
  • 开局自带code reviewer,对于代码质量有很是大的提高
  • 能够互相给对方的code写测试,这种开发和测试并行的方式效率很高
    缺点
  • 须要两我的有必定的公共技术栈,不然相差太远不少思考方式或者编码习惯上的问题会阻碍效率
  • 有可能会产生矛盾,甚至是1+1<1

1四、界面模块,测试模块和核心模块的松耦合(附加题)

  • 这个部分咱们与 申化文 16231247和肖萌威 16061030两位同窗交换了GUI模块和计算模块,可是因为是最后一天晚上才交换,以前没有约定公共的接口格式,致使在输入输出数据转化时须要额外的时间开销,咱们本来设计的接口接受的输入是整个字符串,包括了单词的分割处理。然而对方的处理方式是输入已经分割完成的单词数组,输出也是单词数组,最终咱们未能在截止时间前完成调用对方GUI的工做。可是咱们的计算模块成功在他人的GUI跑通,这说明咱们的计算模块是兼容性比较高的。此次经历告诉咱们:必定要尽早约定公共接口设计,同时更要尽早完成工做不要压线赶DDL。
相关文章
相关标签/搜索