结对项目-最长英语单词链


哈哈,这次记住了,来,初始化!

项目

内容

这个作业属于哪个课程

2023年北航敏捷软件工程社区

这个作业的要求在哪里

结对项目-最长英语单词链

我在这个课程的目标是

学习软件开发的原则、方法,并对敏捷软件开发的具体技术有实践能力

这个作业在哪个具体方面帮助我实现目标

在与人合作开发上有所精进,对于模块化编程以及一些测试的基本方法有了更深的了解和实践

项目信息

  • 教学班级:周四下午班

  • 项目地址:BUAA_SE_MaximumWordsChain

PSP表格

PLANNING

计划

30

· Estimate

· 估计这个任务需要多少时间

30

Development

开发

1825

· Analysis

· 需求分析 (包括学习新技术)

360

· Design Spec

· 生成设计文档

60

· Design Review

· 设计复审 (和同事审核设计文档)

15

· Coding Standard

· 代码规范 (为目前的开发制定合适的规范)

10

· Design

· 具体设计

120

· Coding

· 具体编码

900

· Code Review

· 代码复审

60

· Test

· 测试(自我测试,修改代码,提交修改)

300

Reporting

报告

190

· Test Report

· 测试报告

150

· Size Measurement

· 计算工作量

10

· Postmortem & Process Improvement Plan

· 事后总结, 并提出过程改进计划

30

合计

2045

读书与接口

Information Hiding

信息隐藏原则:代码模块应该采用定义良好的接口来封装,这些模块的内部结构应该是程序员的私有财产,外部是不可见的。

此项目被分割为了如下几个模块:读入,处理和输出,读入模块负责处理读入和分词,将结果传给处理模块,处理模块计算答案,传给输出模块,输出模块负责写入文件。三个模块各司其职,互不干涉,在定义对GUI的接口时按需调用即可,无需关注内部实现。

Interface Design

接口设计应遵循六大原则:

  • 单一职责原则:单接口的职责应尽量单一

  • 里氏替换原则:所有引用基类的地方必须能够透明地使用其子类的对象

  • 依赖倒置原则:高层模块不应该依赖低层模块,二者都应依赖其抽象

  • 接口隔离原则:接口小、高内聚

  • 迪米特法则:一个类应该对自己需要耦合或调用的类知道的最少

  • 开闭原则:一个软件实体(类、模块、函数)应该对扩展开发、对修改关闭

关于我们的实现,具体可以看这里。

Loose Coupling

维基百科如是说:

In computing and systems design, a loosely coupled system is one

  1. in which components are weakly associated (have breakable relationships) with each other, and thus changes in one component least affect existence or performance of another component.

  1. in which each of its components has, or makes use of, little or no knowledge of the definitions of other separate components. Subareas include the coupling of classes, interfaces, data, and services. Loose coupling is the opposite of tight coupling.

也就是说,模块之间互不影响,且无需知道对方的内部实现。在此项目中,读入、处理和输出模块确实是按此方式设计的,core和gui之间也同样如此。

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

计算模块接口设计

所有的计算都在同一个模块engine中实现,提供int engine(int *options, char *res[])函数用于求解。

针对与前端交互,暴露了如下几个接口:

  • 返回所有单词链:int gen_chains_all(const char* words);

  • 返回单词数量最多的单词链:int gen_chain_word(const char*);

  • 返回字母数量最多的单词链:int gen_chain_char(const char* words, char head, char tail, char prohibit, bool enable_loop);

  • 返回上一次操作所花费的时间:double get_execution_time();

  • 返回上一次操作的结果:char *getResult();

其中,words为输入的所有单词;head、tail、prohibit分别代表指定的首字母、尾字母和禁止出现的首字母,若参数值为0则表示没有限制;enable_loop代表是否允许形成单词环。各个接口通过调用engine进行计算。

实现过程

抽象

将单词看做边,首尾看做节点,相连看做边,按一定规则构建图,计算最长路,得到最长的英语单词链。

一开始,我们采用了单词为点,相连为边的策略,虽然在解决单词链长度 >= 2的问题上颇有成效,但是对于自环的处理迟迟无法走上正道。于是,我们对建图过程进行了重构:

  • 图用vector<pair<int,int>> graph[60][60]表示

  • 点:a-z的26个字母

  • 边:存在一个单词,s.t.首字母 = 起点,尾字母 = 终点

  • 边权:-n/-w边权为1,-c边权为单词长度

  • 举例:如果存在buaaSE这样的词,则graph[b][e] = {边权, 边编号}

实现

  • 两个需要解决的问题:自环、单词链中单词数量需要大于2。

  • 判环:没有-r参数时需要先判断图中无环。这里采用拓扑排序进行环的判断。但是,当存在首位字母相同的单词时,此图中出现自环。对自环数量进行讨论:

  • 如果图中各个点上的自环数量 <= 1,则自环不会影响单词环的形成,这个时候删去自环,直接运行拓扑排序即可。举例:a ab。

  • 如果图中存在至少一个点,使得这个点上的自环数量 >= 2,那么这两个自环自己就会形成单词环,直接返回结果。举例:aa aba。

  • -n:采用dfs暴力搜索出所有可能的单词数量>=2的单词链

  • -w、-c

  • 目标:采取动态规划求解,用 表示以字母i为首字母得到的最长链长度。

  • 在之前的建图过程中,为了进行拓扑排序,我们把自环拿掉了。现在为了求解还需要把它们重新连上。为了更方便处理自环,不在使用边权计算答案的情况下再引入点权,我们需要修改当前的图。

  • 由于没有环,所以每个点上的自环最多存在一条。于是考虑新加入a'-z'26个节点,即a-z对应下标0-25,a'-z'对应下标26-51,意为把a点拆成a->a',原a的入边不变,出边改为从a'指出即可。对于没有自环的节点,不变。

  • 重新进行拓扑排序,根据倒序进行答案的更新:有 ,则 ,同时记录i的“前驱”节点为j。

  • 此时我们拿到了所有的字母为首的最长链长度,要开始研究如何获得答案。

  • 这里我遇到了一个棘手的问题,即如何保证最长链的单词数量 >= 2,最后是参考了往届学长的做法,对前两个字母进行了暴力搜索,保证了单词数 >= 2的情况下再进行最大答案的统计。

  • -r

  • 鉴于上面提到的重新建图方法,本想采取改变graph的存储方式为priority_queue,按边长从大到小排序,每用掉一条边就pop一次,回溯时再加回来的暴搜方法求解,但是碍于这样又无法解决链长需要大于2的问题,弃之。

  • 虽然是n!的复杂度,但惧于近似算法的正确性无法保证(以及想不到),最后依旧采用了暴力搜索所有结果的方法。

  • -h

  • 简化计算过程,使得无论是暴力搜索还是搜索起始的字母都从给定字母出发即可。

  • -t

  • 对于暴搜,判断答案的末尾字母;

  • 对于动态规划,初始化的时候让别的点不可达即可,如除了给定的字母外全部初始化为-0x3f3f3f3f。

  • -j

  • 在建图的时候就不把以给定字母为首字母的单词加进去。

对于建图过程,可以看如下图示(对于-c,且输入单词为aa acb bcda的情况):

建图

优化

展示在所在开发环境下编译器编译通过无警告的截图

这里虽然看起来剩下五个warning,但是这五个函数没有被用到的原因是他们都是和GUI交互的接口。

UML

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

由于一开始不合适的建图导致程序卡在了自环的问题上,为了测试正确性,几乎全部使用的暴力搜索(-w和-c也是),复杂度为 。后为改进性能,用约三个小时重新建图并引入动态规划,成功把只有-w和-c的复杂度降至拓扑排序的 和dp的 ,其中n为52,e为单词数量;并且,在只有-w和-c的情况下,建图时只保留了边权最长的边(自环同理,只会有一条边),其余边直接忽略,节省dp的时间;又由最终单词链长度不可能超过52(否则一定有环),而且经过删边的图最多只剩下52条边,所以这个过程其实完全是常数时间。

-n在我们看来,只有全部暴力搜索一遍的方法,无需改进,它同样满足链长不会超过52的性质。

-r有很大改进空间,如(以下皆为思想实验)使用priory_queue而非vector去存储两点之间的边,依旧使用递归回溯的方法进行搜索,每次使用边长最长的边,删去,递归,回溯时再把这条边加回来。但之所以没有继续改进的原因是改完后发现没有办法保证找到的单词链长度大于2,于是尝试再保存一条次长路(最长和次长都存在的情况下二者单词数量不可能同时为1,否则二者可以一起构成更长的环,必有一条满足条件),但在具体实现未能想到如何处理,所以最复杂的-r依旧采用了暴力搜索的策略。

在这样一组随机数据下的-w input.txt -r的性能体现:

lo bmktw lnupc cfalbj onl dm rvr tuuf srai pk usdft bjcpw be zxi r cgw hejy ls oxed nf lotsp p pjbrrx yevzx rgxr asz km fenaw r li k xqs nbpvy hzaii x sbt x leqv dr vlylm il qmwaw xdbbwx cif dixuk gieu mf etqhqb teh bi jpooxn hgqrm obuk oa gdvbh sio tde arrj kamxr rbxh

火焰图

调用树

Method List

可以发现,复杂度集中在dfsMaxRing也就是暴力搜索所有含环结果的函数中,伴随着大量遍历数组,和vector相关等的操作。

阅读

Design by Contract & Code Contract

It prescribes that software designers should define[formal, precise and verifiable interface specifications for software components, which extend the ordinary definition of abstract data types with preconditions, postconditions and invariants.

契约式设计(让我回忆起了面向对象第三单元的uml):

  • 优点:逻辑严密,提升了程序的鲁棒性;双方地位平等,感觉在契约下能减少bug的出现

  • 缺点:需要提前设计;增加了对不同模块开发人员沟通的必要性;需要在程序中时刻保持严谨的逻辑,不能说是缺点,只是说对于程序员的要求更高;给“工作”附加了“义务”

计算模块部分单元测试展示

结果展示

测试框架:google test,在windows下的clion中使用。

这里不能看第一行!因为引入了googletest,导致对项目进行覆盖率统计时这个文件夹也被统计了进去,导致第一行的总覆盖率看起来非常糟糕。

其中,和实现功能相关的文件为engine和paramParser,全部计算在engine.cpp中完成,这个文件的分支覆盖率也达到了89%。其余文件的单元测试也尽力做了,可以查看下面的测试思路,把所有可能的输入情况都遍历到了,几乎所有行覆盖率都到达了100%,但是分支覆盖率提不上去。如output.cpp中只有一个用于输出的循环,在单元测试数据为循环0/1/多次的情况下每行都hit了至少4次,依旧无法突破33%的分支覆盖率,疑为和该测试框架对分支覆盖率的判定有关。

测试思路

单元测试分为对paramParser、engine和output三个模块的测试。

读入与分词

封装了如下函数(只保留了主要代码):

voidparseWordUnitTest(stringinput, intargc, char*argv[], char*wordAns[], intwordAnsLen, int*optAns) {// read from file//parseparser.parseParams(argc, (constchar**) argv, options);// compare ans sizeASSERT_EQ(tmpAns.size(), wordAnsVector.size());//compare contentfor (auto&str: tmpAns) {ASSERT_EQ(wordsSet.count(str), 1);}//compare optionsfor (inti=0; i<7; i++) {ASSERT_EQ(options[i], optAns[i]);}}

测试样例:包含特殊字符,大小写字母和重复单词:

 // parseWordUnitTest -c -j -r -hTEST(Manual, T14) {stringinput="orz@OrZ#orz%zSO";intargc=8;char*argv[10] = {"Wordlist.exe", "-c", "input.txt", "-r", "-j", "a", "-h", "o"};char*wordAns[10] = {"orz", "zso"};intwordAnsLen=2;intoptAns[8] = {0, 0, 1, 'o', 0, 'a', 1, 0};parseWordUnitTest(input, argc, argv, wordAns, wordAnsLen, optAns);}

核心计算功能测试

手搓数据

这里对和接口交互封装了三个函数,下面以gen_chains_all接口为例:

 voidtest_gen_chain_all(constchar*words, char*ans[], intansLen) {char**testRes= (char**) malloc(20005);inttestLen=gen_chains_all(words, testRes);ASSERT_EQ(testLen, ansLen);for (inti=0; i<testLen; i++) {ASSERT_EQ(strcmp(ans[i], testRes[i]), 0);}}

测试数据:

 // -w -h -tTEST(Manual, T8) {char *words = "a ac*aD d D#bc cd bd\n";char *ans[10] = {"a", "ac", "cd", "d"};int ansLen = 4;test_gen_chain_word(words, 'a', 'd', 0, true, ans, ansLen);}
随机数据

我们还采用了生成随机数据,并和暴力对拍的策略进行测试。

 voidcreateData(intn, int*options) {for (inti=0; i<n; i++) {stringstr;while (true) {str.clear();intwordLen=rand() %6+1;for (intj=0; j<wordLen; j++) {ints=rand() %26, t=rand() %2, r=rand() %2;if (t) str+= (s+'a');elsestr+= (s+'A');if (r) str+="#";}if (!options[OP_R]) {if (str.back() <=str.front()) {continue;}}}randomWords.push_back(str);}}

用暴力对随机数据求解(主要代码):

 voidbruteForce(intn, int*options) {intglobalMaxAns=0;for (inti=0; i<n; i++) {if (options[OP_J] ==randomWords[i].front()) {continue;}if (!options[OP_H] || (options[OP_H] ==randomWords[i].front())) {randPaths.clear();singlePath.clear();memset(randVis, 0, sizeof(randVis));singlePath.push_back(randomWords[i]);randVis[i] =true;dfs(i, options);​intmaxAns=0, maxIdx=0;for (intj=0; j<randPaths.size(); j++) {if (randPaths[j].size() <2) {continue;}if (options[OP_N]) {stringstr;for (auto&k: randPaths[j]) {str+=k;str+=" ";}randomGlobalAns.push_back(str);} else {if (options[OP_J]) {for (auto&k: randPaths[j]) {if (options[OP_J] ==k.front()) {continue;}}}if (options[OP_T]) {stringstr=randPaths[j].back();if (options[OP_T] !=str.back()) {continue;}}if (options[OP_W]) {if (randPaths[j].size() >maxAns) {maxAns=randPaths[j].size();maxIdx=j;}} elseif (options[OP_C]) {intcharLen=0;for (auto&k: randPaths[j]) {charLen+=k.size();}if (charLen>maxAns) {maxAns=charLen;maxIdx=j;}}}}​if (options[OP_W] ||options[OP_C]) {if (maxAns>globalMaxAns) {globalMaxAns=maxAns;randomGlobalAns.clear();for (auto&j: randPaths[maxIdx]) {randomGlobalAns.push_back(j);}}}}}}
 voidrandomTestEngine(intn, int*options, stringinput) {// initializecreateData(n, options);// build graph and prepare for topsortbruteForce(n, options);}

对比engine模块的输出和暴力得到的结果(主要代码)

void randomTestCmp(int *options) {// initializeint ans = engine(options, randomResult);ASSERT_EQ(ans, randomGlobalAns.size());if (!options[OP_N]) {for (int i = 1; i < ans; i++) {string str1 = charStarToString(randomResult[i - 1]);string str2 = charStarToString(randomResult[i]);ASSERT_EQ(str1.back(), str2.front());}}if (options[OP_C]) {int len1 = 0, len2 = 0;for (int i = 0; i < ans; i++) {len1 += strlen(randomResult[i]);len2 += randomGlobalAns[i].size();}ASSERT_EQ(len1, len2);}// check for -h// check for -t// check for -j
}

测试数据(枚举所有可能的输入排列):

// -n
TEST(Random, T1) {int options[8] = {1, 0, 0, 0, 0, 0, 0, 0};for (int i = 0; i < 5; i++) {Sleep(1000);randomTestEngine(100, options, "");//randomTestPrint();randomTestCmp(options);}
}
// -w
TEST(Random, T2) {int options[8] = {0, 1, 0, 0, 0, 0, 0, 0};for (int i = 0; i < 5; i++) {Sleep(1000);randomTestEngine(100, options, "");//randomTestPrint();randomTestCmp(options);}
}
// -w -h
TEST(Random, T3) {for (int i = 0; i < 5; i++) {int h = rand() % 26;int options[8] = {0, 1, 0, h + 'a', 0, 0, 0, 0};Sleep(1000);randomTestEngine(100, options, "");//randomTestPrint();randomTestCmp(options);}
}
// -w -t
TEST(Random, T4) {for (int i = 0; i < 5; i++) {int t = rand() % 26;int options[8] = {0, 1, 0, 0, t + 'a', 0, 0, 0};Sleep(1000);randomTestEngine(100, options, "");//randomTestPrint();randomTestCmp(options);}
}
// -w -j
TEST(Random, T5) {for (int i = 0; i < 5; i++) {int j = rand() % 26;int options[8] = {0, 1, 0, 0, 0, j + 'a', 0, 0};Sleep(1000);randomTestEngine(100, options, "");//randomTestPrint();randomTestCmp(options);}
}
// -w -h -t
TEST(Random, T6) {for (int i = 0; i < 10; i++) {int h = rand() % 26;int t = rand() % 26;int options[8] = {0, 1, 0, h + 'a', t + 'a', 0, 0};Sleep(1000);randomTestEngine(100, options, "");//randomTestPrint();randomTestCmp(options);}
}
// -w -h -j
TEST(Random, T7) {for (int i = 0; i < 10; i++) {int h = rand() % 26;int j = rand() % 26;int options[8] = {0, 1, 0, h + 'a', 0, j + 'a', 0};Sleep(1000);randomTestEngine(100, options, "");//randomTestPrint();randomTestCmp(options);}
}
// -w -t -j
TEST(Random, T8) {for (int i = 0; i < 10; i++) {int t = rand() % 26;int j = rand() % 26;int options[8] = {0, 1, 0, 0, t + 'a', j + 'a', 0};Sleep(1000);randomTestEngine(100, options, "");//randomTestPrint();randomTestCmp(options);}
}
// -w -h -t -j
TEST(Random, T9) {for (int i = 0; i < 10; i++) {int h = rand() % 26;int t = rand() % 26;int j = rand() % 26;int options[8] = {0, 1, 0, h + 'a', t + 'a', j + 'a', 0};Sleep(1000);randomTestEngine(100, options, "");//randomTestPrint();randomTestCmp(options);}
}
// -w -r
TEST(Random, T10) {int options[8] = {0, 1, 0, 0, 0, 0, 1, 0};for (int i = 0; i < 5; i++) {Sleep(1000);randomTestEngine(40, options, "");//randomTestPrint();randomTestCmp(options);}
}
// -w -h -r
TEST(Random, T11) {for (int i = 0; i < 5; i++) {int h = rand() % 26;int options[8] = {0, 1, 0, h + 'a', 0, 0, 1, 0};Sleep(1000);randomTestEngine(40, options, "");//randomTestPrint();randomTestCmp(options);}
}
// -w -t -r
TEST(Random, T12) {for (int i = 0; i < 5; i++) {int t = rand() % 26;int options[8] = {0, 1, 0, 0, t + 'a', 0, 1, 0};Sleep(1000);randomTestEngine(40, options, "");//randomTestPrint();randomTestCmp(options);}
}
// -w -j -r
TEST(Random, T13) {for (int i = 0; i < 5; i++) {int j = rand() % 26;int options[8] = {0, 1, 0, 0, 0, j + 'a', 1, 0};Sleep(1000);randomTestEngine(40, options, "");//randomTestPrint();randomTestCmp(options);}
}
// -w -h -t -r
TEST(Random, T14) {for (int i = 0; i < 10; i++) {int h = rand() % 26;int t = rand() % 26;int options[8] = {0, 1, 0, h + 'a', t + 'a', 0, 1};Sleep(1000);randomTestEngine(40, options, "");//randomTestPrint();randomTestCmp(options);}
}
// -w -h -j -r
TEST(Random, T15) {for (int i = 0; i < 10; i++) {int h = rand() % 26;int j = rand() % 26;int options[8] = {0, 1, 0, h + 'a', 0, j + 'a', 1};Sleep(1000);randomTestEngine(40, options, "");//randomTestPrint();randomTestCmp(options);}
}
// -w -t -j -r
TEST(Random, T16) {for (int i = 0; i < 10; i++) {int t = rand() % 26;int j = rand() % 26;int options[8] = {0, 1, 0, 0, t + 'a', j + 'a', 1};Sleep(1000);randomTestEngine(40, options, "");//randomTestPrint();randomTestCmp(options);}
}
// -w -h -t -j -r
TEST(Random, T17) {for (int i = 0; i < 10; i++) {int h = rand() % 26;int t = rand() % 26;int j = rand() % 26;int options[8] = {0, 1, 0, h + 'a', t + 'a', j + 'a', 1};Sleep(1000);randomTestEngine(40, options, "");//randomTestPrint();randomTestCmp(options);}
}
// -c
TEST(Random, T18) {int options[8] = {0, 0, 1, 0, 0, 0, 0, 0};for (int i = 0; i < 5; i++) {Sleep(1000);randomTestEngine(100, options, "");//randomTestPrint();randomTestCmp(options);}
}
// -c -h
TEST(Random, T19) {for (int i = 0; i < 5; i++) {int h = rand() % 26;int options[8] = {0, 0, 1, h + 'a', 0, 0, 0, 0};Sleep(1000);randomTestEngine(100, options, "");//randomTestPrint();randomTestCmp(options);}
}
// -c -t
TEST(Random, T20) {for (int i = 0; i < 5; i++) {int t = rand() % 26;int options[8] = {0, 0, 1, 0, t + 'a', 0, 0, 0};Sleep(1000);randomTestEngine(100, options, "");//randomTestPrint();randomTestCmp(options);}
}
// -c -j
TEST(Random, T21) {for (int i = 0; i < 5; i++) {int j = rand() % 26;int options[8] = {0, 0, 1, 0, 0, j + 'a', 0, 0};Sleep(1000);randomTestEngine(100, options, "");//randomTestPrint();randomTestCmp(options);}
}
// -c -h -t
TEST(Random, T22) {for (int i = 0; i < 10; i++) {int h = rand() % 26;int t = rand() % 26;int options[8] = {0, 0, 1, h + 'a', t + 'a', 0, 0};Sleep(1000);randomTestEngine(100, options, "");//randomTestPrint();randomTestCmp(options);}
}
// -c -h -j
TEST(Random, T23) {for (int i = 0; i < 10; i++) {int h = rand() % 26;int j = rand() % 26;int options[8] = {0, 0, 1, h + 'a', 0, j + 'a', 0};Sleep(1000);randomTestEngine(100, options, "");//randomTestPrint();randomTestCmp(options);}
}
// -c -t -j
TEST(Random, T24) {for (int i = 0; i < 10; i++) {int t = rand() % 26;int j = rand() % 26;int options[8] = {0, 0, 1, 0, t + 'a', j + 'a', 0};Sleep(1000);randomTestEngine(100, options, "");//randomTestPrint();randomTestCmp(options);}
}
// -c -h -t -j
TEST(Random, T25) {for (int i = 0; i < 10; i++) {int h = rand() % 26;int t = rand() % 26;int j = rand() % 26;int options[8] = {0, 0, 1, h + 'a', t + 'a', j + 'a', 0};Sleep(1000);randomTestEngine(100, options, "");//randomTestPrint();randomTestCmp(options);}
}
// -c -r
TEST(Random, T26) {int options[8] = {0, 0, 1, 0, 0, 0, 1, 0};for (int i = 0; i < 5; i++) {Sleep(1000);randomTestEngine(40, options, "");//randomTestPrint();randomTestCmp(options);}
}
// -c -h -r
TEST(Random, T27) {for (int i = 0; i < 5; i++) {int h = rand() % 26;int options[8] = {0, 0, 1, h + 'a', 0, 0, 1, 0};Sleep(1000);randomTestEngine(40, options, "");//randomTestPrint();randomTestCmp(options);}
}
// -c -t -r
TEST(Random, T28) {for (int i = 0; i < 5; i++) {int t = rand() % 26;int options[8] = {0, 0, 1, 0, t + 'a', 0, 1, 0};Sleep(1000);randomTestEngine(40, options, "");//randomTestPrint();randomTestCmp(options);}
}
// -c -j -r
TEST(Random, T29) {for (int i = 0; i < 5; i++) {int j = rand() % 26;int options[8] = {0, 0, 1, 0, 0, j + 'a', 1, 0};Sleep(1000);randomTestEngine(40, options, "");//randomTestPrint();randomTestCmp(options);}
}
// -c -h -t -r
TEST(Random, T30) {for (int i = 0; i < 10; i++) {int h = rand() % 26;int t = rand() % 26;int options[8] = {0, 0, 1, h + 'a', t + 'a', 0, 1};Sleep(1000);randomTestEngine(40, options, "");//randomTestPrint();randomTestCmp(options);}
}
// -c -h -j -r
TEST(Random, T31) {for (int i = 0; i < 10; i++) {int h = rand() % 26;int j = rand() % 26;int options[8] = {0, 0, 1, h + 'a', 0, j + 'a', 1};Sleep(1000);randomTestEngine(40, options, "");//randomTestPrint();randomTestCmp(options);}
}
// -c -t -j -r
TEST(Random, T32) {for (int i = 0; i < 10; i++) {int t = rand() % 26;int j = rand() % 26;int options[8] = {0, 0, 1, 0, t + 'a', j + 'a', 1};Sleep(1000);randomTestEngine(40, options, "");//randomTestPrint();randomTestCmp(options);}
}
// -c -h -t -j -r
TEST(Random, T33) {for (int i = 0; i < 10; i++) {int h = rand() % 26;int t = rand() % 26;int j = rand() % 26;int options[8] = {0, 0, 1, h + 'a', t + 'a', j + 'a', 1};Sleep(1000);randomTestEngine(40, options, "");//randomTestPrint();randomTestCmp(options);}
}

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

我们一共设计了13种异常,可以分为以下三类:

文件路径类

  • FILE_INVALID: 1

  • 输入单词文本文件不合法,即没有以 .txt 扩展名结尾

  • ERROR: The file extension is illegal, please enter a file name ending with .txt .

TEST(FILE_BUG, FILE_INVALID) {try {paramParser parser = paramParser();int argc = 3;char *argv[10] = {"Wordlist.exe", "-w", "input.txtt"};int options[8];parser.parseParams(argc, (const char **) argv, options);ASSERT_EQ(0, 1);} catch (bugReport &e) {ASSERT_EQ(e.getErrorFlag(), -1 * FILE_INVALID);}
}
  • FILE_NONEXIST: 2

  • 输入单词文本文件不存在,即路径错误问题

  • ERROR: The file does not exist, please check if the file path is correct.

TEST(FILE_BUG, FILE_NONEXIST) {try {paramParser parser = paramParser();int argc = 3;char *argv[10] = {"Wordlist.exe", "-n", "noneExist.txt"};int options[8];parser.parseParams(argc, (const char **) argv, options);ASSERT_EQ(0, 1);} catch (bugReport &e) {ASSERT_EQ(e.getErrorFlag(), -1 * FILE_NONEXIST);}
}
  • FILE_EMPTY: 3

  • 输入的单词文本文件为空

  • ERROR: The input file is empty or doesn't contain valid words.

TEST(FILE_BUG, FILE_EMPTY) {try {paramParser parser = paramParser();int argc = 3;char *argv[10] = {"Wordlist.exe", "-n", "empty.txt"};int options[8];parser.parseParams(argc, (const char **) argv, options);ASSERT_EQ(0, 1);} catch (bugReport &e) {ASSERT_EQ(e.getErrorFlag(), -1 * FILE_EMPTY);}
}
  • FILE_MISSING: 4

  • 检测到 -n, -w, -c 但其后缺少输入文件的绝对路径

  • ERROR: Missing input file path.

TEST(FILE_BUG, FILE_MISSING) {try {paramParser parser = paramParser();int argc = 2;char *argv[10] = {"Wordlist.exe", "-n"};int options[8];parser.parseParams(argc, (const char **) argv, options);ASSERT_EQ(0, 1);} catch (bugReport &e) {ASSERT_EQ(e.getErrorFlag(), -1 * FILE_MISSING);}
}
  • FILE_FAIL_OUTPUT: 5

  • 无法写输出文件到 solution.txt

  • ERROR: Fail to output the solution.

  • 这里需要先把solution.txt设为只读

TEST(FILE_BUG, FILE_FAIL_OUTPUT) {try {int options[8] = {1, 0, 0, 0, 0, 0, 0, 0};char *result[10] = {"output"};output(options, result, 1);ASSERT_EQ(0, 1);} catch (bugReport &e) {ASSERT_EQ(e.getErrorFlag(), -1 * FILE_FAIL_OUTPUT);}
}

选项异常类

  • PARAM_LACK

缺少必要的参数,包括以下类型:

  • PARAM_LACK_LETTER: 6

  • -h, -t, -j 后缺少字母

  • ERROR: Lack of a specified letter after -h, -t or -j.

TEST(PARAM_BUG, PARAM_LACK_LETTER) {    try {paramParser parser = paramParser();int options[8];int argc = 4;char *argv[10] = {"Wordlist.exe", "-w", "input.txt", "-h"};parser.parseParams(argc, (const char **) argv, options);ASSERT_EQ(0, 1);} catch (bugReport &e) {ASSERT_EQ(e.getErrorFlag(), -1 * PARAM_LACK_LETTER);}
}
  • PARAM_LACK_OPT: 7

  • 缺少 -n, -w, 或者 -c

  • ERROR: Lack of option, please choose one option from -n, -w and -c.

TEST(PARAM_BUG, PARAM_LACK_OPT) {try {paramParser parser = paramParser();int options[8];int argc = 3;char *argv[10] = {"Wordlist.exe", "-h", "a"};parser.parseParams(argc, (const char **) argv, options);ASSERT_EQ(0, 1);} catch (bugReport &e) {ASSERT_EQ(e.getErrorFlag(), -1 * PARAM_LACK_OPT);}
}
  • PARAM_CONFLICT

输入的参数选项不兼容

  • PARAM_CONFLICT_N: 8

  • 输入的 -n 没有独立使用

  • ERROR: -n can not be used in combination with other options.

TEST(PARAM_BUG, PARAM_CONFLICT_N) {try {paramParser parser = paramParser();int options[8];int argc = 5;char *argv[10] = {"Wordlist.exe", "-n", "input.txt", "-h", "a"};parser.parseParams(argc, (const char **) argv, options);ASSERT_EQ(0, 1);} catch (bugReport &e) {ASSERT_EQ(e.getErrorFlag(), -1 * PARAM_CONFLICT_N);}
}
  • PARAM_CONFLICT_CW: 9

  • -w 和 -c 不能同时使用

  • ERROR: -w and -c can not be used in combination. Please choose one of them.

TEST(PARAM_BUG, PARAM_CONFLICT_CW) {try {paramParser parser = paramParser();int options[8];int argc = 5;char *argv[10] = {"Wordlist.exe", "-c", "input.txt", "-w", "input.txt"};parser.parseParams(argc, (const char **) argv, options);ASSERT_EQ(0, 1);} catch (bugReport &e) {ASSERT_EQ(e.getErrorFlag(), -1 * PARAM_CONFLICT_CW);}
}
  • PARAM_DUPLICATE: 10

  • 同样的操作参数重复出现,比如 -n -n

  • 多个文件参数,比如 [path1] [path2]

  • ERROR: Duplicate parameters were found

TEST(PARAM_BUG, PARAM_DUPLICATE) {try {paramParser parser = paramParser();int options[8];int argc = 5;char *argv[10] = {"Wordlist.exe", "-c", "input.txt", "-c", "input.txt"};parser.parseParams(argc, (const char **) argv, options);ASSERT_EQ(0, 1);} catch (bugReport &e) {ASSERT_EQ(e.getErrorFlag(), -1 * PARAM_DUPLICATE);}
}
  • PARAM_INVALID: 11

  • 操作选项格式错误,不符合 -n, -w, -c, -r, -h, -t, -j 中的任意一种,比如 xyz 或者 -a

  • -h, -t, -j 后接的单字符不是字母(a-zA-Z)

  • ERROR: Non-existent parameter option. Please check the parameter format.

TEST(PARAM_BUG, PARAM_INVALID) {try {paramParser parser = paramParser();int options[8];int argc = 3;char *argv[10] = {"Wordlist.exe", "-x", "input.txt"};parser.parseParams(argc, (const char **) argv, options);ASSERT_EQ(0, 1);} catch (bugReport &e) {ASSERT_EQ(e.getErrorFlag(), -1 * PARAM_INVALID);}
}

运行时错误类

  • BUG_RING_EXIST: 12

  • 操作选项中不含 -r 但构成了单词环

  • ERROR: The input data contains word rings.

TEST(RING_BUG, BUG_RING_EXIST) {try {int options[8] = {1, 0, 0, 0, 0, 0, 0, 0};int ans = engine(options, result);ASSERT_EQ(0, 1);} catch (bugReport &e) {ASSERT_EQ(e.getErrorFlag(), -1 * BUG_RING_EXIST);}
}
  • BUG_CHAIN_TOO_LONG: 13

  • 计算出超过 20000 条单词链

  • ERROR: There are more than 20000 word chains.

  • 这里使用的样例是:

aa ab ac ad ae af ag ah ai aj ak al am an ao ap aq ar as at au av aw ax ay azbb bc bd be bf bg bh bi bj bk bl bm bn bo bp bq br bs bt bu bv bw bx by bzcc cd ce cf cg ch ci cj ck cl cm cn co cp cq cr cs ct cu cv cw cx cy czdd de df dg dh di dj dk dl dm dn do dp dq dr ds dt du dv dw dx dy dzee ef eg eh ei ej ek el em en eo ep eq er es et eu ev ew ex ey ezff fg fh fi fj fk fl fm fn fo fp fq fr fs ft fu fv fw fx fy fzgg gh gi gj gk gl gm gn go gp gq gr gs gt gu gv gw gx gy gzhh hi hj hk hl hm hn ho hp hq hr hs ht hu hv hw hx hy hzii ii ik il im in io ip iq ir is it iu iv iw ix iy izjj jk jl jm jn jo jp jq jr js jt ju jv jw jx jy jz
TEST(CHAIN_BUG, BUG_CHAIN_TOO_LONG) {try {paramParser parser = paramParser();int options[8];int argc = 3;char *argv[10] = {"Wordlist.exe", "-n", "input.txt"};parser.parseParams(argc, (const char **) argv, options);int ans = engine(options, result);output(options, result, ans);ASSERT_EQ(0, 1);} catch (bugReport &e) {ASSERT_EQ(e.getErrorFlag(), -1 * BUG_CHAIN_TOO_LONG);}
}

界面模块的详细设计过程

前言

那花狗是艺术家,不知死活的那一种,忽然发现造化少了一只鞋,就抵死要去把那缺憾补回来,一次一次把自己累得半死也不知停;黑狗却是哲学家,它在想,鞋子捡回来,又怎么样呢?又能怎么样呢?造化又不知安着什么心眼?拖鞋事件大约跟希腊神话里西西弗斯的那块石头,或中国神话里吴刚的那株桂树类同吧?这一场不知何时罢手的永恒重复,做了亦无所得,不做,亦无所失。每次它跑到岸边,脚趾触到温暖的海水,它就穷知究虑起来。它每想一次,疑团就更大,决定就更困难。看来生命是一场善意的圈套,在一带美丽的海滩上进行,你不知该怎么办。上当呢,是傻;不上当呢,是无趣。

出于美观、技术学习成本等角度的考虑,我们选择了使用 electron + vue.js 构造结对任务的 GUI ,并且在结对任务一开始时就提出了可视化单词链的美好构想 —— 只是事实证明我们成了上当的花狗,跨生态开发带来的麻烦事是我们在初期口嗨时完全没有预料到的,为了使 dll 文件成功与 GUI 对接,我们在无尽的版本适配、插件重装等“踩坑”过程上花费了三倍于具体编码的时间。但好在,历尽许多困难并得到了学长 p 的倾心帮助后,我们没有放弃,终于让项目刚开始时那句 “单词链这么明显的链条结构,做个可视化图应该还挺容易的” 的口嗨成为了现实。

当然,具体编码的部分确实没有什么难度,97% 的气力都花在了解决环境上。

技术框架

  • 打包构建类

  • vue-cli-plugin-electron-builder 2.1.1

  • electron 13.0.0

  • 注意:electron 版本高于 22 时将无法兼容 ffi-napi 插件,低于 9.0.0 时也会产生版本不兼容问题,目前发现 9.0.0、12.x、13.0.0 都是可以顺利构建项目的

  • 功能实现类

  • eCharts 5.4.1: 实现单词链可视化

  • vuetify 2.6.0: 前端页面组件

  • 注意:构建初期,我们使用了 ant-design 作为前端组件,却遇到了 webpack 失效的问题(即 electron 无法正确构建应用程序)。当然,当时还有许多其他问题没有解决,因此不敢肯定是组件库涉及到的样式语言的原因,仅在此予以记录

  • sass 1.32.0: 前端页面组件样式语言

  • 使用该样式语言书写了一些自定义文件,某种程度上改进了 vuetify 不够精美的原生外观

  • file-saver 2.0.5: 实现文件保存

  • ffi-napi 4.0.3: 实现 dll 与 vue-electron 的数据交互

  • 其他环境

  • node:14.21.3

  • 注意:node 版本高于 16 、低于 14 都不可行

  • Windows-build-tools 4.0.0

  • 注意:使用 yarn 或者 npm 获取安装会因为包损坏问题失败(当然,更大的概率是先败在网络问题 / npm 无法识别 python 版本等问题上),此时需要去网络论坛中下载损坏包的光驱文件,并在该插件的全局文件夹下手动安装。如果试图自己安装 python2.7 和 Visual Studio Build Tools 都是会失败的。

界面设计

我们将 GUI 设计为如下所示的双栏排版:

(好看,燕大神我吹爆!!!)

  • 左侧用于指定参数与编辑输入单词文本,其中,输入文本支持手动输入与从文件导入两种模式。

  • 我们在前端添加了对文本内容(不允许为空)和参数组合(不允许冲突参数存在)的验证,便利了异常处理环节。下面展示 Script 部分的验证,其余参数冲突问题可以通过表单的特性解决(例如,设置 -n , -w, -c 处为单选框)

genWordsChain() {let that = this;console.log({rawWords: that.rawWords,calType: that.calType,hM: that.headLetterMust,tM: that.tailLetterMust,hN: that.headLetterNot,aR: that.allowRing})let validateFlag = 1;if (that.rawWords === '') {that.emptyWordsAlert = true;validateFlag = 0;}if (that.calType === "1") {if (that.allowRing !== false ||that.headLetterMust !== 'none' ||that.headLetterNot !== 'none' ||that.tailLetterMust !== 'none') {that.conflictParamsAlert = true;validateFlag = 0;}}if (that.headLetterMust === that.headLetterNot &&that.headLetterMust !== 'none') {that.conflictParamsAlert = true;validateFlag = 0;}if (validateFlag === 1) {// ...}
  • 右侧用于输出单词链计算结果,包括单词链数量、求解时间、可视化的单词链结果与导出结果文件选项。如果没有求得符合条件的单词链或者源文本在不允许环存在的场景下存在环,则会触发前端报错。

  • 关于可视化单词链的设计,我们使用了 eCharts 插件完成节点关系图的生成功能,并且采用了以下设计方案(该配色方案同样是 GUI 的整体配色方案):

  • 节点配色

  • 单词链头: #626c91

  • 单词链尾: #3fb1e3

  • 单词链中段: #6be6c1

  • 节点大小: 单词长度 * 5

  • 节点位置: 在画布内随机分布

  • Script 部分实现较长,可参见仓库源码,大体思路形如:

let idx = 0;
for (let i = 0; i < subGraph.length; i++) {let tmpLink = subGraph[i];let tmpLen = tmpLink.length;if (tmpLink[tmpLen - 1] === ' ') {tmpLink = tmpLink.slice(0, tmpLen - 1);}tmpLink = tmpLink.split(' ');for (let j = 0; j < tmpLink.length; j++, idx++) {let curWord = tmpLink[j];data.push({name: curWord,value: curWord.length,x: Math.random() * 30 + Math.random() * 5,y: Math.random() * 30 + Math.random() * 5,symbolSize: curWord.length * 5,id: idx,itemStyle: {color: (j === tmpLink.length - 1) ?'#3fb1e3' : (j === 0) ?'#626c91' :'#6be6c1',}})if (j !== 0) {edges.push({source: idx - 1,target: idx,})}}
}

需要注意的是,由于画图插件本身的性能限制问题,我们在进行压力测试时发现可视化单词链会在 -n 选项 + 15000 条单词链的情景下变得十分卡顿,但 solution.txt 文件可以正确输出 —— 这是一个我们未能优化的问题。

(这个出格的点对于我来说很难受,但是是燕大神无意间拖出来的,那一定有她的深意)

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

模块对接

此处我们使用了 ffi-napi 插件,以实现 node.js 生态与 dll 的链接。我们至今未知其根源的一个问题是:使用 CLion 打包的 dll 无法被 ffi-napi 正确识别(会报 Error 126 的错误),而使用 VisualStudio 打包则可以解决。同时,想要采用这个方案的同学还应该注意,dll 接口部分必须使用纯 C 实现,否则会导致模块无法被识别。(祭那个困扰了我们很久的 #include <string> )

网络上该插件的使用都有比较详细的教程,只要使用如下的方式引入 dll ,就可以在方法中对其进行调用了:

const ffi = require('ffi-napi')
const myDll = ffi.Library('./core.dll', {gen_chains_all: ['int',['string']],gen_chain_word: ['int',['string', 'char', 'char', 'char', 'bool']],gen_chain_char: ['int',['string', 'char', 'char', 'char', 'bool']],get_execution_time: ['double',[]],getResult: ['string',[]]
})
// ...
let curNum = myDll.gen_chain_word(that.rawWords + '\x1a',that.headLetterMust === 'none' ? 0 : that.headLetterMust.charCodeAt(0),that.tailLetterMust === 'none' ? 0 : that.tailLetterMust.charCodeAt(0),that.headLetterNot === 'none' ? 0 : that.headLetterNot.charCodeAt(0),that.allowRing);
// ...

需要注意的是,需要在输入的文本后手动加上一个文件结束符 \x1a 才能正确计算结果。(由于 JavaScript 本身的特性,加上一个换行符 \n 也能解决这个问题)

以及我们至今没有解决的一个问题是函数参数的更新问题,比如,如果像作业要求中的接口示例那样将 result[] 写在函数参数中,该字符串值则不会得到正确更新 —— 尝试了几种改进方案之后,要么获得的是无法转换的地址值,要么是由于没有正确操作内存导致的乱码。因此我们单独开发了一个获取结果字符串的接口 getResult() 。这不是一个很好的方案,因此我们也希望得到问题的正确解答。

实现效果展示

https://live.csdn.net/v/283294

这里是个演示视频,在可怜的yyy研究出来如何直接插入视频之前,它可能会维持超链接的模样。

结对的过程

我们选择在主北 4 楼尽头的公共学习区完成结对编程。在此感谢楼上新开的 wings 咖啡,还有学习区桌上被 gxy 把叶子翻过来卷过去无数次还默默吸收着电脑辐射的绿萝和君子兰。

结对照片如下:

优缺点:

  • 结对编程:

  • 优点:

  • 沟通方便,能用语言、肢体、乱写乱画等多种方式说明想法

  • 有效防止摸鱼,获得更充足的动力

  • 可能更快速的定位问题并找到解决方法

  • 学到别人的代码风格、开发经验

  • 缺点:

  • 需要在工作时间和地点上保持一致

  • 充分展现自己能力的不足

  • GXY

  • 优点:

  • 对前端框架部分的编码十分熟练,在GUI开发部分贡献巨大

  • 效率极高,开发速度快

  • 学习能力强,对于新技术/新编辑器环境等都能很快通过文档上手并投入使用

  • 解决问题能力强,能快速定位问题所在并找到解决方法

  • 理解能力强,沟通效率高

  • 具有令人放心甚至惊羡的审美

  • 缺点:

  • 算法竞赛方面的基础比较薄弱,因此在计算模块书写和性能提升部分起到的作用不多(简直就没什么作用,蚌)

  • 作息和心态都比较阴间,一定程度上带坏了队友

  • YYY

  • 优点

  • 对计算模块部分的算法比较熟悉,因此在 engine 开发部分贡献非常非常大

  • 对 C++ 工程的构建比较熟悉,比如 CMakeList.txt 的书写

  • 信息搜集与整合能力较高

  • 心态良好健康,能够为阴暗的队友提供积极的情绪支持,提高开发的可持续发展程度

  • 对自己要求高,时常在队友想要摆烂的时候提出优化算法 / 项目框架 / 接口结构的需求,并且经常能给出具有建设性和创意的建议

  • 缺点:

  • 效率低下,经常用大量时间解决愚蠢问题

  • 最喜欢的解决问题方式:遇到困难睡大觉

  • 环境配置杀手,指总是能掉进连环坑中,包括但不限于自己看串行或者各种版本问题

  • 到处不初始化,量产让队友误以为是多线程的bug

实际PSP表格

PLANNING

计划

30

22

· Estimate

· 估计这个任务需要多少时间

30

22

Development

开发

1645

1885

· Analysis

· 需求分析 (包括学习新技术)

180

810

· Design Spec

· 生成设计文档

60

60

· Design Review

· 设计复审 (和同事审核设计文档)

15

20

· Coding Standard

· 代码规范 (为目前的开发制定合适的规范)

10

10

· Design

· 具体设计

120

90

· Coding

· 具体编码

900

415

· Code Review

· 代码复审

60

30

· Test

· 测试(自我测试,修改代码,提交修改)

300

450

Reporting

报告

190

150

· Test Report

· 测试报告

150

120

· Size Measurement

· 计算工作量

10

10

· Postmortem & Process Improvement Plan

· 事后总结, 并提出过程改进计划

30

20

合计

1865

2057

软工第三次作业-结对编程相关推荐

  1. 软工第三次作业——结对编程之最长单词链

    项目 内容 这个作业属于哪个课程 2023北航敏捷软工 这个作业的要求在哪里 结对项目-最长单词链 我在这个课程的目标是 学习软件工程理论,在实践中体会并运用软件工程理论,收获团队开发和软件工程实践经 ...

  2. 罗杰软工第三次作业——结对编程

    BUAA-2023软件工程结对编程博客作业 项目 这个作业属于哪个课程 2023北航敏捷软件工程 这个作业的要求在哪里 结对项目-最长英语单词链 我在这个课程的目标是 学习并实践软件工程开发的方法论. ...

  3. [2022软工第三次作业]结对编程项目——最长英语单词链

    项目 内容 本作业所属课程 2022年北航敏捷软件工程教学实践 本作业要求 结对编程项目-最长英语单词链 个人课程目标 学习到软件工程的方法论,了解整个过程,并进行亲自实践 本作业在哪个具体方面帮助我 ...

  4. ASE第三次作业——结对编程

    ASE第三次作业--结对编程 成员:张贺 杨涛 石恩升 github地址:https://github.com/ThomasMrY/ASE-project-MSRA 题目简介: 此次编程的题目是--统 ...

  5. 第三次作业-结对编程

    Github项目地址:https://github.com/WHYNOTEN/WordCount.git 合作同学作业地址:https://www.cnblogs.com/Mchandu/p/1065 ...

  6. 北航2022软件工程第三次作业——结对编程(最长英语单词链)

    软件工程第三次结对编程作业 项目 内容 这个作业属于哪个课程 北京航空航天大学2022春季软件工程(罗杰 任健) 这个作业的要求在哪里 结对编程项目-最长英语单词链 我在这个课程的目标是 学习软件工程 ...

  7. 2023软工第三次作业-最长英语单词链

    结对项目-最长英语单词链 项目 内容 这个作业属于哪个课程 2023北航软件工程 这个作业的要求在哪里 结对项目-最长英语单词链 我在这个课程的目标是 帮助我初步建立软件工程敏捷开发的整体流程和概念, ...

  8. 软件工程第三次作业 结对编程

    结对编程 小组成员: 201521123055 林一心 博客地址 201521123046 张杭镖 博客地址 项目地址 一.分析改进现有代码 1.单元测试: 2.覆盖率: 3.确定当前设计的一个弱点, ...

  9. 第三次作业——结对编程

    一.地址 GITH地址:https://github.com/haveadate/WordCount.git 结对伙伴的作业地址:https://www.cnblogs.com/haveadate/p ...

最新文章

  1. 在linux系统下实现音视频即时通讯的部分代码
  2. 网络应用 axIos +vue的应用
  3. PHP Session有效期的相关问题
  4. python输入一个正整数n求下列算式的值_C语言编写程序:输入一个正整数x和一个正整数n,求下列算式的值。,C语言 编写一个程序,输入一个正整数,求出它是几位数。...
  5. 不同php怎么传递参数,php – 将所有参数传递给另一个函数
  6. Maven工程的多模块
  7. php 去掉script,php怎么去除js
  8. Leetcode 323.无向连通图中的连通分量个数
  9. 【python】lxml-The E-factory
  10. jQuery密码强度校验
  11. axure后台示例_【Axure电商案例】如何设计和真的后台一样给客户看
  12. 控件无法安装的问题-Unable to execute file
  13. 第12届蓝桥杯 第七题:《砝码称重》的两种解法dfs和dp算法
  14. oracle 2703,Oracle11gR2光钎链路切换crs服务发生crash
  15. Vue 中使用Pug
  16. LSDSLAM算法解析
  17. ERROR: failed to establish dependency between database sgerp5 and diskgroup resource ora.DATA.dg
  18. AutoJs学习-截屏器
  19. 【WebService】Caused by: org.apache.cxf.transport.http.HTTPException: HTTP respon ...
  20. 海尔智家罗马尼亚冰箱工厂将投产;长沙世茂希尔顿酒店今夏揭幕​;现代与LG新能源打造环保汽车电池 | 美通企业周刊...

热门文章

  1. html css如何渐变阴影,CSS 实现文字阴影 + 文字渐变色
  2. nkoj P8845 过滤空格
  3. 黑龙江“数字工商”系统启动实现辖区监管移动执法
  4. python编程之 np.random.randint()的用法,像烙铁一样印在你的脑海里面
  5. RadioButtonList的一些问题
  6. 生物信息学|DeepPurpose:药物靶标相互作用预测的深度学习库
  7. 三维gazebo模型库3dgems介绍
  8. 李善友:你的认知有多深,你的边界就有多宽
  9. 李善友,你太有才了!
  10. html picker插件,jQuery元素选择器插件-multiPicker