(防扒小助手)
本人CSDN博客:
https://blog.csdn.net/m0_61753302 ;
本人博客园博客(同步CSDN):
何以牵尘 – 博客园 (cnblogs.com)https://www.cnblogs.com/kalesky/;
如果对你有用的话欢迎点赞关注哟!
目录
3.2.5 任务5:投票活动Poll的实现类GeneralPollImpl
3.3.2 任务8:采用Strategy设计模式实现灵活的计票规则
3.3.3 任务9:采用Strategy设计模式实现灵活的遴选规则
3.3.5 任务11:采用Visitor设计模式实现功能扩展
3.5.3 聚餐点菜应用:取消权重设置、只计算”喜欢”的票数
1、实验目标概述
本次实验覆盖课程第2 、3 章的内容,目标是编写具有可复用性和可维护性
的软件,主要使用以下软件构造技术:
- 子类型、泛型、多态、重写、重载
- 继承、委派、CRP
- 语法驱动的编程、正则表达式
- 设计模式
本次实验给定了多个具体应用,学生不是直接针对每个应用分别编程实现,而是通过 ADT 和泛型等抽象技术,开发一套可复用的ADT 及其实现,充分考虑这些应用之间的相似性和差异性,使ADT 有更大程度的复用(可复用性)和更容易面向各种变化(可维护性)。
2、 实验环境配置
2.1 实验环境
Intellij IDEA 2022.1(Ultimate Edition)
2.2 GitHub Lab3仓库的URL地址
略。
3、实验过程
3.1 待开发的三个应用场景
3.1.1 应用场景
① 商业表决(BusinessVoting)
面向某个商业公司,其内部成员提出某个商业提案(例如”关于投资xx 项目的提案”),公司董事会的各位董事对其进行实名表决(支持、反对、弃权),各董事在表决中的权重取决于其所持有的公司股票的比例,根据该持股比例对投票结果进行计算,若”支持”票的比例超过2/3 ,则该提案通过,否则该提案不通过。
② 代表选举(Election)
针对某次活动(例如哈工大学生代表大会),需要从一群候选人中选出若干人,作为代表参加活动。在该选举中,提前确定一部分候选人,投票人从已确定的候选人中选取,不可提名新的候选人。计划选出的代表数量是提前确定的。投票人针对每个候选人匿名选择”支持、反对、弃权”之一,但选择”支持”的人数不能高于计划选出的代表数量,否则为非法票。所有投票人的权重均相等。
③ 聚餐点菜(DinnerOrder)
一群人去餐馆就餐,需要从该餐馆提供的菜单中选择若干道菜,点菜的数量要大于等于就餐总人数,且小于总人数+5 。每个人针对菜单上的每一道菜实名表达自己的喜好(喜欢、不喜欢、无所谓),选择这三个选项的数目无限制。不同人的身份不同,其偏好的影响力会有所不同(例如家庭聚餐时,老人的权重更大、子女的权重更小,见下表黄色部分)。所有人表达观点之后,根据影响力加权计票(喜欢、不喜欢、无所谓分别得分2 、0 、1 ),取总得分最高的前k道菜。
3.1.2 共性需求
3.1.3 差异
3.2 ADT识别与设计
3.2.1 任务1:投票类型VoteType
(1)测试策略
根据VoteType中方法的spec,对checkLegality和getScoreByOption方法进行设计测试。
checkLegality():
通过对已有的投票类型和未出现的投票类型进行checkLegality的正确性测试。
getScoreByOption():
给定的投票选项的名称,通过测试其getScoreByOption返回的值是否与给定值相等进行正确性的判断。
(2)字段和方法
① spec规约
选项应该至少有2个,每个选项的长度不超过5,且不允许出现空格。
使用options实现的函数对应关系的映射。
必要时使用防御性拷贝防止表示泄露。
② 字段:
key表示选项名字,value表示该选项所对应的权重。
用来记录哪个选项表示支持。
③ 方法:
检查不变性:如果有选项名称长度超过5或者少于两个选项,则返回false,否则返回true。
默认使用”支持”(1)|”反对”(-1)|”弃权”(0)这三种投票选项。
传入一个给定的Map类型参数,建立一个VoteType类型的对象。
根据满足特定语法规则的字符串,创建一个投票类型对象。
判断一个投票选项是否合法(用于Poll中对选票的合法性检查)。
根据一个投票选项,得到其对应的分数。
返回VoteType中的支持选项的选项名。
(3)测试结果
测试结果运行如下:
3.2. 2 投票项 VoteItem
(1)测试策略
根据VoteItem中方法的spec,对getCandidate和getVoteValue方法进行设计测试。
getCandidate():
给定一个投票项,通过观察调用getCandidate()返回值是否为给定的candidate。
getVoteValue():
给定一个投票项,通过观察调用getVoteValue()返回值是否为给定的value。
(2)字段和方法
① spec规约
选项名称不允许出现空格。
投票选项对候选人类型及String类型的映射
字段使用private,必要时使用防御性拷贝防止表示泄露。
② 字段
本投票项所针对的候选对象
对候选对象的具体投票选项,例如”支持”、”反对”等
③ 方法
检查不变性:如果有选项名称长度超过5或者出现空格,则返回false,否则返回true。
创建一个投票项对象
得到该投票选项的具体投票项
得到该投票选项所对应的候选人
(3)测试结果
测试结果运行如下:
3.2.3 任务3:选票Vote
(1)测试策略
根据VoteItem中方法的spec,对getCandidate和getVoteValue方法进行设计测试。
getCandidate():
给定一个投票项,通过观察调用getCandidate()返回值是否为给定的candidate。
getVoteValue():
给定一个投票项,通过观察调用getVoteValue()返回值是否为给定的value。
(2)字段和方法
① spec规约
合法的voteItem
对应到自己的成员变量
rep均为private,采用防御式拷贝
② 字段
已经产生了多少票,用于为id计数
用于标记投票的序号
一个投票人对所有候选对象的投票项集合
投票时间
③方法
创建一个选票对象
查询该选票中包含的所有投票项
一个特定候选人是否包含本选票中
(3)测试结果
测试结果运行如下:
3.2.4 任务4:投票活动Poll 的测试
(1)测试策略:
由于Poll
(2)测试方法:
① setInfo()
② addCandidates()
③ addVoters()
④ addVote()
3.2.5 任务 5 :投票活动 Poll 的实现类 GeneralPollImpl
(1)spec规约
(2)字段和方法
① rep
② 方法
检查不变量:quantity需要大于0
设置投票活动的相关信息
向投票活动中添加voters及其对应的权重
向投票活动中添加candidates
检查投票是否合法
想投票活动中添加选票
检查票的合法性
按规则计票
按规则遴选
输出遴选结果
public void accept(Visitor visitor);
public String getName();
public Calendar getDate();
public Set
(3)测试结果
测试结果如下:
3.2.6 任务6:投票活动 Poll 的子类型
(1)B usinessVoting
需要重写addVote和checkVotes方法
候选对象数量只能为1,必须为实名投票,在计算得票时还需考虑投票人所占权重。
① addVote方法重写如下:
先检查vote是否为实名投票,再检查vote是否合法,若vote不合法或者voters中未包含该vote的投票人,则将该选票列入illegalVotes集合中。
② checkVotes 方法重写如下:
对于votes中的每一个vote,如果vote的voter不包含在该投票活动poll的voters中,则将该选票列入illegalVotes集合中。若在已投票的投票人集合votedVoter中包含了该voter,则将该投票人列入reVoters中,并在之后的计票活动中将重复投票的voter的选票不计入在内。
(2) Election
需要检查一个投票人投的支持票总数是否小于等于候选对象数量,该投票活动支持匿名投票。所有投票人的权重均相同。
① 增添一个新方法supportCount
用于返回一个选票内的赞成票数量。
② 重写addVote方法:
对vote进行判断,如果vote非法,则将其加入非法选票集合illegalVotes。 如果该投票人投的支持票数量大于候选人数量,也将其加入非法选票集合illegalVotes。
(3) DinnerOrder
需要重写addVote和checkVotes方法
必须为实名投票,在计算得票时还需考虑投票人所占权重。
① addVote方法重写如下:
先检查vote是否为实名投票,再检查vote是否合法,若vote不合法或者voters中未包含该vote的投票人,则将该选票列入illegalVotes集合中。
② checkVotes 方法重写如下:
对于votes中的每一个vote,如果vote的voter不包含在该投票活动poll的voters中,则将该选票列入illegalVotes集合中。若在已投票的投票人集合votedVoter中包含了该voter,则将该投票人列入reVoters中,并在之后的计票活动中将重复投票的voter的选票不计入在内。
(4)测试结果
① B usinessVoting Test
Junit测试结果如下:
② ElectionTest
Junit测试结果如下:
③ DinnerOrderTest
Junit测试结果如下:
3.3 ADT行为的设计与实现
3.3.1 任务7:合法性检查
GeneralPollImpl中将合法性检查的行为抽取到一个单独的方法isLegal之中进行合法性检查:
对一张选票的各投票选项进行考察,若投票选项与候选人数目不相同,则返回非法值false。
之后对投票选项进行遍历每一个投票,若所投票的对象不包含在本次投票的候选人之中,或者投票的类型与所投值不相符号,则返回非法值false。
在addVote方法中对isLegal方法进行调用即可:
并将isLegal判断所对应的非法选票加入到非法选票的集合illegalVotes中。
3.3.2 任务8:采用Strategy设计模式实现灵活的计票规则
(1)BusinessStatistics计票规则
遍历Vote
由于BusinessVoting是实名投票,故需要获取投票人的权重以便进行最终结果的计算。
对每一张选票vote的每一个投票选项VoteItem
(2)ElectionStatistics计票规则
遍历Vote
由于Election是匿名投票,所以不需要获取投票人的权重等信息。
对每一张选票vote的每一个投票选项VoteItem
(3)DinnerStatistics计票规则
遍历Vote
由于DinnerOrder是实名投票,故需要获取投票人的权重以便进行最终结果的计算。
对每一张选票vote的每一个投票选项VoteItem
3.3.3 任务 9 :采用Strategy设计模式实现灵活的遴选规则
(1)BusinessSelection遴选规则
初始化proposal的占比为0.0,本规则对Business进行了扩展,可同时提出多个proposal而不仅限于一个proposal,以适应未来任务的要求。
若对应proposal的计票结果超过了2/3,则将其放入result中传出给结果。即遴选出最终方案。
(2)ElectionSelection遴选规则
首先将所有候选人放入TreeSet中利用TreeSet进行排序:
Set
set =new TreeSet <>(new Comparator () {
@Override
public int compare (Person o1,Person o2) {
if (statistics .get (o1) >statistics .get (o2))return -1 ;
else if (statistics .get (o1) <statistics .get (o2))return 1 ;
return o1.getName ().compareTo (o2.getName ());
}
});
set .addAll (statistics.keySet ());
Election要求根据支持票数量排序,前k个候选人当选;若有多个候选人的支持票数量相等而无法自然排出前k名,则仅有那些明确可进入前k名的人当选。
因此需要判断第k名和第k+1名的分数是否相同,若得分不同,则取出这前k名作为最终结果传递给result输出。若得分相同则需要从头重新遍历,取出前面与这第k名的分数不同的所有候选人Person,传递给result输出。
double rank =0.0 , score =Integer . MIN_VALUE;
Iteratoriterator =set .iterator ();
for (int i =0 ; i <Math .min(quantity, statistics.size ()); i++) {
score = statistics.get (iterator.next ());
}
if (!iterator.hasNext () || statistics.get (iterator.next ()) != score) {
iterator =set .iterator ();
for (int i =0 ; i <Math .min(quantity, statistics.size ()); i++) {
results .put (iterator.next (), ++rank);
}
}
else {
iterator =set .iterator ();
Person person = iterator.next ();
while (statistics.get (person) != score) {
results .put (person, ++rank);
person = iterator.next ();
}
}
return results ;
(3)DinnerSelection遴选规则
DinnerSelection的遴选规则类似Election的遴选规则,区别在于在其第k个和第k+1个数目相同时,除了取前面与第k个得分不同的候选对象,DinnerSelection还需要在这些得分相同的Dish中选出一部分补齐这k个菜。
因此都在前面使用TreeSet对候选对象的得分结果进行排序,最后DinnerSelection不需要重新对Dish进行遍历取出,只需要按照顺序自然取出这k个菜即可。
3.3.4 任务 10 :处理匿名和实名投票
应用1提案表决和应用3聚餐点菜是实名投票,而应用2代表选举是匿名投票。
vote 包中的Vote类缺省为”匿名投票”,其rep中没有出现投票人信息。为了支持实名投票,在选票 ADT 中额外记录投票人信息。构造Vote的子类型 RealNameVote,并使用Decorator设计模式,在Vote基础上进行扩展,为其绑定新的行为,通过委托机制增加到对象上。
构造方法在 Vote 的基础上,增加 voter 属性,同时调用父类的构造方法,并给予相应的参数列表。
新增 getVoter()方法获得投票人,getVoteItemsByVoter()通过投票人获得其对应的选票,并继承了父类的其他方法。
重写 equals()方法时,采取判断数据等价,其中涉及到关于判断投票选项是否相等时,调用父类的equals()方法,依然是引用等价的判断。
RealNameVote的测试用例,在 3.2.6 节中的应用1和应用3已经覆盖,这里不做赘述。
3.3.5 任务 11 :采用Visitor设计模式实现功能扩展
访问者 Visitor 模式将算法与其所用的对象隔离开来,为ADT预留一个将来可以作为扩展功能的接入点,外部代码可以在不改变ADT本身的情况下在需要时通过委托接入ADT。
新增visitor包,并新建接口Visitor及其实现类VotedLegalVisitor。
在实现类VotedLegalVisitor中实现了接口中的两个方法:
public void visit (GeneralPollImpl
poll);
public double getData ()
其中visit的作用是直接对投票进行访问,并进行信息统计;getData的作用是获取统计信息。
在接口Poll中及其实现类GeneralPollImpl中预留了一个accept方法:
使用以下操作调用Visitor方法:
对于其测试用例,在test文件夹中新建包visitor,新增测试文件VisitorTest类,在投票结果下输出合法选票的比例。
Junit测试结果如下:
3.3.6 任务 12 :基于语法的数据读入
该部分要求用正则表达式接受格式形如{“喜欢”(2)|”不喜欢”(0)|”无所谓”(1)}和{“支持”|”反对”|”弃权”}这两种字符串来创建投票类型VoteType对象。要求每个选项不能出现空白符,且长度≤5。
这里我先用String的split方法先将各个选项分割开:
String []input = regex.split (“\|”);
然后对于每个选项尝试用正则表达式去匹配,用捕获组来获取各个成分。用一个变量state记录一下这个正则表达式是哪种上面情况(带数字的/不带数字,默认权值相等的)。state=1为带数字的,state=2为不带数字的。
任务要求的两种正则表达式即为如下两种:
“\S+”([+-]?\d+)
“\S+”
考虑到java语法,需要使用转义,因此最终java语句中使用如下两种实现:
Pattern regexNum =Pattern .compile(“\\”(\S+)\\”\(([\+-]?\d+)\)”);
Pattern regexNoNum =Pattern .compile(“\\”(\S+)\\””);
匹配完成之后使用其内部各成分来创建VoteType的options字段。
对于其测试用例,采取正确输入和错误输入两种等价类划分。在正确输入中,又分别测验两种正则表达式。测试用例在 VoteTypeTest 中新增测试。
Junit测试结果如下:
3.4 任务1 3 :应用设计与开发
3.4.1 商业表决系统
首先定义GeneralPollImpl的子类BusinessVoting类,依次设立基本信息,增加候选提案(只有1个)、投票人、选票,然后开始计算支持率和结果,并调用Visitor模式中的合法选票比例方法,最终控制台输出结果。若提案没有通过,则输出”本次投票未选出合法结果”;否则输出通过的信息。
输出样例如下:
3.4.2 代表选举系统
首先定义GeneralPollImpl的子类Election类,依次设立基本信息,增加候选学生、投票人、选票,然后开始计算支持率和结果,并调用Visitor模式中的合法选票比例方法,最终控制台输出结果。
输出样例如下:
3.4.3 聚餐点菜系统
首先定义GeneralPollImpl的子类DinnerOrder类,依次设立基本信息,增加候选菜品、投票人、选票,然后开始计算支持率和结果,并调用Visitor模式中的合法选票比例方法,最终控制台输出结果。
输出样例如下:
3.5 任务1 4 :应对面临的新变化
3.5.1 商业表决应用:可以一次表决多个商业提案
原来并未限定BusinessVoting的rep只能为一个提案,因此可以直接使用原先的BusinessStatistics策略进行统计遴选,下面给出测试用例:
3.5.2 代表选举应用: 遴选规则变化
该改动要求我们在支持票相同时,比较反对票,反对票少的胜出。问题的关键在于,反对票在statistics方法里没有统计。如果我们想要获取这个信息,要么改动接口里的statistics方法和所有Poll子类的statistics方法,要么就得重新统计一遍反对票的信息。显然前一种修改的代价太大了,因此这里采用的是重新统计的方法。可以看出这要求遴选策略的直接改动,于是新建一个NewElectionSelection,实现SelectionStrategy,但是它的构造方法需求额外的三个参数,在构造方法里计算一下反对票的信息:
然后在select方法中重写一下TreeSet的排序规则:
这里使用两个变量分别记录第k个人的赞成票数量和反对票数量。先去用赞成票比较第k+1个人的赞成票数量,如果不等,则按照原来的方法得出结果,如果相等则比较第k+1个人的反对票数量,利用与赞成票相似的规则再次进行判断:与第k+1个人的反对票数量不等则录入并输出结果,反对票数量相等则按顺序录入直到反对票数量与第k个人相等或已经录入k个人为止。代码实现如下:
原先的排名结果是ABC、GHI,因为测试给定的条件是:ABC、GHI的支持票数量相等但GHI反对票数量少于ABC,排名结果应为GHI、ABC。因此可知测试用例的结果与预期相符。
3.5.3 聚餐点菜应用:取消权重设置、 只计算”喜欢”的票数
需要创建一个新的计分策略NewDinnerStatistics,把计分部分修改一下之后用的时候拿这个策略去替换原来的DinnerStatistics。
计分部分修改如下:
if (!newDinnerResult .containsKey (dish ))newDinnerResult .put (dish ,0.0 );
double value =newDinnerResult .get (dish );
if (voteItem .getVoteValue ().equals (voteType.getSupport ()))
newDinnerResult .put (dish ,value +1.0 );
其余部分不需要变动。测试结果如下:
可见所有候选对象的权值全变为相同权值了,因此最终结果的排序按字典排序处理。
3.6 Git仓库结构
(1)git log输出结果
(2)Git GUI中的分支图
4、实验进度记录
日期
时间段
计划任务
实际完成情况
6.20
15:00-18:00
完成 VoteType 及其测试用例
按计划完成
6.21
8:00-11:00
完成VoteItem及其测试用例
按计划完成
6.22
16:00-20:00
完成 Vote 及其测试用例
按计划完成
6.23
19:00-21:00
完成Poll接口的具体实现与测试
未完成
6.24
7:00-23:30
完成具体计票规则及遴选规则的撰写
按计划完成
6.25
9:00-次日3:00
完成基于语法的数据读入与app实现
未完成
6.26
8:00-21:30
完成功能扩展与撰写报告
按计划完成
5、实验过程中遇到的困难与解决途径
遇到的难点
解决途径
正则表达式的语法在java语句中的具体应用与java相关模块的具体实现语法了解不够深入。
上网查阅相关资料并咨询同学,并在实现过程中不断修改与理解。
理解给定的框架。
在实现 ADT 的途中不断理解、修改。
对于扩展功能的具体实现策略,如何简便迅捷地扩展实现感觉颇为棘手。
自己苦思冥想并咨询经验丰富的同学一些相关方法的实现手段。
撰写相关规约与测试策略困难,不知道怎么写
自己苦思冥想并在交流群里浏览他人心得体会。
对接口与子类的继承关系不够熟悉。
在实现过程中反复尝试,并在网上查阅相关文献,加深印象。
6、实验过程中收获的经验、 教训、感想
6.1 实验过程中收获的经验和教训(必答)
第一,spec规约对于一个程序员来说十分重要,本次实验中补全一个ADT的spec的过程十分折磨,因为需要具体分析其各种可能出现的情况并要找出其不变量还要考虑到防止表示泄露的情况,但是当spec补全以后,很多测试用例的撰写以及相关调用的时候十分方便,因为通过spec我能很快了解到这个ADT的功能是什么,该怎么去调用这个spec。
第二,根据已有框架展开编写代码有好有坏。好处在于可以省去构思整体布局的时间,坏处在于如果对于框架理解不到位将产生很复杂且严重的连锁反应,并且少了一次锻炼且提升自己的机会,并且框架如果设计不够合理,再去整体改动框架将会十分复杂,甚至所需要的付出的精力不亚于重新实现这个框架。
第三,我本以为本次实验工程量不会很大,因此前几天没有花出足够的时间去完成实验,很多工作量都压到了最后两天,为了赶ddl而不得不通宵写实验,这不仅影响身体健康,也对工程能否按时交付有很大影响。因此我收获到了凡事都要提早完成的教训,不能想当然认为什么都可以在ddl之前能够有足够的时间去做完。
6.2 针对以下方面的感受 (必答)
(1)重新思考Lab2中的问题:面向ADT的编程和直接面向应用场景编程,你体会到二者有何差异?本实验设计的ADT在三个不同的应用场景下使用,你是否体会到复用的好处?
答:面向 ADT 的设计是自底向上,先设计每一部分的操作,封装后形成 ADT,不受具体数据类型的限制。
面向应用场景的自顶向下,先考虑需求,再考虑用什么方法实现,不同的数据类型会形成不同的代码。
三个不同场景下的应用,且其中有很多的共性,则要基于共性进行复用,在各自个性的基础上进行各自的重写。多次复用比多次重新实现方便简单很多,也减少了很多无谓的工作量。
(2)重新思考Lab2中的问题:为ADT撰写复杂的specification,invariants,RI, AF,时刻注意ADT是否有rep exposure,这些工作的意义是什么?你是否愿意在以后的编程中坚持这么做?
答:使编写的代码更加安全和可读性更强,也便于他人快速理解。愿意这么做。
(3)之前你将别人提供的ADT/API用于自己的程序开发中,本次实验你尝试着开发给别人使用的ADT/API,是否能够体会到其中的难处和乐趣?
答:能够体会到,难处很高,但也趣味横生。
(4)你之前在使用其他软件时,应该体会过输入各种命令向系统发出指令。本次实验你开发了一个简单的解析器,使用语法和正则表达式去解析一个遵循特定规则的字符串并据此构造对象。你对语法驱动编程有何感受?
答:基于正则表达式进行解析可以省去大量的通过while和if语句进行判断的操作,方便编程并且提高代码的美观性。
(5)Lab1和Lab2的工作都不是从0开始,而是基于他人给出的设计方案和初始代码。本次实验中也提供了一部分基础代码。假如本实验要求你完全从0开始进行ADT的设计并用OOP实现,你觉得自己是否能够完全搞定?你认为”设计ADT”的难度主要体现在哪些地方?
答:能搞定。对各种应用共性和个性的把握,对整体框架结构的设计,对不同应用特性的分析,各种 ADT 中属性的把握,基于可复用性、正确性、可持续性模式的使用,提高框架的可延展性。
(6)”抽象”是计算机科学的核心概念之一,也是ADT和OOP的精髓所在。本实验的三个应用既不能完全抽象为同一个ADT,也不是完全个性化,如何利用”接口、抽象类、类”三层体系以及接口的组合、类的继承、委派、设计模式等技术完成最大程度的抽象和复用,你有什么经验教训?
答:将宏观上共性的操作放在接口中实现,将共性的操作在抽象类中进行声明,必要的时候可以实现,将各自的个性放在实现类中进行重写。
继承和委派都是在现有类的基础上增加新的属性、操作的方法,可以提高代码的复用性,是很好的方法模式。
(7)关于本实验的工作量、难度、deadline。
答:工作量很大很大,难度较高,deadline时间很紧张。
(8)课程结束了,你对《软件构造》课程内容和任课教师的评价如何?
答:课程很好,可以说是计算机专业不可缺少的一门课,质量很高,但是学时很少,要求掌握的内容很多,导致很多东西学的不深入不扎实,多快好省反而适得其反。
任课教师还不错。
Original: https://www.cnblogs.com/kalesky/p/16573431.html
Author: 何以牵尘
Title: 哈工大软件构造Lab3(2022)
原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/593535/
转载文章受原作者版权保护。转载请注明原作者出处!