结对编程(前后端实现)
队员:郑伟金 3117004680、陈俊锋 3117004647
友情链接: 结对编程(前后端实现) 之 前端篇
一 、Github项目地址
https://github.com/S-TRAVELER/JFWebServer
二、PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 20 | 20 |
· Estimate | · 估计这个任务需要多少时间 | 20 | 20 |
Development | 开发 | 1500 | 1485 |
· Analysis | · 需求分析 | 90 | 80 |
· Design Spec | · 生成设计文档 | 60 | 50 |
· Design Review | · 设计复审 | 30 | 45 |
· Coding Standard | · 代码规范 | 90 | 60 |
· Design | · 具体设计 | 60 | 60 |
· Coding | · 具体编码 | 950 | 1000 |
· Code Review | · 代码复审 | 100 | 120 |
· Test | · 测试(自我测试,修改代码,提交修改) | 120 | 90 |
Reporting | 报告 | 150 | 140 |
· Test Report | · 测试报告 | 60 | 50 |
· Size Measurement | · 计算工作量 | 30 | 20 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 60 | 70 |
Total | 总计 | 1670 | 1645 |
三、效能分析
1. 题目生成优化:
原来生成题目是通过生成随机数,来组成分子和分母;同时为了使分子分母的最大公约数为1,需要求最大公约数。然后再进行查重和排序,花费时间很多。后来进行优化,不再通过随机数取余的方法,而是通过最简分数的规律去生成最简的真分数,然后可以生成分数的匹配,可以有效地避免查重和排序的操作,从而减少花费的时间。
优化前:生成1w 道题目并写入文件共花费了约 1.14s;
优化后:生成1w 道题目并写入文件共花费了约 0.06s ,优化效果明显。
2. 提取题目优化:
原来是浏览器请求一次,就生成一次题目;后来改为把所有题目存在内存中,并在浏览器请求时,从内存提取数据并返回给浏览器。除此之外,由于题目有要求限制题目的最大数字不能超过某个数字(例如n),所以通过按式子的最大值进行由小到大的排序(式子在生成时已完成,不需生成后再排序),然后记录每个最大值的位置以在浏览器请求时,快速确定上界。有效的减少提取式子的时间。
四、设计过程
看到题目时,为小学生提供四则运算的训练,我们很快想到以网页的形式实现,因为从用户来说,网页可以提供更好更快捷的使用体验。其次,也是为贴合我们的学习方向。所以,我们使用前端和C++后台的方式完成项目。
功能概览:
1. 设计实现过程:
原型设计图
2. 代码说明:
前端:
下面挑出前端部分比较有意思的代码进行说明
// 上传文件校对答案的部分代码 this.$form.on('change', '#file', function(e) { $(this).clone().replaceAll(_this.file = this) _this.setForm() _this.currentProgress = 0 var clock = setInterval(function() { $.ajax({ url: '/check/rate', type: 'GET', headers: {id: _this.userId} }) .done(function(res) { if (res != '100') { let rate = parseInt(res) if (rate <= 50) { _this.$rate[0].innerText = res + '%' _this.$rightCircle.css('transform', `rotate(${-135+180*rate/50}deg)`) _this.$leftCircle.css('transform', 'rotate(135deg)') } else { _this.$rate[0].innerText = res + '%' _this.$rightCircle.css('transform', 'rotate(45deg)') _this.$leftCircle.css('transform', `rotate(${(135+180*rate/100)}deg)`) } } else if (res === '100') { $('.downLoad h2')[0].innerText = '点击下载' _this.$rate[0].innerText = res + '%' _this.$rightCircle.css('transform','rotate(45deg)') _this.$leftCircle.css('transform', 'rotate(315deg)') _this.$downLoadBtn.addClass('ready') var xhr = new XMLHttpRequest() xhr.open('GET', '/check/answer', true) xhr.responseType = 'blob' xhr.setRequestHeader('id', _this.userId) xhr.onload = function() { if (this.status === 200) { var blob = this.response _this.a = document.createElement('a') _this.a.download = 'Result.txt' _this.a.href = window.URL.createObjectURL(blob) } } xhr.send() clearInterval(clock) } }) .fail(function(err) { console.log(err) }) }, 500) }) }, setForm: function() { var _this = this var form = document.getElementById('uploadForm') console.log(form) var formData = new FormData(form) formData.append('file', $('#file')[0].files[0]) $.ajax({ url: '/check/question', type: 'POST', data: formData, headers: { id: _this.userId }, processData: false, contentType: false }) .done(function(res) { console.log(res) }) .fail(function() { console.log("error..") }) .always(function() { $('#file').replaceWith(_this.file) }) }
上传文件是发送请求中遇到的较为困难的一个:
1、input [type=file] 控件选择同一个文件之后不触发 change 事件,刷新浏览器之后仍不能触发,如果用户选择多次选择相同文件名上传的话会出现文件选择框不弹出来的 bug
原因:因为 input [type=file] 控件的 change 事件是通过 input 输入框中的文本改变来触发事件的,而不是像别的编程语言通过选择文件来触发,这就导致多次选择相同的文件,第一次之后就无法弹出文件选择框;还有浏览器会自动保存 input [type=file] 控件上的文字,页面关闭后打开仍然会恢复原来的文本,这时候选择同路径的文件也不会触发 change 事件。
解决方法:创建一个新的 input [type=file] 控件将旧的替换掉,事件绑定通过事件冒泡来获取
2、提交表单数据,上网找了一下找到了 form 表单封装成 formdata 对象的上传方法,但是会附加额外的信息增加后端处理的负担。
后端:
下面代码展示了分数以及分数操作的封装,代码中展示了使用宏去实现代码复用,实现分数大小于比较操作。
(由于代码较多,折叠了,有兴趣的可以点开看一下)
class Fraction { public: friend ostream& operator <<(ostream &os, Fraction &); friend Fraction &operator >>(const string &str, Fraction &that); Fraction(int integer = 0, int numerator = 0, int denominator = 1) : _integer(integer), _numerator(numerator), _denominator(denominator) { if (!(_denominator > 0)) { cout << _integer<< " "<<_numerator<<" "<<_denominator << endl; throw runtime_error("denominator must bigger than 0"); } } Fraction(const Fraction &that) : Fraction(that._integer, that._numerator, that._denominator) {} Fraction(const std::pair<int, int> &that,int integer=0): Fraction(integer,that.first,that.second){} static Fraction parse(const string &str) { size_t pos = str.find('\''); size_t oldPos; int tmp_integer = 0, tmp_numerator = 0, tmp_denominator = 1; if (pos != str.npos) { tmp_integer = atoi(str.substr(0, pos).c_str()); } if (pos == str.npos) { oldPos = 0; pos = 0; } else { oldPos = pos + 1; } pos = str.find('/', pos); if (pos != str.npos) { // cout << "str:" << str << endl; tmp_numerator = atoi(str.substr(oldPos, pos - oldPos).c_str()); tmp_denominator = atoi(str.substr(pos + 1).c_str()); } else { tmp_integer = atoi(str.c_str()); } return Fraction(tmp_integer, tmp_numerator, tmp_denominator); } void setInteger(int integer = 0, bool nozore=false) { _integer = integer; if(_integer==0&&nozore&&_numerator==0){ if(_denominator==1){ _integer=1; }else{ _numerator=1; } } } void set(int integer = 0, int numerator = 0, int denominator = 1) { _integer = integer; _numerator = numerator; _denominator = denominator; } int gcd(int a, int b) { int c = a; if (b < a) { a = b; b = c; } while (b) { c = a % b; a = b; b = c; } return a; } void operator =(const Fraction &that){ _integer=that._integer; _numerator=that._numerator; _denominator=that._denominator; } operator std::string() { char buffer[MAX_FRACTION_LEN]{'\0'}; if (_denominator == 1) { sprintf(buffer, "%d", _integer + _numerator); } else if (_numerator == 0) { sprintf(buffer, "%d", _integer); } else if (_integer == 0) { sprintf(buffer, "%d/%d", _numerator, _denominator); } else { sprintf(buffer, "%d'%d/%d", _integer, _numerator, _denominator); } return string(buffer); } Fraction operator+(const Fraction &that) { int tmp_denominator, tmp_numerator; if (that._numerator == 0) { tmp_denominator = this->_denominator; tmp_numerator = this->_numerator; } else { int tmp_gcd = gcd(that._denominator, this->_denominator); tmp_denominator = that._denominator * this->_denominator / tmp_gcd; tmp_numerator = that._numerator * this->_denominator / tmp_gcd + this->_numerator * that._denominator / tmp_gcd; } if (tmp_numerator > tmp_denominator) { return Fraction(that._integer + this->_integer + 1, tmp_numerator - tmp_denominator, tmp_denominator); } else { return Fraction(that._integer + this->_integer, tmp_numerator, tmp_denominator); } } Fraction operator-(const Fraction &that) { int tmp_denominator, tmp_numerator; if (that._denominator == 0) { tmp_denominator = this->_denominator; tmp_numerator = this->_numerator; } else { int tmp_gcd = gcd(that._denominator, this->_denominator); tmp_denominator = that._denominator * this->_denominator / tmp_gcd; tmp_numerator = this->_numerator * that._denominator / tmp_gcd - that._numerator * this->_denominator / tmp_gcd; } int this_integer = this->_integer; if (tmp_numerator < 0) { tmp_numerator += tmp_denominator; this_integer -= 1; } return Fraction(this_integer - that._integer, tmp_numerator, tmp_denominator); } Fraction operator/(const Fraction &that) { int this_numerator = this->_numerator + this->_integer * this->_denominator; int that_numerator = that._numerator + that._integer * that._denominator; if(that_numerator==0){ throw runtime_error("除数不能为0"); } int tmp_denominator, tmp_numerator; if (this_numerator == 0) { tmp_denominator = 1; tmp_numerator = 0; } else { tmp_denominator = that_numerator * this->_denominator; tmp_numerator = that._denominator * this_numerator; int tmp_gcd = gcd(tmp_denominator, tmp_numerator); tmp_denominator /= tmp_gcd; tmp_numerator /= tmp_gcd; } int &&tmp_integer = tmp_numerator / tmp_denominator; return Fraction(tmp_integer, tmp_numerator - tmp_integer * tmp_denominator, tmp_denominator); } Fraction operator*(const Fraction &that) { int this_numerator = this->_numerator + this->_integer * this->_denominator; int that_numerator = that._numerator + that._integer * that._denominator; int tmp_denominator, tmp_numerator; if (that_numerator == 0) { tmp_denominator = 1; tmp_numerator = 0; } else { tmp_numerator = that_numerator * this_numerator; tmp_denominator = that._denominator * this->_denominator; int tmp_gcd = gcd(tmp_denominator, tmp_numerator); tmp_denominator /= tmp_gcd; tmp_numerator /= tmp_gcd; } int &&tmp_integer = tmp_numerator / tmp_denominator; return Fraction(tmp_integer, tmp_numerator - tmp_integer * tmp_denominator, tmp_denominator); } /*-----------------------------使用宏实现代码复用--------------------------*/ #define FRACTION_BINARY_PREDICATE(cmp, auxcmp) \ bool operator cmp(const Fraction &that) \ { \ int r = _integer - that._integer; \ int d = compare(that._numerator, that._denominator); \ return ((r auxcmp 0) || ((r == 0) && (d cmp 0))); \ } FRACTION_BINARY_PREDICATE(<, <) FRACTION_BINARY_PREDICATE(<=, <) FRACTION_BINARY_PREDICATE(>=, >) FRACTION_BINARY_PREDICATE(>, >) bool operator==(const Fraction &that) { int r = this->_integer - that._integer; int d = compare(that._numerator, that._denominator); return (((r == 0) && (d == 0))); } private: inline int compare(int numerator, int denominator) { return this->_numerator * denominator - numerator * this->_denominator; } private: int _integer; //整数 int _numerator; //分母 int _denominator; //分子 }; ostream& operator <<(ostream &os, Fraction &that){ return os<<string(that); } Fraction &operator >>(const string &str, Fraction &that) { that=Fraction::parse(str); return that; }
四则运算:
Fraction Calculator::onCompute(bool &onErr){ bool exit=false; onErr=false; for(auto &it:_keys) { const string &tmp = it; char op; if (tmp[0] >= '0' && tmp[0] <= '9') { _nums.push_back(Fraction::parse(tmp)); } else if (tmp[0] != ' ') { switch (tmp[0]) { case SUB: case ADD: if (_operators.size()>0&&_operators.back()==MUL||_operators.back()==DIV||_operators.back()==SUB){ while (op != LBR && _operators.size() > 0) { op = _operators.back(); _operators.pop_back(); if (op != LBR) { calculate(op); } } if (op == LBR) { _operators.push_back(op); } } _operators.push_back(tmp[0]); break; case MUL: case LBR: _operators.push_back(tmp[0]); break; case RBR: while (op != LBR && _operators.size() > 0) { op = _operators.back(); _operators.pop_back(); if (op != LBR) { calculate(op); } } break; case EQU: while (_operators.size() > 0) { op = _operators.back(); _operators.pop_back(); if (op != LBR) { calculate(op); } } exit=true; break; default: if (tmp.compare(divide) == 0) { _flag = true; _operators.push_back(DIV); } else { onErr=true; return Fraction(); } break; } } if(exit){ break; } } if(_nums.size() != 1){ onErr=true; return Fraction(); } return _nums.back(); } bool Calculator::calculate(char op) { auto tmp2 = _nums.back(); _nums.pop_back(); auto tmp1 = _nums.back(); _nums.pop_back(); switch (op) { case ADD: _nums.push_back(tmp1 + tmp2); break; case SUB: _nums.push_back(tmp1 - tmp2); break; case MUL: _nums.push_back(tmp1 * tmp2); break; case DIV: _nums.push_back(tmp1 / tmp2); break; default: return false; } return true; }
五、测试代码
1. 正确性测试
测试四则计算的准确性
#include "Server/Calculator.h" #include <assert.h> using namespace std; using namespace Server; int main(){ { Calculator c; cout << string(c.compute("12 x 14 + 15 ÷ 3 - 1/23 = ")) << endl; cout << string(c.compute("1/2")) << endl; cout<< string(c.compute("( 1 + 2 ) x 3 = "))<<endl; cout<< string(c.compute("3 x ( 1 + 1'1/2 ) ="))<<endl; cout<< "3 x ( 1 + 1'1/2 ) = 7'1/2 "<<(c.check("3 x ( 1 + 1'1/2 ) = 7'1/2")?"true":"false")<<endl; string question("1+ 2-3+(1+2)x 5= 1"); string answer; bool result=c.check(question,answer); cout<< question<<" "<<(result?"true":"false")<<" answer: "<<answer<<endl; } return 0; }
2. 分数类操作符测试
分数的操作测试
#include <iostream> #include <assert.h> #include "Server/Producer.h" using namespace Server; using namespace std; void test_producer(){ Producer::Instance().generate(10,10); } int main() { test_producer(); { Fraction f1(2, 1, 2); Fraction f2(2, 1, 3); assert(f1 > f2); cout << string(f1) << endl; cout << string(f2) << endl; cout << string(f1 + f2) << endl; cout << string(f1 - f2) << endl; cout << string(f1 * f2) << endl; cout << string(f1 / f2) << endl; } { Fraction f1(1, 1, 2); Fraction f2(2, 1, 3); Fraction f3(2, 1, 3); assert(f3 == f2); assert(f2 - f1 == Fraction(0, 5, 6)); } { Fraction f1(0, 1, 2); Fraction f2(2); assert(f1 + f2 == Fraction(2, 1, 2)); assert(f2 - f1 == Fraction(1, 1, 2)); assert(f1 * f2 == Fraction(1)); assert(f1 / f2 == Fraction(0, 1, 4)); } { Fraction f1 = Fraction::parse("4'1/2"); Fraction f2 = Fraction::parse("4"); Fraction f3 = Fraction::parse("1/2"); assert(f1 == Fraction(4, 1, 2)); assert(f2 == Fraction(4)); assert(f3 == Fraction(0, 1, 2)); } return 0; }
3. 性能分析代码
观察花费的时间看,以方便优化。
#include <iostream> #include <vector> #include <cstdlib> #include <boost/timer/timer.hpp> #include <fstream> #include "Server/Producer.h" #include <Server/CalculatorRecord.h> using namespace Server; using namespace boost::timer; using namespace std; #define LINE_MAX_LENGTH 100 int main(){ { cpu_timer t; ofstream qafile("QuestionAndAnswer.txt", ios::out); if(qafile.is_open()){ //获取问题和答案,getAllQuestionAndAnwsers接口只提供给测试 Producer::Instance().getAllQuestionAndAnswers(qafile); qafile.close(); } cout<<t.format(); } { CalculatorRecord cr; ifstream qafile("QuestionAndAnswer.txt", ios::in); ofstream outfile("Result.txt", ios::out); cpu_timer t; char strbuf[LINE_MAX_LENGTH+1]; if(qafile.is_open()&&outfile.is_open()){ //string::eof接口会在最后一行执行2次,所以用stirng::peek while(qafile.peek()!=EOF){ //逐行读取,并放入运算器中 qafile.getline(strbuf,LINE_MAX_LENGTH); cr.push_back(strbuf); } qafile.close(); cr.compute(); cr.Foreach([&](const string &it){ outfile<<it<<endl; }); } cout<<t.format(); } return 0; }
六、测试结果
1. 页面展示
欢迎页面:
在线答题:
获取题目与答案:
上传文件核对:
2. 在线答题核对
答题正确:
答题错误或未作答:
3. 上传答题核对
4. 性能
(1)10000道题目生成用时 0.068 s
(2)10000道题目核对用时 6.809s(由于为了支持用户自定义题目的核对,故采用了正则表达式进行识别,所以识别花费时间比较多,如果想提高速度可以考虑去除正则匹配以及并行优化)
七、项目心得
这次结对编程的收获很大,对于合作开发有了更深的理解,从不同的技术层面和不同的角度去看待项目,每个人都有不同的想法和收获。与单人项目相比,结对项目有着更多的交流以及想法思绪,毕竟一个好的程序或项目不是一个人的狂欢;与多人项合作相比,结对编程又显得精小而有力,在开发中也更加团结和迅速。 当然,也很幸运这次的合作能遇到很给力的队友👍,整个开发过程中保持密切的交流和积极的热情,所以开发很高效也很愉快。
与大部分的同学不同,我们选择以前后端的形式实现四则运算项目,其一是想切合我们的学习方向;此外从用户体验来说,网页的形式可以给用户提供更好的体验。