这个作业属于哪个课程 | 2301-计算机学院-软件工程 |
这个作业要求在哪里 | 软件工程实践第一次作业 |
这个作业的目标 | 实现一个简易的计算器 |
目录
界面展示
GitCode 项目地址
PSP表格
PSP | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 20 | 20 |
• Estimate | • 估计这个任务需要多少时间 | 20 | 20 |
Development | 开发 | 1240 | 1350 |
• Analysis | • 需求分析 (包括学习新技术) | 40 | 30 |
• Design Spec | • 生成设计文档 | 200 | 240 |
• Design Review | • 设计复审 | 20 | 20 |
• Coding Standard | • 代码规范 (为目前的开发制定合适的规范) | 20 | 20 |
• Design | • 具体设计 | 30 | 30 |
• Coding | • 具体编码 | 720 | 800 |
• Code Review | • 代码复审 | 30 | 30 |
• Test | • 测试(自我测试,修改代码,提交修改) | 180 | 180 |
Reporting | 报告 | 50 | 40 |
• Test Repor | • 测试报告 | 20 | 15 |
• Size Measurement | • 计算工作量 | 10 | 5 |
• Postmortem & Process Improvement Plan | • 事后总结, 并提出过程改进计划 | 20 | 20 |
合计 | 1310 | 1410 |
解题思路描述
本人对后端开发较为熟悉,这次作业希望挑战前端语言,故选择了HTML、CSS、Javascript前端三件套,因有一些基础,故跳过了语言学习这一阶段,具体思路如下:
- 先编写HTML代码搭出计算器网页界面的大体架构,再添加css的效果美化界面
- 编写js文件实现界面的交互动作
- js实现计算的具体逻辑
- 编写测试代码并进行代码优化
问题1—— 如何测试代码的覆盖率?
- 通过上网查阅资料发现,测试js代码覆盖率的工具有QUnit、Jasmine、JUnit、Jest等等,考虑到Jest较为轻便且操作容易,故选择Jest
在编写测试用例时发现,Jest不支持ES6的语法,于是引入babel转义支持,通过babel进行,编译,使其变成node的模块化代码
npm install @babel/core@7.4.5 @babel/preset-env@7.4.5 -D
在项目根目录新建.babelrc
文件,并写入
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"node": "current"
}
}
]
]
}
问题2 ——nodejs项目如何打包成exe文件?
- Electron是使用JS、HTML和CSS构建跨平台的桌面应用程序框架
导入Electron和Electron builder
npm install electron --save-dev
npm install electron-builder --save-dev
Electron不兼容ES6,在package.json中配置 "type": "commonjs", 但在main.js的入口程序中,有使用到ES6相关语法,需要通过babel转换生成兼容ES5的代码
.babelrc文件配置如下
//es6转es5使用
{
"presets": [
"es2015"
],
"plugins": []
}
package.json配置如下
"scripts": {
//将src目录下的.js文件转换为es5并将其保存在static目录下
"dev":"babel src --out-dir staic",
"start": "electron .",
"test": "jest",
"dist": "electron-builder",
"coverage": "jest --coverage"
},
运行 npm run dev 即可生成es5文件
接口设计和实现过程
- 界面展示和计算逻辑代码解耦合
为实现代码的解耦合,将代码划分为三个部分
1. HTML负责网页的架构,如计算器的结构(输入输出显示屏、按键面板等)
2. CSS负责网页的样式与美化(计算器的各部分位置、大小、形状、颜色等)
3. JS负责网页的行为(点击按键触发的事件,何时开始计算、何时显示\清空结果等)
三部分依次完成,其中,HTML文件除了开头调用css、js文件外,不嵌入任何css、js代码
<script src="calculator.js"></script>
<link rel="stylesheet" href="calculator.css">
- 计算过程的实现
由用户按下“=”键触发计算表达式事件,调用getRes()函数
已知用户输入的字符串为中缀表达式,故利用栈,将中缀表达式转为后缀表达式计算,其中sin、cos、tan的优先级等同(),可将其用<>、[]、{}代替,视同()
button.addEventListener("click", function () {
......
if (value === "=") {
res = transStr(block1.innerHTML);
try {
block2.innerHTML = getRes(res);
} catch (e) {
alert(e);
clear(res);
block1.innerHTML = "";
}
}
......
});
关键代码展示
- 事件的触发与函数的调用
// 为每个按钮添加点击事件监听器
buttons.forEach(function (button) {
button.addEventListener("click", function () {
let value = button.id;
if (value === "C") {
clear(res);
block1.innerHTML = "";
block2.innerHTML = "";
} else if (value === "d") {
block1.innerHTML = block1.innerHTML.slice(0, -1);
} else if (value === "=") {
res = transStr(block1.innerHTML);
try {
block2.innerHTML = getRes(res);
} catch (e) {
alert(e);
clear(res);
block1.innerHTML = "";
}
} else {
block1.innerHTML += value
}
});
});
- 表达式计算逻辑
function getRes(res) {
for (let i = 0; i < res.length; i++) {
switch (res[i]) {
case "{":
case "[":
case "<":
case "(":
opa_stack.push(res[i]);
break;
case "}":
let t6;
while (opa_stack.length > 0 && (t6 = opa_stack.pop()) !== "{") {
opa_stack.push(t6);
popCalculator();
}
let temp1 = data_stack.pop();
data_stack.push(Math.sin(temp1));
break;
case "]":
let t7;
while (opa_stack.length > 0 && (t7 = opa_stack.pop()) !== "[") {
opa_stack.push(t7);
popCalculator();
}
let temp2 = data_stack.pop();
data_stack.push(Math.cos(temp2));
break;
case ">":
let t8;
while (opa_stack.length > 0 && (t8 = opa_stack.pop()) !== "<") {
opa_stack.push(t8);
popCalculator();
}
let temp3 = data_stack.pop();
data_stack.push(Math.tan(temp3));
break;
case ")":
let t1;
while (opa_stack.length > 0 && (t1 = opa_stack.pop()) !== "(") {
opa_stack.push(t1);
popCalculator();
}
break;
case "+":
case "-":
if (i === 0 ||
(res[i] === "-" && isNaN(res[i - 1]) &&
res[i - 1] !== ")" && res[i - 1] !== "}" && res[i - 1] !== "]" && res[i - 1] !== ">")) {
let [num, size] = getNum(res, i);
data_stack.push(num);
i += size - 1;
} else {
while (opa_stack.length > 0) {
let t3 = opa_stack.pop();
opa_stack.push(t3);
if (t3 !== "(" && t3 !== "{" && t3 !== "[" && t3 !== "<") {
popCalculator();
} else {
break;
}
}
opa_stack.push(res[i]);
}
break;
case "*":
case "/":
case "%":
while (opa_stack.length > 0) {
let t2 = opa_stack.pop();
opa_stack.push(t2);
if (t2 === "*" || t2 === "/" || t2 === "%" || t2 === "^") {
popCalculator();
} else {
break;
}
}
opa_stack.push(res[i]);
break;
case "^":
opa_stack.push(res[i]);
break;
default:
if (!isNaN(res[i])) {
let [num, size] = getNum(res, i);
data_stack.push(num);
i += size - 1;
} else {
throw "expression error";
}
break;
}
}
while (data_stack.length >= 2 && opa_stack.length >= 1) {
popCalculator();
}
if (data_stack.length === 1 && opa_stack.length === 0) {
return data_stack.pop();
}
throw "expression error";
}
单元测试
- 采用Jest进行测试,代码中,getRes()函数用于计算中缀表达式的值,而popCalculator()函数进行一次的计算,由getRes()多次调用,二者逻辑较复杂,且调用次数多,为测试的重点对象
- 数据构建——在一个测试里测试尽可能多的分支,分别测试了在输入复杂表达式、表达式除数为0、表达式语法错误的情况下的值
import {getRes} from './calculate'
import { transStr } from './calculator'
test('test calculate', () => {
let res1 = "-1+2*3/2+sin(-1*cos(0+0)%1-tan(1.5-1.5*2))*8^(9-0)^-1+2*2^1";
res1 = transStr(res1);
expect(getRes(res1)).toBe(7.259116142081281);
});
test('test exception1', () => {
let res2 = "-1/0";
res2 = transStr(res2);
expect(() => getRes(res2)).toThrow("divide by zero");
});
test('test exception2', () => {
let res3 = "-1*()))";
res3 = transStr(res3);
expect(() => getRes(res3)).toThrow("expression error");
});
- 测试结果 (html报告和命令行)
性能改进
- 在单元测试的过程中,发现覆盖率一直不变,有几行代码从未被执行,经检查发现是冗余代码,遂删去
- 使用JS的Math库计算表达式,提高运算的效率
let temp2 = data_stack.pop();
data_stack.push(Math.cos(temp2));
- JS前端语言的计算精度不够(如图),写函数提高计算精度
通过以下一系列函数,提高js的计算精度
function accDiv(arg1, arg2) {
var t1 = 0, t2 = 0, r1, r2;
t1 = arg1.toString().split(".")[1].length;
t2 = arg2.toString().split(".")[1].length;
with (Math) {
r1 = Number(arg1.toString().replace(".", ""));
r2 = Number(arg2.toString().replace(".", ""));
return (r1 / r2) * Math.pow(10, t2 - t1);
}
}
function accAdd(arg1, arg2) {
var r1, r2, m, c;
try {
r1 = arg1.toString().split(".")[1].length;
}catch (e) {
r1 = 0;
}
try {
r2 = arg2.toString().split(".")[1].length;
}catch (e) {
r2 = 0;
}
c = Math.abs(r1 - r2);
m = Math.pow(10, Math.max(r1, r2));
if (c > 0) {
var cm = Math.pow(10, c);
if (r1 > r2) {
arg1 = Number(arg1.toString().replace(".", ""));
arg2 = Number(arg2.toString().replace(".", "")) * cm;
} else {
arg1 = Number(arg1.toString().replace(".", "")) * cm;
arg2 = Number(arg2.toString().replace(".", ""));
}
} else {
arg1 = Number(arg1.toString().replace(".", ""));
arg2 = Number(arg2.toString().replace(".", ""));
}
return (arg1 + arg2) / m;
}
function accMul(arg1, arg2) {
var m = 0, s1 = arg1.toString(), s2 = arg2.toString();
m += s1.split(".")[1].length;
m += s2.split(".")[1].length;
return Number(s1.replace(".", "")) * Number(s2.replace(".", "")) / Math.pow(10, m);
}
异常处理
分为除数为0和表达式不合规范两种
通过throw new Error的方式抛出
throw new Error("expression error");
throw new Error("divide by zero");
在$Function(){}中统一catch,通过弹出提示框通知用户,删除计算器缓存的所有数据
try {
block2.innerHTML = getRes(res);
} catch (e) {
alert(e);
clear(res);
block1.innerHTML = "";
}
心得体会
在软件工程的课程中,我开发了一个简易的基于前端三件套的计算器页面,实现了基本运算和科学计算。在完成项目的过程中,我对HTML、CSS、JavaScript的掌握更进了一步,也更加了解了软件开发的基本流程。
在开发过程中,我了解到制定计划的重要性和独立思考的重要性。首先,在项目的初始阶段,有一个确切的方向是关键,要通过搜索引擎或ai助手明确这个项目需要用的的技术,再查找关于该技术的资料,明确接下来的每一个阶段具体要做哪些工作,在项目刚开始的阶段,我没有了解关于前端开发的相关流程,误入歧途,在无关紧要的内容上耗费了很多时间。
而进入开发过程后,难免会遇到各式各样的bug,我曾多次在前端的各类开发工具上因版本的缘故出错,在通过查找国内外博客和github上文档对版本和各类函数运用的说明后,逐一解决了问题,虽然过程极为艰辛,但也培养了我耐心阅读开发文档的能力。