UI-12组结对编程作业总结
源码Github地址
https://github.com/tilmto/TILMTO/tree/master/Arithmetic
作业摘要
本次结对编程作业分为以下两种类型Core(计算核心)和UI(用户界面),我们组是UI组, 负责把core组生成的四则运算表达式展现在用户界面上。
UI需求要求
1.对Core各属性参数(生成题目的数量,操作数的数量,题目及答案中的数值的范围……)进行设置;
2.调用Core模块得到题目和运算结果,显示题目,接受输入,并能判断答案是否正确;
3.增加“倒计时”功能,每个题目必须在20秒内完成,否则,得0分并进入下一题;
4.增加“错题记录”功能,对于答错的题,将其保存下来,当下次进行“复习”时,增大错题在练习题中的概率;
5.增加”历史纪录“功能,把用户做题的成绩纪录下来并可以展现历史纪录。
PSP表格
Statu |
Stages |
预估耗时/h |
实际耗时/h |
Accept |
——需求分析 |
0.5 |
0.5 |
Accept |
——技术学习 |
6 |
4 |
Accept |
——倒计时功能 |
2 |
1 |
Accept |
——错题读入功能 |
2 |
3 |
Accept |
——错题保存功能 |
2 |
1 |
Accept |
——历史记录功能 |
2 |
1.5 |
Accept |
——UI-core对接 |
3 |
6 |
Accept |
—— 测试 |
2 |
1 |
Accept |
——博客撰写 |
2 |
2.5 |
Accept |
——合计 |
21.5 |
20.5 |
分工
PB15000175 傅泳淦: 需求分析、技术学习、功能设计、全部代码编写、博客撰写;
PB15061308 张军: 技术学习、辅助提议、与core组通联、软件使用说明书撰写。
结对编程意义与感想
结对编程是软件开发人员必须掌握的开发流程,其最大的优点在于极大地促进了开发人员人间的信息交流,省去了以往沟通上耗费的时间,并且让多样化的思维降低了代码的错误率,提升了可靠性。
这次作业与上次作业相比,工作量大了许多,除了组内两人结对编程外,UI组与Core组也要彼此交流对接,共同开发一套软件。所以,这次的作业与上次的作业相比,多了许多交流合作与信息沟通的成分。这一点在大家以往的编程经历中从未出现过的,然而信息交流在真正的团队开发中是十分重要的,其重要性绝不亚于个人码代码的能力,缺少了必要的信息沟通会严重耽误整个项目的进展,相信这一点也是老师和助教们希望我们从这次经历中体会到的。
我们组的这次结对编程过程与其他组比较不同,因为我们两人的编程能力相差较大,我们的结对关系类似于软件企业中师傅带徒弟的模式,我(傅泳淦 PB15000175)承担了全部的功能设计与代码编写工作,张军同学充当徒弟的角色在一旁学习使用QT与C++的使用。在给他讲解的过程中我也顺势理清了思路,相信他也有所收获。在这个过程中我发现要想对自己的代码有个透彻的了解,就要拉个人给他讲自己的代码是怎么回事,讲清楚了也就知道代码有没有问题了。
这次作业过程中邓老师有句话让我映象很深刻:标准是要抢的。以前我自己报过经济学类课程,其中很重要的一个问题就是标准,先抢到标准制定权的人往往能抢得先机。软件开发中也是这个道理,先发出sdk接口的组,只要质量基本令人满意,便很有机会垄断整个行业。像这次最先发core的几个组,很多UI组的参数输入界面就是根据这些core定制的,可见其抢占了大片市场。
UI使用指南
- 概述
软件名称为Arithmetic,为小学生提供自定制算术训练。
主页面如下:
上方有菜单栏和工具栏以及相应快捷键以供选择,用户可以利用他们生成随机算式、回顾错题、保存错题、查看历史成绩以及退出应用。每当鼠标移动到功能位置,下方状态栏会给出相应使用提示。
主界面的四个功能与菜单栏相应功能对应,用法详见菜单栏介绍。
- 菜单栏
菜单栏function下有五个选项,分别是:
- Generate:产生自定制随机算式,快捷键CTRL+G。
- Review:回复错题,点击打开文档搜索的界面,用户可以选择自己保存的错题文件(xml格式),快捷键CTRL+R。
- Save:保存错题,生成错题文档(xml格式),快捷键CTRL+S。
- History:查看历史做题记录,会显示正确题数/总题数以及对应成绩,快捷键CTRL+H。
- Close:关闭应用,快捷键Esc。
当鼠标停留在某一个功能上时,状态栏会给出这个功能的使用方式。例如,当鼠标停在 Generate 功能时,下方状态栏会有 Generate new exercises randomly 的提示。
- 工具栏
在工具栏中,我们又将这五项功能列了出来,使得用户可以通过移动工具栏停靠位置获得更好的体验。
- 功能介绍
Exercise/Generate:点击主界面的 Exercise 或菜单栏/工具栏的 Generate 可产生随机算式,首先会弹出算式设置窗口,如图:
参数含义:
Exercise Num:生成的练习题数量,默认为10;
Max Operator Num:运算符最大的数量,默认为5;
Range of Numbers:算式中的最大数,需输入大于20的数,默认1000;
Precision of Decimals:结果保留小数的精度,默认为2;
Has Fraction:可否出现带分数,默认为否;
Has Decimal:可否出现小数,默认为否;
Has Multiply/Divide:可否出现乘除号,默认为是;
Has Power:可否出现乘方,默认为否。
设置完成后点击random generate生成随机算式。
生成题目界面如图所示:
每道题有20s的答题时间,时间一过文本框将无法输入。
回答完毕点击 Show Answers或者全部时间用完,系统进行批改并打分:
此时可以点击 Save 按钮,选择保存路径新建一个文件以保存错题。
Review:点击主界面或菜单栏/工具栏的review 功能,可读入 Save的错题进行复习,重新训练并再次打分。
History:点击主界面或菜单栏/工具栏的History功能,可查看做题记录:
Close:点击主界面的Quit或菜单栏/工具栏的close即可退出程序。
具体代码分析
首先是打开程序的主页面,直接用UI设计即可(拖动控件),然后人为添加几个槽函数:
void MainWindow::on_pushButton_clicked() { settingWindow(); } void MainWindow::on_pushButton_2_clicked() { readXML(); } void MainWindow::on_pushButton_3_clicked() { history(); } void MainWindow::on_pushButton_4_clicked() { this->close(); }
在做接下来的步骤前先把需要的资源文件添加进工程,我使用的是八大行星的贴图,来作为每个功能的图标。
接下来实现菜单栏、工具栏、状态栏,用 QMainWindow 中的函数可以创建三个栏目,同时为菜单栏、工具栏添加动作,即要求的功能,产生随机算式、复习错题、保存错题、查看历史成绩、退出应用,分别对应 generateAction 、 readXMLAction 、writeXMLAction、 historyAction 、 closeAction 几个动作对应,最后用 connect 将每个动作与其对应槽函数连接。
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { ui->setupUi(this); setWindowTitle(tr("Arithmetic")); resize(500,500); QAction *generateAction=new QAction(QIcon(":/images/neptune"),tr("Generate"),this); generateAction->setStatusTip(tr("Generate new exercises randomly")); generateAction->setShortcut(tr("ctrl+g")); QAction *readXMLAction = new QAction(QIcon(":/images/jupiter.png"), tr("Review"), this); readXMLAction->setStatusTip(tr("Review the wrong exercises")); readXMLAction->setShortcut(tr("ctrl+r")); QAction *writeXMLAction = new QAction(QIcon(":/images/venus.png"), tr("Save"), this); writeXMLAction->setStatusTip(tr("Save the wrong exercises")); writeXMLAction->setShortcut(tr("ctrl+w")); QAction *historyAction = new QAction(QIcon(":/images/mars.png"), tr("History"), this); historyAction->setStatusTip(tr("Show history")); historyAction->setShortcut(tr("ctrl+h")); QAction *closeAction=new QAction(QIcon(":/images/mercury"),tr("Close"),this); closeAction->setStatusTip(tr("Close the application")); closeAction->setShortcut(tr("esc")); QMenu *menu = menuBar()->addMenu(tr("Function")); menu->addAction(generateAction); menu->addAction(readXMLAction); menu->addAction(writeXMLAction); menu->addAction(historyAction); menu->addAction(closeAction); QToolBar *toolBar = addToolBar(tr("Function")); toolBar->addAction(generateAction); toolBar->addAction(readXMLAction); toolBar->addAction(writeXMLAction); toolBar->addAction(historyAction); toolBar->addAction(closeAction); statusBar()->showMessage(tr("Ready")); connect(generateAction,&QAction::triggered,this,&MainWindow::settingWindow); connect(readXMLAction,&QAction::triggered,this,&MainWindow::readXML); connect(writeXMLAction,&QAction::triggered,this,&MainWindow::writeXML); connect(historyAction,&QAction::triggered,this,&MainWindow::history); connect(closeAction,&QAction::triggered,this,&MainWindow::close); }
以上内容效果如下:
下面实现每个具体的功能。
settingWindow 函数实现的是为设置随机算式的格式,针对使用的core定制参数输入界面,要求输入的参数如使用手册中所示,不再重复,重点在与如何实现布局。此次我使用的方式是,将每个 QLabel 和 QLineEdit 放入一个 QHBoxLayout ,再将所有 QHBoxLayput 放入一个统一的 QVBoxLayout ,如此实现较工整的布局。
void MainWindow::settingWindow() { wSet=new QWidget; vlayout=new QVBoxLayout; labelExerciseNum=new QLabel("Exercise Num"); editExerciseNum=new QLineEdit; editExerciseNum->setPlaceholderText("10"); hlayout[0]=new QHBoxLayout; hlayout[0]->addWidget(labelExerciseNum); hlayout[0]->addWidget(editExerciseNum); vlayout->addItem(hlayout[0]); labelMaxOperator=new QLabel("Max Operator Num"); editMaxOperator=new QLineEdit; editMaxOperator->setPlaceholderText("5"); hlayout[1]=new QHBoxLayout; hlayout[1]->addWidget(labelMaxOperator); hlayout[1]->addWidget(editMaxOperator); vlayout->addItem(hlayout[1]); labelMaxRange=new QLabel("Range of Numbers"); editMaxRange=new QLineEdit; editMaxRange->setPlaceholderText("1000"); hlayout[2]=new QHBoxLayout; hlayout[2]->addWidget(labelMaxRange); hlayout[2]->addWidget(editMaxRange); vlayout->addItem(hlayout[2]); labelPrecision=new QLabel("Precision of Decimals"); editPrecision=new QLineEdit; editPrecision->setPlaceholderText("2"); hlayout[3]=new QHBoxLayout; hlayout[3]->addWidget(labelPrecision); hlayout[3]->addWidget(editPrecision); vlayout->addItem(hlayout[3]); labelFraction=new QLabel("Has Fraction"); radioFraction=new QRadioButton("Fraction"); radioFraction->setChecked(false); radioFraction->setAutoExclusive(false); hlayout[4]=new QHBoxLayout; hlayout[4]->addWidget(labelFraction); hlayout[4]->addWidget(radioFraction); vlayout->addItem(hlayout[4]); labelDecimal=new QLabel("Has Decimal"); radioDecimal=new QRadioButton("Decimal"); radioDecimal->setChecked(false); radioDecimal->setAutoExclusive(false); hlayout[5]=new QHBoxLayout; hlayout[5]->addWidget(labelDecimal); hlayout[5]->addWidget(radioDecimal); vlayout->addItem(hlayout[5]); labelMuldiv=new QLabel("Has Multiply/Divide"); radioMuldiv=new QRadioButton("* /"); radioMuldiv->setChecked(true); radioMuldiv->setAutoExclusive(false); hlayout[6]=new QHBoxLayout; hlayout[6]->addWidget(labelMuldiv); hlayout[6]->addWidget(radioMuldiv); vlayout->addItem(hlayout[6]); labelPower=new QLabel("Has Power"); radioPower=new QRadioButton("^"); radioPower->setChecked(false); radioPower->setAutoExclusive(false); hlayout[7]=new QHBoxLayout; hlayout[7]->addWidget(labelPower); hlayout[7]->addWidget(radioPower); vlayout->addItem(hlayout[7]); buttonGenerate=new QPushButton("Random Generate"); vlayout->addWidget(buttonGenerate); wSet->setLayout(vlayout); wSet->show(); connect(buttonGenerate,&QPushButton::clicked,this,&MainWindow::randomGenerate); }
效果如图:
randomGenerate 接收参数,做范围检查,若有效则调用core函数,将产生的算式与答案保存,并立刻调用 showExpression 函数展示在界面上:
void MainWindow::randomGenerate() { expression.clear(); answer.clear(); coreExp=new string(); coreAns=new string(); int exerciseNum=editExerciseNum->text().toInt(); if(exerciseNum==0) exerciseNum=10; else if(exerciseNum>20) { QMessageBox::warning(this,tr("Exercise Num"),tr("Please input exercise num <= 20")); return; } int maxOperator=editMaxOperator->text().toInt(); if(maxOperator==0) maxOperator=5; int maxRange=editMaxRange->text().toInt(); if(maxRange==0) maxRange=1000; else if(maxRange<20) { QMessageBox::warning(this,tr("Max Range"),tr("Please input max range >= 20")); return; } int precision=editPrecision->text().toInt(); if(precision==0) precision=2; int fraction=radioFraction->isChecked()?1:0; int decimal=radioDecimal->isChecked()?1:0; int muldiv=radioMuldiv->isChecked()?1:0; int power=radioPower->isChecked()?1:0; set_setting(maxOperator,maxRange,precision,fraction,decimal,muldiv,power); for(int i=0;i<exerciseNum;i++) { generate(coreExp,coreAns); expression.push_back(QString::fromStdString(*coreExp)); answer.push_back(QString::fromStdString(*coreAns)); } wSet->hide(); showExpression(); }
showExpression 函数与 settingWindow 中的布局方式类似,每一行 hlayout[i] 包括算式文本框 labelExp[i] 、用户输入答案文本框 edit[i] 、正确答案文本框 labelAns[i] 、正确/错误标签 labelConsq[i] ,将产生的所有算式及相关信息排成一列。同时在函数触发时使用 QTimer 类进行定时,每当定时时间(20s)到达,触发一次 forbidWrite 函数,依次禁止用户输入文本框的输入。
void MainWindow::showExpression() { wDisp=new QWidget; this->setCentralWidget(wDisp); vlayout=new QVBoxLayout; vlayout->addWidget(labelHelp); for(int i=0;i<expression.size();i++) { hlayout[i]=new QHBoxLayout; labelExp[i]=new QLineEdit(expression[i]); labelExp[i]->setReadOnly(true); labelAns[i]=new QLineEdit; labelAns[i]->setReadOnly(true); labelAns[i]->setPlaceholderText("Click to show the answer"); labelConsq[i]=new QLabel; edit[i]=new QLineEdit; edit[i]->setPlaceholderText("Input your answer"); hlayout[i]->addWidget(labelExp[i]); hlayout[i]->addWidget(edit[i]); hlayout[i]->addWidget(labelAns[i]); hlayout[i]->addWidget(labelConsq[i]); vlayout->addItem(hlayout[i]); } buttonAnswer=new QPushButton(tr("Show Answers")); labelGrade=new QLabel(" Grade: "); QHBoxLayout *consq=new QHBoxLayout; consq->addWidget(buttonAnswer); consq->addWidget(labelGrade); vlayout->addItem(consq); wDisp->setLayout(vlayout); connect(buttonAnswer,&QPushButton::clicked,this,&MainWindow::showAnswer); myTimer=new QTimer(); myTimer->setInterval(20000); connect(myTimer,&QTimer::timeout,this,&MainWindow::forbidWrite); myTimer->start(); }
void MainWindow::forbidWrite() { if(nextForbid<expression.size()-1) { edit[nextForbid]->setReadOnly(true); nextForbid++; } else showAnswer(); }
效果如图:
当所有题目的计时总时间用完,或者用户点击 Show Answers 按钮触发 showAnswer 函数,定时器停止工作并禁用所有用户输入文本框和 Show Answer 按钮,并为每一行的算式验证用户答案与正确答案,将正确答案显示在正确答案文本框 labelAns[i] 里,同时将正确与否显示在 labelConsq[i] 里。根据正确题目与题目总数的比例,为用户计算出此次成绩,显示在 labelGrade 里。
void MainWindow::showAnswer() { int count=0; int i; int grade; buttonAnswer->setEnabled(false); myTimer->stop(); nextForbid=0; for(i=0;i<answer.size();i++) edit[i]->setReadOnly(true); for(i=0;i<answer.size();i++) { labelAns[i]->setText(answer[i]); if(QString::compare(edit[i]->text(),labelAns[i]->text())==0) { labelConsq[i]->setText("Correct"); count++; } else labelConsq[i]->setText("Wrong"); } grade=(double)count/answer.size()*100; labelGrade->setText(QStringLiteral(" Grade: ")+QString("%1").arg(grade)); updateHistory(count); }
同时,也要在历史记录中保存此次练习的结果,所以在 showAnswer 中需要更新历史记录,即以读写方式打开当前目录下的 history.txt 文件,并将指针移动到文末添加此次练习的结果。
void MainWindow::updateHistory(int count) { QString path="history.txt"; QFile file(path); if(!file.open(QIODevice::ReadWrite | QIODevice::Text)) { QMessageBox::warning(this,tr("File"),tr("Cannot Update History.")); return; } QTextStream os(&file); os.seek(file.size()); os<< count << " / " << answer.size() << " Grade: " << int((double)count/answer.size()*100) << " "; file.close(); }
效果如图:
若要查看历史记录,则要使用 history 函数,实现方式就是读取上面提到的 history.txt 文件并将内容输出到界面:
void MainWindow::history() { QString path="history.txt"; QFile file(path); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { QMessageBox::warning(this, tr("File"), tr("No History")); return; } QTextStream is(&file); editHistory=new QTextEdit; editHistory->setText(is.readAll()); file.close(); editHistory->show(); }
效果如图:
最后实现错题保存功能与读入功能。这里考虑了MVP模式,即模型与视图分离,且数据具有较好的可移植性,特地使用了 xml 格式来记录和读入错题。Qt提供了完善的 xml 处理函数,即 QXmlStreamReader 和QXmlStreamWriter ,分别可以实现对 xml 文件的读出和写入。用这样一套api和相关文件操作可以轻松实现 xml 文件的各种处理,若是今后软件移植到网页端或移动端,只需调整控制器代码,即可将历史数据复现在新的视图上。
void MainWindow::writeXML() { if(expression.size()==0) { QMessageBox::warning(this,tr("File"),tr("No exercises.")); return; } QString path = QFileDialog::getSaveFileName(this,tr("Save File"),"/",tr("XML Files(*.xml)")); if(!path.isEmpty()) { QFile file(path); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { QMessageBox::warning(this, tr("File"),tr("Cannot Save File.")); return ; } QXmlStreamWriter writer(&file); writer.setAutoFormatting(true); writer.writeStartDocument(); writer.writeStartElement("exercise"); for(int i=0;i<expression.size();i++) { if(QString::compare(labelConsq[i]->text(),QStringLiteral("Wrong"))==0) { writer.writeTextElement("expression",expression[i]); writer.writeTextElement("answer",answer[i]); } } writer.writeEndElement(); writer.writeEndDocument(); file.close(); } else { QMessageBox::warning(this, tr("Path"),tr("You did not save any file.")); return; } }
void MainWindow::readXML() { QString path = QFileDialog::getOpenFileName(this,tr("Open File"),"/",tr("XML Files(*.xml)")); if(!path.isEmpty()) { QFile file(path); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { QMessageBox::warning(this, tr("File"),tr("Cannot Open File.")); return ; } expression.clear(); answer.clear(); QXmlStreamReader reader; reader.setDevice(&file); while (!reader.atEnd()) { QXmlStreamReader::TokenType type = reader.readNext(); if (type == QXmlStreamReader::StartElement) { QString elementName=reader.name().toString(); if(QString::compare(elementName,QStringLiteral("expression"))==0) expression.push_back(reader.readElementText()); if(QString::compare(elementName,QStringLiteral("answer"))==0) answer.push_back(reader.readElementText()); } } if (reader.hasError()) { QMessageBox::warning(this, tr("XML"),reader.errorString()); } file.close(); if(expression.size()!=answer.size()) { QMessageBox::warning(this, tr("XML"),tr("Wrong XML Format.")); return; } showExpression(); } else { QMessageBox::warning(this, tr("Path"),tr("You did not select any file.")); return; }
以上就是全部的UI核心功能实现。
Bug总结
因为以前有一些Qt经验,所以这次没遇到什么卡着过不去的bug,但总归遇到了一些小问题:
- 这次的UI中因为存在连续更改标签和输入框的情况,最好将所有可能用到的标签、文本框、布局都定义成 MainWindow 类的成员,每次重新使用前 new 一下这样不容易出现内存问题。我一开始因为有的定义成了成员,有的定义在堆上,有的定义在栈上,所以有时候函数执行完,对象就不见了,或者出现内存错误程序停止运行。为避免这样的问题,最好的方式就是像一开始说的那样,频繁更改的对象定义成类成员并建立在堆上。
- 一开始我所有的UI界面都是用代码写的,这对题目展示还凑合,但对于初始界面想用代码写的很美观就太困难了,所以最后我还是用了UI设计。可见对于位置、字体等美观要求较高的界面还是用UI设计较高效。但UI设计还有一个问题,就是经常更改了UI控件的属性,运行后却发现还是老样子,这是Qt一个固有问题,经网上查资料,我发现最好的方式是在debug模式下更改UI设计,而在release模式下运行,这样UI控件都能及时更新。
- 这次还有一个bug困扰了我不少时间,就是我的习惯是使用 this->setCentralWidget() 来显示新的界面,然后存在一个问题是当我多次通过该方式切换界面后,再切换一些特定的界面程序会停止运行,比如多次读错题后再读 history 再读错题,这时会出问题。我分析原因可能在于 MainWindow::setCentralWidget 在一些特定的切换下新界面是不能对原界面进行覆盖的。为解决问题,我最后对一些界面采用了 show 函数,即开启新的一个窗体,这样就不存在任何问题了。
对“走上工作岗位后是否选择结对编程用于解决部分任务”的看法
我觉得走上工作岗位后对部分任务采取结对编程是必要的。
首先,刚刚进入岗位时,对相关技术还不够熟悉,此时需要一个代码方面的老师来指导具体的编写技巧,由驾驶员指导领航员驾驶的技巧。同时,作为领航员虽然代码编写经验不足,但是可以用自己在其他方面的经验和逻辑思维能力,帮助驾驶员检查代码中的漏洞,提供多样化的思维方式。
其次,就算不是企业中的新手,当遇到逻辑十分复杂的项目,也很适合结对编程。两个结对者首先讨论出可行的设计方案,当驾驶员行驶时,领航员时刻监控着逻辑网的搭建,一旦出现问题立刻反馈给驾驶员,同时领航员在行进过程中也时刻思考着逻辑上的简化与更好的解决方案,时刻能对代码进行优化。当逻辑最复杂的一段路行驶完后,两个结对者可以分道扬镳,继续进一步实现不同的功能模块,由于他们对基础模块的来历都十分清晰,无需过多的交流,他们立刻都能立刻稳定地再次出发。
最后,结对也是个互相学习的过程,技术随着时代一直在变,结对是一个很好地彼此学习交流的机会,能够促进良性竞争。
课程建议
首先必须给邓老师和助教们点赞,与其他老师助教相比你们的工作量要大很多,邓老师是我见过水群最多的老师,可见各位老师助教都很负责。
要说建议的话,我希望今后的作业能够弹性更大一点,或者说要求更宽松一点。邓老师希望以软件企业的要求来给我们更真切的体验与更丰富的经验,但是毕竟我们大部分人的志向都不是软件开发人员,我们各有各的方向,这点是与其他学校软件工程班不同的。有时候作业的要求有点矫枉过正,班里有很多大三的学生忙着申请暑研,大家也都有繁忙的其他课程,作业的要求与ddl如果太死会给我们很大压力。如果是因为要熟悉企业的管理方式、获得更多的经验,却丢掉了课内其他课程的许多内容,可能有点得不偿失。所以,我希望作业的弹性能够大一些。
今后学习方式
今后要在每次的个人作业、结对作业及课后作业中吸取经验,改完每个bug或是看完每套准则后,都要带着思考这些经验怎么能用于我的团队项目,或者说我的团队项目在哪可能出现这样的bug或者用到这样的准则,每次积累一点经验,逐步改善开发流程。同时,要将每次的个人作业的方向往团队项目上靠,比如我的团队项目是android开发,那我今后的读书笔记方向就可以是java设计模式,多借鉴别人的经验比自己一点点摸索要快得多。