0


哈工大2022软件构造Lab3

说明

此博客内容为哈工大2022春季学期软件构造Lab3:Reusability and Maintainability oriented Software Construction,文章为个人记录,不保证正确性,仅供练习和思路参考,请勿抄袭。实验所需文件可以从这里获取(若打不开可以复制到浏览器)。
实验环境:IntelliJ IDEA 2022.1(Ultimate Edition)
注:由于博客主要为思路提示,部分实验要求没有提及,请以实验指导书的要求为准;博客与指导书相比可能顺序略有调整;由于本文是完成整个任务后所写,一些后面补充的rep/方法可能会出现在前面。框架里的JUnit是JUnit5可能会报错,可以按照Lab2改成JUnit4.

任务1

这部分要求完成VoteType的测试用例以及实现代码等。这个类里只有一个从String到Integer的Map,(key,value)表示值为key的选项得分为value。我们需要补充的有构造方法以及两个辅助方法、对hashCode和equals的重写。这部分都不难实现。由于后面判断合法性时需要统计支持票的数量,任务14还需要统计反对票的数量,又因为任务12让我们支持正则表达式的输入,我们没法确定支持票/反对票一定是哪个选项,所以我在这还额外记录了一下哪个选项代表支持(比如用户把2分的“赞成”作为支持票,而不是指导书里给的1分的“支持”),哪个选项代表反对(这部分只在任务14的change branch中出现)。

publicclassVoteType{// key为选项名、value为选项名对应的分数privateMap<String,Integer> options =newHashMap<>();//由于Election需要统计赞成票个数,需要记录哪个字符串对应赞成//这里取得分最高的选项为赞成票privateString support;}

我写了两种构造方法(任务12中有第三种),分别是一个无参的(默认为支持1/弃权0/反对-1):

publicVoteType(){
    options.put("支持",1);//支持
    options.put("反对",-1);//反对
    options.put("弃权",0);//弃权
    support ="支持";assertcheckRep();}

和另一个给定Map的,这时候需要防御性拷贝一下:

publicVoteType(Map<String,Integer> origin){this.options =newHashMap<>(origin);int maxval =Integer.MIN_VALUE;for(String str: options.keySet()){if(str.length()>5)thrownewIllegalArgumentException("非法输入:选项名过长");if(!str.matches("\\S+"))thrownewIllegalArgumentException("非法输入:出现空白符");if(options.get(str)> maxval){// 更新最大值和支持选项
            maxval = options.get(str);
            support = str;}}assertcheckRep();}

我的RI是:选项应该至少有2个,并且长度

    ≤
   
   
    5
   
  
  
   \le 5
  
 
≤5,不能出现空白符(任务12正则表达式中的要求)。
privatebooleancheckRep(){for(String str: options.keySet()){if(str.length()>5||!str.matches("\\S+"))returnfalse;}return options.size()>=2;}

两个public的辅助方法很简单,直接查询Map就可以了,这里不再给出。
测试单元,这个是框架里自带的:

@Testpublicvoidtest(){VoteType voteType =newVoteType();assertTrue(voteType.checkLegality("支持"));assertTrue(voteType.checkLegality("反对"));assertTrue(voteType.checkLegality("弃权"));assertFalse(voteType.checkLegality("强烈反对"));assertEquals(1, voteType.getScoreByOption("支持"));assertEquals(-1, voteType.getScoreByOption("反对"));assertEquals(0, voteType.getScoreByOption("弃权"));}

任务2

这部分任务要求完成VoteItem的相关部分。
一个VoteItem只有一个泛型的candidate和一个String类型的value,表示该票为投给candidate的,投票选项为value。RI还是检查value的合法性:长度

    ≤
   
   
    5
   
   
    ,
   
  
  
   \le 5,
  
 
≤5,不包含空格。这部分也没什么好说的,hashCode这些简单的方法就不给出了。
privatebooleancheckRep(){return value.matches("\\S+")&& value.length()<=5;}/**
 * 创建一个投票项对象 例如:针对候选对象“张三”,投票选项是“支持”
 *
 * @param candidate 所针对的候选对象
 * @param value     所给出的投票选项
 */publicVoteItem(C candidate,String value){this.candidate = candidate;this.value = value;assertcheckRep();if(!checkRep()){thrownewIllegalArgumentException("非法输入:选项不合法");}}

测试单元(也是原先框架里有的):

@Testpublicvoidtest(){VoteItem<String> voteItem1 =newVoteItem<>("Alice","支持");VoteItem<String> voteItem2 =newVoteItem<>("Bob","支持");VoteItem<String> voteItem3 =newVoteItem<>("Alice","支持");assertEquals("Alice", voteItem1.getCandidate());assertEquals("支持", voteItem1.getVoteValue());assertTrue(voteItem1.equals(voteItem3));assertFalse(voteItem1.equals(voteItem2));}

任务3

补充Vote相关的代码。Vote的Rep里有一个VoteItem的集合和一个date。这里我补充了一个私有的id变量作为一个Vote的唯一标识,每次调用构造方法时给Vote对象分配一个。原因是:如果采用Date和voteItems来判断两个Vote是否相等,考虑下面的这个投票:

投票人2和投票人5的Vote如果Date在毫秒级别上也是一样的(Date的精度就到毫秒),那么他们两个的票就会被当成一样的,只会有一个被放到HashSet之类的容器中,这显然是不对的。如果这两票是在很短的时间内连续new的,它们的Date很有可能就是看不出区别的(写的时候踩过这个坑,2和5被当成了一票,统计的时候总是少了一票)。所以我就不用Date和VoteItem作为equals的依据了,而是给每个Vote分配一个唯一的id,用它作为唯一标识,就避免了上述问题。
构造方法:

//已经产生了多少票,用于为id计数staticprivateint num =0;//用于标记投票的序号,引入这个变量是因为两个几乎同时创建的不同Vote的Date是一样的,会导致它们被判定为同一个Voteprivateint id;// 一个投票人对所有候选对象的投票项集合privateSet<VoteItem<C>> voteItems =newHashSet<>();// 投票时间privateCalendar date =Calendar.getInstance();//...//构造方法publicVote(Set<VoteItem<C>> voteItems){this.id =++Vote.num;this.voteItems.addAll(voteItems);assertcheckRep();}

至于RI,我认为这层没什么好检查的。VoteItem已经保证了自己的合法性,candidate需要与特定的投票关联到一起才会产生“合法”的意义,因此我就让checkRep始终返回true了。
测试单元:

@Testpublicvoidtest(){Set<VoteItem<String>> voteItems =newHashSet<>();
    voteItems.add(newVoteItem<String>("Alice","支持"));
    voteItems.add(newVoteItem<String>("Bob","支持"));
    voteItems.add(newVoteItem<String>("Cathy","反对"));Vote<String> vote =newVote<String>(voteItems);assertTrue(vote.candidateIncluded("Alice"));assertTrue(vote.candidateIncluded("Bob"));assertFalse(vote.candidateIncluded("Tracy"));assertEquals(voteItems, vote.getVoteItems());}

任务10

因为实名投票就是在匿名投票上的扩展,就把这部分提到前面了。对于指导书中的三个场景,商业决议和晚餐点菜都是实名的,而刚才的Vote是匿名的。因此我们需要扩展Vote类型,使其能额外携带一个投票人Voter的信息。指导书里提示可以用继承/装饰器模式实现,这里我为了简便直接使用继承实现了:

publicclassRealNameVote<C>extendsVote<C>{//投票人privateVoter voter;publicRealNameVote(Voter voter,Set<VoteItem<C>> voteItems){super(voteItems);this.voter = voter;}publicVotergetVoter(){returnthis.voter;}}

GeneralPollImpl

任务4-任务9即这次实验的主体部分。我做这个实验的顺序是前面的Immutable ADT

    →
   
  
  
   \rightarrow
  
 
→GeneralPollImpl

 
  
   
    →
   
  
  
   \rightarrow
  
 
→三种子类型

 
  
   
    →
   
  
  
   \rightarrow
  
 
→在此基础上完成任务11-14.本博客也按照从一般(Poll)到特殊(三个具体场景)安排。JUnit的部分代码会放在后面。

Poll接口

里面有一个静态create方法需要补充。只需要返回任意一个子类型(虽然这时候还没实现)即可。我们返回Election类型。

publicstatic<C>Poll<C>create(){return(Poll<C>)newElection();}

GeneralPollImpl

我们把三个子类中共性的地方留到GeneralPollImpl中实现,在具体的子类里再特殊调整(Override)一下。如果有需要也可以把这个类设成抽象的,部分方法留到子类中再实现。

Rep的补充与修改

由于我们在计票的时候需要判定哪些票是不合法的(而不能直接删除它们!),我们需要拿一个集合保存一下不合法的票。

//增加的rep:为了标记选票的合法性,用Set记录不合法的选票protectedSet<Vote<C>> illegalVotes =newHashSet<>();

此外,以下的rep被我修改成了protected(而非原来的private),以便在子类中进行查询。

// 投票人集合,key为投票人,value为其在本次投票中所占权重protectedMap<Voter,Double> voters;// 拟选出的候选对象最大数量(赋了一个大的初值)protectedint quantity =Integer.MAX_VALUE;// 所有选票集合protectedSet<Vote<C>> votes =newHashSet<>();// 计票结果,key为候选对象,value为其得分protectedMap<C,Double> statistics;

setInfo

方法原型:

publicvoidsetInfo(String name,Calendar date,VoteType type,int quantity);

只需要设置一下GeneralPollImpl中的rep就可以了。date需要防御性拷贝。

publicvoidsetInfo(String name,Calendar date,VoteType type,int quantity){if(quantity <=0){thrownewIllegalArgumentException("选出的人数应当为正!");}this.name = name;this.date =Calendar.getInstance();this.date.setTime(date.getTime());this.voteType = type;this.quantity = quantity;checkRep();}

addVoters

方法原型:

publicvoidaddVoters(Map<Voter,Double> voters);

这个方法没什么说的,防御性拷贝一下就好了。

publicvoidaddVoters(Map<Voter,Double> voters){this.voters =newHashMap<>(voters);checkRep();}

addCandidates

方法原型:

publicvoidaddCandidates(List<C> candidates);

这个方法的参数给的是一个List,有可能会有重复的candidate。我先用Set去了一下重:

publicvoidaddCandidates(List<C> candidates){//用HashSet去一下重this.candidates =newArrayList<>(newHashSet<>(candidates));checkRep();}

addVote

方法原型:

publicvoidaddVote(Vote<C> vote);

这个方法把一个Vote加入到votes(GeneralPollImpl的Rep)中。由于我们还新增了一个维护不合法投票的illegalVotes集合,在方法过程中还需要更新这个集合。我们看一下任务7(3.4.1)中的合法性检验:
在这里插入图片描述

考虑一下对于匿名的投票(实名投票在子类里再考虑),我们如何检查上面的前四点。
对于(1)和(2),我们可以简化成一个逻辑:首先判断该Vote的voteItem集合大小是否等于候选人个数(candidates.size())。若相等,再去检查是否出现了不在本次投票活动的候选人。如果没有出现,说明此票恰好覆盖了所有候选人。其实就是一个集合的简单推论(挺适合作为一道集合论考题):

    ∣
   
   
    S
   
   
    ∣
   
   
    =
   
   
    ∣
   
   
    T
   
   
    ∣
   
   
    且
   
   
     
   
   
    ∀
   
   
    s
   
   
    ∈
   
   
    S
   
   
    ,
   
   
    s
   
   
    ∈
   
   
    T
   
   
    ⇒
   
   
    S
   
   
    =
   
   
    T
   
   
    .
   
  
  
   |S|=|T|且\ \forall s \in S,s\in T \Rightarrow S=T.
  
 
∣S∣=∣T∣且 ∀s∈S,s∈T⇒S=T.

对于(3),在检查(1)(2)的过程中进行检验即可。
对于(4),我们可以维护一个appeared集合判断某个候选对象是否已经出现过。如果当前候选对象已经出现过,则返回false。
如果上面的不合法情况都不满足,最终返回true。把上面的语言翻译成代码,封装成一个isLegal方法:

/**
 * 对投票进行一般合法性检查,包括检查是否选择了所有候选人、投票选项的合法性、是否有多张票选了同一个人
 * @param vote 被检查的投票
 * @return 当投票合法,返回true;否则返回false
 */protectedbooleanisLegal(Vote<C> vote){Set<VoteItem<C>> items = vote.getVoteItems();// 投票各选项if(items.size()!= candidates.size()){returnfalse;// 投票的对象数或多或少}//接下来检查这些对象是否只出现过一次且选项合法Set<C> appeared =newHashSet<>();for(VoteItem<C> item: items){if(appeared.contains(item.getCandidate())){returnfalse;// 已经出现过了}if(!candidates.contains(item.getCandidate())){returnfalse;// 该对象不是候选对象}if(!voteType.checkLegality(item.getVoteValue())){returnfalse;// 不是合法选项}
        appeared.add(item.getCandidate());}returntrue;}

为了实现addVote方法,我们只需要简单地调用isLegal方法即可(至少在匿名投票里是这样)。

publicvoidaddVote(Vote<C> vote){//该投票不合法if(!isLegal(vote)) illegalVotes.add(vote);//加入投票列表中
    votes.add(vote);checkRep();}

statistics

方法原型:

publicvoidstatistics(StatisticsStrategy ss);

这个方法统计得分,将结果存在rep的statistics(Map)里。我们在这里只实现检查合法性的功能,把计分的功能委托给StatisticsStrategy(策略模式),稍后实现。合法性的判断规则如下:
在这里插入图片描述
这里有个问题:匿名投票对这两点的判断只能是对投票个数的判断,因为我们无法把投票区分开来。因此,我们提取出来一个检查合法性的方法checkVotes,只判断投票个数:

/**
 * 检查合法性,由于默认人是匿名的,无法防止也无法检测有人投多次票,只能检验投票数
 * @param votes 投票的集合
 * @return 当所有人都投票了,返回true;否则返回false
 */protectedbooleancheckVotes(Set<Vote<C>> votes){//对于匿名而言,直接判断投票的个数即可。if(votes.size()> voters.size())//投票人数比预计的多,此时应当抛出一个异常(因为无法区分是谁投的)thrownewVoteMoreThanVoterException("票数多于投票者数!");// 这是一个自定义的unchecked异常return votes.size()== voters.size();}

至于我们为什么不直接在statistics里写这个逻辑而要单独提出来一个checkVotes方法,是因为statistics的逻辑可以在父类和子类之间保持一致,我们只需在子类里重写这个checkVotes即可,而无需重写statistics:

publicvoidstatistics(StatisticsStrategy ss)throwsVoteNotEnoughException{if(!checkVotes(votes)){thrownewVoteNotEnoughException("有人还没有投票!");// 这是一个自定义checked异常,用于提醒客户还需要等待没投票的人投票。自定义异常的写法请自行搜索}//传入的参数:投票者(用于计算权值),票的集合
    statistics = ss.statistics(voteType, voters, votes, illegalVotes);checkRep();}

可以看到按这个逻辑,子类只需要修改checkVotes方法即可。为了防止迷惑,我们先给出statistics的接口和其继承结构:

/**
 * 根据给定参数计算本次投票的各候选者得分
 * @param voteType 投票类型
 * @param voters 记录投票者及其权值的映射(仅在实名时有意义)
 * @param votes 所有投票的集合
 * @param illegalVotes 不合法投票的集合
 * @return 根据参数(以及不同的策略子类)计算出来的,记录本次投票得分的映射
 */publicMap<C,Double>statistics(VoteType voteType,Map<Voter,Double> voters,Set<Vote<C>> votes,Set<Vote<C>> illegalVotes);

继承结构:
在这里插入图片描述

selection

方法原型:

publicvoidselection(SelectionStrategy ss);

这个方法根据上面得到的statistics计算本次投票的结果,保存在results(rep)中(Map<C, Double>,C为候选人,Double表示候选人的排名)。我们还是把整个计算过程交给SelectionStrategy。下面是SelectionStrategy的接口:

/**
 * @param statistics 投票得到的统计数据;(key, value)中key表示候选者,value表示得票(得分)
 * @param maximum 最多选出maximum个候选者。当maximum>statistics中元素个数时,返回结果为全选
 * @return 选择的结果
 */publicMap<C,Double>selection(Map<C,Double> statistics,int maximum);

selection的代码:

publicvoidselection(SelectionStrategy ss){
    results = ss.selection(statistics, quantity);checkRep();}

selection的具体实现我们留到后面。

result

方法原型:

publicStringresult();

该方法根据selection的结果返回一个表示投票结果的字符串。selection返回的结果是一个从候选人到其排名的映射。我们首先根据这个Map的value来排序(排名的数字越小越靠前):

Set<C> candidates =newTreeSet<>(newComparator<C>(){@Overridepublicintcompare(C o1,C o2){return(int)(results.get(o1)- results.get(o2));}});

candidates.addAll(results.keySet());

这样candidates集合中得到的候选人就是按从排名从高到低排序的了。然后我们把字符串格式化一下,把结果连接到一起即可。

int rank =0;StringBuilder stringBuilder =newStringBuilder(this.name +"的投票结果:\n排名\t候选者\n");for(C candidate: candidates){
    stringBuilder.append(String.format("%d\t%s\n",++rank, candidate.toString()));}

全部代码:

publicStringresult(){if(results.size()==0)return"本次投票未选出有效对象";Set<C> candidates =newTreeSet<>(newComparator<C>(){@Overridepublicintcompare(C o1,C o2){return(int)(results.get(o1)- results.get(o2));}});

    candidates.addAll(results.keySet());int rank =0;StringBuilder stringBuilder =newStringBuilder(this.name +"的投票结果:\n排名\t候选者\n");for(C candidate: candidates){
        stringBuilder.append(String.format("%d\t%s\n",++rank, candidate.toString()));}return stringBuilder.toString().trim();}

其它方法

除了上面需要的方法,为了任务11的visitor模式(或者为了测试方便)还需要加一些getter方法:
在这里插入图片描述
visitor的accept方法可以暂时忽略。
此外还有一个toString方法,我设置的格式是打印这个投票的全部信息,大概长这个样子:
在这里插入图片描述

三个具体应用场景

BusinessVoting

我们检查一下上面GeneralPollImpl的方法。有两个需要在BusinessVoting中重写:addVote和checkVotes(因为BusinessVoting是实名的)。下面分别说明。

addVote

由于这个方法的参数是一个Vote类,但在这个应用场景里我们要求每个投票都是实名的,因此我们需要判断一下这个Vote是否为一个RealNameVote。如果不是,需要抛出一个异常,告知用户应该传进来一个实名投票:

if(!(vote instanceofRealNameVote)){thrownewNotRealNameException("投票必须为实名!");}//NotRealNameException是一个unchecked异常

此外,由于是一个实名投票,我们还要判断这个投票人是否在voters(GeneralPollImpl的rep)中:

//该投票不合法:在实名时还包含确认该人是否在投票人列表中if(!isLegal(vote)||!voters.containsKey(((RealNameVote) vote).getVoter())){
    illegalVotes.add(vote);}//加入投票列表中
votes.add(vote);checkRep();

我们只是在GeneralPollImpl的addVote基础上新增了一个条件判断(!voters.containsKey( ((RealNameVote) vote).getVoter()),就完成了子类的合法性判断。注意这个向下转型的写法,需要填两层括号,否则getVoter会被认为是Vote的方法(但是实际并没有)。

checkVotes

我们需要还需要完成对于实名投票的以下判断(在调用statistics开始时):
在这里插入图片描述
首先检查是否所有人都投了票,逻辑见代码:

//分别表示已投票的投票人(保证其中一定都是voter)、投了多次票的投票人Set<Voter> votedVoters =newHashSet<>(), duplicateVoters =newHashSet<>();for(Vote<Proposal> vote: votes){Voter voter =((RealNameVote) vote).getVoter();if(!voters.containsKey(voter)){
        illegalVotes.add(vote);// 不是这次投票的投票人,记为非法continue;}if(votedVoters.contains(voter)){
        duplicateVoters.add(voter);// 该投票人投了多次票,记为非法}else{
        votedVoters.add(voter);// 是一个未投票的投票人,加入集合中}}

之后根据求出的votedVoters,判断是否所有人都投了票。如果集合不相等,返回false:

if(!votedVoters.equals(voters.keySet()))returnfalse;// 并非所有人都投票了

然后我们把所有投了多票的(duplicateVoters集合中的)投票人投的票都放进不合法的投票集合illegalVotes:

//下面把重复投票的都记为非法票for(Vote<Proposal> vote: votes){Voter voter =((RealNameVote) vote).getVoter();if(duplicateVoters.contains(voter))// 该投票人投了多次票
        illegalVotes.add(vote);}returntrue;// 标记完非法票后,返回true

DinnerOrder

这部分和BusinessVoting是一样的,因为都是实名制投票,唯一要修改的地方就是把Proposal换成Dish。

Election

这部分我们只需要重写addVote。原因在这里:
在这里插入图片描述
在应用2(即Election)中我们需要判断选票中的支持票是否超过k。这个很好判断,只需要统计一下传入的Vote中支持票个数即可。

publicvoidaddVote(Vote<Person> vote){if(!isLegal(vote)) illegalVotes.add(vote);if(supportCount(vote)>this.quantity) illegalVotes.add(vote);

    votes.add(vote);}

supportCount(vote)返回vote中包含的赞成票数量:

/**
 * 返回一个投票内的赞成票数量。
 * @param vote 待统计的投票
 * @return 票内包含的赞成票数
 */privateintsupportCount(Vote<Person> vote){int count =0;Set<VoteItem<Person>> voteItems = vote.getVoteItems();for(VoteItem<Person> item: voteItems)if(item.getVoteValue().equals(voteType.getSupport()))
            count++;return count;}

getSupport是VoteType类的getter方法,返回该选项类型的支持选项字符串(如“支持”/“赞成”)。

策略模式的具体实现

StatisticsStrategy

这一部分是计分策略。这里只给出DinnerStatisticsStrategy的逻辑(这是一个稍微麻烦一些的),其它两个可以仿照写出。

publicMap<Dish,Double>statistics(VoteType voteType,Map<Voter,Double> voters,Set<Vote<Dish>> votes,Set<Vote<Dish>> illegalVotes){Map<Dish,Double> ret =newHashMap<>();for(Vote<Dish> vote: votes){if(illegalVotes.contains(vote))continue;// 非法票Voter voter =((RealNameVote)vote).getVoter();// 向下转型,获取投票人double weight = voters.get(voter);// 该投票人对应权值//该投票人投的各票Set<VoteItem<Dish>> voteItems = vote.getVoteItems();for(VoteItem<Dish> voteItem: voteItems){Dish candidate = voteItem.getCandidate();//如果还没统计到,初始化值为0if(!ret.containsKey(candidate)) ret.put(candidate,0.0);double value = ret.get(candidate);

            ret.put(candidate, value + weight * voteType.getScoreByOption(voteItem.getVoteValue()));}}return ret;}

SelectionStrategy

这里也只给出一个Election的,其它的只会比这个简单。
首先按照得分高低对所有候选人排序(放到TreeSet里):

Map<Person,Double> results =newHashMap<>();Set<Person> set =newTreeSet<>(newComparator<Person>(){@Overridepublicintcompare(Person o1,Person o2){if(statistics.get(o1)> statistics.get(o2))return-1;elseif(statistics.get(o1)< statistics.get(o2))return1;return o1.getName().compareTo(o2.getName());}});
set.addAll(statistics.keySet());

对于Election这个需求,我们看一下排名第k的人和第k+1个人分是不是一样的。如果不一样,我们可以放心地取前k个人;否则,在第k名处出现了并列,我们从头开始找,把不等于第k个人得分的所有人放到结果里。(比如10 9 8 8 7 6里选3个人,第3和第4个人分数一样为8,我们就从10开始,把分数>8的都选出来,最终结果为[10, 9],两个得分为8的都被淘汰了)

double rank =0.0, score =Integer.MIN_VALUE;Iterator<Person> iterator = set.iterator();//看第k个和第k+1个是否相等;如果不相等(或仅有k个人)直接取前k个,否则设相等得分为s,不选前k个中与s相等的候选者。这次循环结束后score为第k个人的分数for(int i =0; i <Math.min(maximum, statistics.size()); i++){
    score = statistics.get(iterator.next());}//仅有k个人(或者少于k个),或第k个和第k+1个不相等if(!iterator.hasNext()|| statistics.get(iterator.next())!= score){
    iterator = set.iterator();for(int i =0; i <Math.min(maximum, 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;

有一个小细节:循环上界取的是min(maximum,statistics.size()),是为了保证maximum>statistics.size()时也能正常工作,此时为全选。

任务11

这个任务要求给ADT添加一个Visitor的接口,来统计合法选票的比例。Visitor模式的介绍及示例代码可以看这篇博客。为了使用Visitor模式,我们需要在GeneralPollImpl留足够的getter方法,使访问者能够访问到这些信息(已经在上面提过了);此外,还需要在GeneralPollImpl中留一个accept方法:

publicvoidaccept(Visitor visitor){
    visitor.visit(this);}

然后我们去写Visitor类。Visitor的一般继承结构如下:

这里我们直接对Poll这个ADT进行访问,即:

publicinterfaceVisitor<C>{//直接对投票进行访问,进行信息统计publicvoidvisit(GeneralPollImpl<C> poll);//获取统计信息publicdoublegetData();}

由于我们的ADT里有两个Set<Vote<C>>类型(votes/illegalVotes),我们如果在Visitor里声明一个访问Set<Vote<C>>的visit方法可能不好把对它们的访问区分开了(至少要加一个额外的参数指明)。所以我们选择直接给Visitor整个Poll对象,而不是让Poll来控制Visitor访问谁。
下面我们针对特定的需求编写特定的Visitor,这个也很简单,我们已经统计过了非法选票的集合:

publicclassVoteLegalVisitor<C>implementsVisitor<C>{privatedouble data;@Overridepublicvoidvisit(GeneralPollImpl<C> poll){
        data =1.0-1.0* poll.getIllegalVotes().size()/ poll.getVotes().size();}@OverridepublicdoublegetData(){returnthis.data;}}

调用Visitor方法(可以写在测试里):

//以下测试Visitor模式Visitor visitor =newVoteLegalVisitor();
poll.accept(visitor);double result = visitor.getData();// 统计结果

任务12

这部分主要是对正则表达式的一个练习。接受的格式就是形如

“喜欢”(2)|“不喜欢”(0)|“无所谓”(1)

“支持”|“反对”|“弃权”

两种。要求每个选项不能出现空白符,长度

    ≤
   
   
    5.
   
  
  
   \le 5.
  
 
≤5.我们可以先用String的split方法先将各个选项分割开:
publicVoteType(String regex){// split的参数是一个正则表达式,‘|’需要转义String[] inputOptions = regex.split("\\|");//...}

然后对于每个选项尝试用正则表达式去匹配,用捕获组来获取各个成分(如果不熟悉可以查一下java的捕获组)。我们用一个变量mode记录一下这个正则表达式是哪种上面情况(带数字的/不带数字,默认权值相等的)。mode=1为带数字的,mode=2为不带数字的。

//接上文//判断是哪种情况int mode =0;if(inputOptions.length <2){thrownewIllegalArgumentException("非法输入:选项少于两个");}else{for(String option: inputOptions){Pattern regexWithNum =Pattern.compile("\\\"(\\S+)\\\"\\(([\\+-]?\\d+)\\)");Pattern regexWithoutNum =Pattern.compile("\\\"(\\S+)\\\"");Matcher m1 = regexWithNum.matcher(option);// 带数字版本的MatcherMatcher m2 = regexWithoutNum.matcher(option);// 不带数字版本的Matcherif(m1.matches()){// 初始化一下modeif(mode ==0) mode =1;// 前后格式不一致了,前面是没数字的后面又有数字了if(mode !=1){thrownewIllegalArgumentException("非法输入:格式不一致");}if(m1.group(1).length()>=5)thrownewIllegalArgumentException("非法输入:选项名过长");
            options.put(m1.group(1),Integer.valueOf(m1.group(2)));}elseif(m2.matches()){if(mode ==0) mode =2;if(mode !=2){thrownewIllegalArgumentException("非法输入:格式不一致");}if(m2.group(1).length()>=5)thrownewIllegalArgumentException("非法输入:选项名过长");
            options.put(m2.group(1),1);// 默认所有选项的权值都为1}else{thrownewIllegalArgumentException("非法输入:正则表达式不匹配");}}}

上面的正则表达式因为转义符显得很乱,其实就是下面两个(不考虑转义和捕获组的括号):
“\S+”([+-]?\d+)
“\S+”
其中\S表示非空白符(没限定选项里不能有其它字符,只是说没有空白符)。

任务13

任务13就是把前面完成的任务放到三个app文件里试一下。我直接把JUnit的测试搬过来了,JUnit的测试也是拿的指导书上的例子。为了测一个程序需要写不少代码,也只是调用之前写的函数,没什么意思,我直接把测试用例放在这里。

BusinessVoting

这个测试对应下图。(这个股权给的总共加起来110%了,但是还是将错就错吧)结果应该是提案没有通过。
在这里插入图片描述

publicclassBusinessVotingApp{publicstaticvoidmain(String[] args){GeneralPollImpl<Proposal> poll =newBusinessVoting<>();Map<Voter,Double> voters =newHashMap<>();Voter v1 =newVoter("董事A");Voter v2 =newVoter("董事B");Voter v3 =newVoter("董事C");Voter v4 =newVoter("董事D");Voter v5 =newVoter("董事E");
        voters.put(v1,0.05);
        voters.put(v2,0.51);
        voters.put(v3,0.10);
        voters.put(v4,0.24);
        voters.put(v5,0.20);//对于BusinessVoting类型,默认只有一个表决项目
        poll.setInfo("HIT会议",Calendar.getInstance(),newVoteType(),1);
        poll.addVoters(voters);List<Proposal> proposalList =newArrayList<>();Proposal p0 =newProposal("给宿舍装空调",Calendar.getInstance());
        proposalList.add(p0);
        poll.addCandidates(proposalList);Set<VoteItem<Proposal>> supportItem =newHashSet<>(), rejectItem =newHashSet<>(), abstainItem =newHashSet<>();
        supportItem.add(newVoteItem<>(p0,"支持"));
        rejectItem.add(newVoteItem<>(p0,"反对"));
        abstainItem.add(newVoteItem<>(p0,"弃权"));Vote<Proposal> voteA =newRealNameVote<>(v1, rejectItem);Vote<Proposal> voteB =newRealNameVote<>(v2, supportItem);Vote<Proposal> voteC =newRealNameVote<>(v3, supportItem);Vote<Proposal> voteD =newRealNameVote<>(v4, rejectItem);Vote<Proposal> voteE =newRealNameVote<>(v5, abstainItem);

        poll.addVote(voteA);
        poll.addVote(voteB);
        poll.addVote(voteC);
        poll.addVote(voteD);
        poll.addVote(voteE);try{
            poll.statistics(newBusinessStatisticsStrategy());}catch(Exception e){System.out.println(e.getMessage());}

        poll.selection(newBusinessSelectionStrategy());System.out.println(poll.result());}}

Election

这部分框架里给了示例,没有用指导书上的。最终选出的结果应该是ABC和GHI。

//originpublicclassElectionApp{publicstaticvoidmain(String[] args){// 创建2个投票人Voter vr1 =newVoter("v1");Voter vr2 =newVoter("v2");// 设定2个投票人的权重Map<Voter,Double> weightedVoters =newHashMap<>();
        weightedVoters.put(vr1,1.0);
        weightedVoters.put(vr2,1.0);// 设定投票类型Map<String,Integer> types =newHashMap<>();
        types.put("支持",1);
        types.put("反对",-1);
        types.put("弃权",0);VoteType vt =newVoteType(types);// 创建候选对象:候选人Person p1 =newPerson("ABC",19);Person p2 =newPerson("DEF",20);Person p3 =newPerson("GHI",21);// 创建投票项,前三个是投票人vr1对三个候选对象的投票项,后三个是vr2的投票项VoteItem<Person> vi11 =newVoteItem<>(p1,"支持");VoteItem<Person> vi12 =newVoteItem<>(p2,"反对");VoteItem<Person> vi13 =newVoteItem<>(p3,"支持");Set<VoteItem<Person>> vote1 =newHashSet<>(), vote2 =newHashSet<>();
        vote1.add(vi11);vote1.add(vi12);vote1.add(vi13);VoteItem<Person> vi21 =newVoteItem<>(p1,"反对");VoteItem<Person> vi22 =newVoteItem<>(p2,"弃权");VoteItem<Person> vi23 =newVoteItem<>(p3,"弃权");//结果://p1-1票//p2-0票//p3-1票

        vote2.add(vi21);vote2.add(vi22);vote2.add(vi23);// 创建2个投票人vr1、vr2的选票Vote<Person> rv1 =newVote<Person>(vote1);Vote<Person> rv2 =newVote<Person>(vote2);// 创建投票活动Poll<Person> poll =Poll.create();// 设定投票基本信息:名称、日期、投票类型、选出的数量
        poll.setInfo("Vote",Calendar.getInstance(), vt,2);// 增加投票人及其权重
        poll.addVoters(weightedVoters);//增加候选人List<Person> candidates =newArrayList<>();
        candidates.add(p1);candidates.add(p2);candidates.add(p3);
        poll.addCandidates(candidates);// 增加三个投票人的选票
        poll.addVote(rv1);
        poll.addVote(rv2);// 按规则计票try{
            poll.statistics(newElectionStatisticsStrategy());}catch(Exception e){System.out.println(e.getMessage());
            e.printStackTrace();}// 按规则遴选
        poll.selection(newElectionSelectionStrategy());// 输出遴选结果System.out.println(poll.result());}}

DinnerOrder

这个用的是指导书上的:
在这里插入图片描述
选出的菜应该是ABCD。

publicclassDinnerOrderApp{publicstaticvoidmain(String[] args){GeneralPollImpl<Dish> poll =newDinnerOrder<>();Map<Voter,Double> voters =newHashMap<>();Voter v1 =newVoter("爷爷");Voter v2 =newVoter("爸爸");Voter v3 =newVoter("妈妈");Voter v4 =newVoter("儿子");
        voters.put(v1,4.0);
        voters.put(v2,1.0);
        voters.put(v3,2.0);
        voters.put(v4,2.0);

        poll.setInfo("家庭聚会",Calendar.getInstance(),newVoteType("\"喜欢\"(2)|\"不喜欢\"(0)|\"无所谓\"(1)"),4);
        poll.addVoters(voters);List<Dish> dishList =newArrayList<>();DishA=newDish("A",1);DishB=newDish("B",2);DishC=newDish("C",3);DishD=newDish("D",4);DishE=newDish("E",5);DishF=newDish("F",6);

        dishList.add(A);
        dishList.add(B);
        dishList.add(C);
        dishList.add(D);
        dishList.add(E);
        dishList.add(F);
        poll.addCandidates(dishList);Set<VoteItem<Dish>> item1 =newHashSet<>(), item2 =newHashSet<>(), item3 =newHashSet<>(), item4 =newHashSet<>();
        item1.add(newVoteItem<>(A,"喜欢"));item1.add(newVoteItem<>(B,"喜欢"));
        item1.add(newVoteItem<>(C,"无所谓"));item1.add(newVoteItem<>(D,"无所谓"));
        item1.add(newVoteItem<>(E,"不喜欢"));item1.add(newVoteItem<>(F,"不喜欢"));

        item2.add(newVoteItem<>(A,"无所谓"));item2.add(newVoteItem<>(B,"喜欢"));
        item2.add(newVoteItem<>(C,"喜欢"));item2.add(newVoteItem<>(D,"喜欢"));
        item2.add(newVoteItem<>(E,"不喜欢"));item2.add(newVoteItem<>(F,"喜欢"));

        item3.add(newVoteItem<>(A,"喜欢"));item3.add(newVoteItem<>(B,"不喜欢"));
        item3.add(newVoteItem<>(C,"不喜欢"));item3.add(newVoteItem<>(D,"不喜欢"));
        item3.add(newVoteItem<>(E,"喜欢"));item3.add(newVoteItem<>(F,"不喜欢"));

        item4.add(newVoteItem<>(A,"喜欢"));item4.add(newVoteItem<>(B,"无所谓"));
        item4.add(newVoteItem<>(C,"喜欢"));item4.add(newVoteItem<>(D,"喜欢"));
        item4.add(newVoteItem<>(E,"喜欢"));item4.add(newVoteItem<>(F,"不喜欢"));Vote<Dish> vote1 =newRealNameVote<>(v1, item1);Vote<Dish> vote2 =newRealNameVote<>(v2, item2);Vote<Dish> vote3 =newRealNameVote<>(v3, item3);Vote<Dish> vote4 =newRealNameVote<>(v4, item4);

        poll.addVote(vote1);
        poll.addVote(vote2);
        poll.addVote(vote3);
        poll.addVote(vote4);try{
            poll.statistics(newDinnerStatisticsStrategy());}catch(Exception e){System.out.println(e.getMessage());}

        poll.selection(newDinnerSelectionStrategy());System.out.println(poll.result());}}

任务14

这个任务应该把前面的部分仔细检查过了再做。之后需要在另一个分支上补充其它内容,如果发现了前面的bug改起来会比较麻烦。
创建分支change:

git checkout -b change

我们有三个子任务:
在这里插入图片描述
我们先看1和3.这两个都比较好改:

商业表决的修改

这个应该不需要修改(至少我这个程序没有改),因为这属于三种投票的共性。只要没把BusinessVoting的rep设计成只存一个提案,应该都可以直接用。这里给出一个测试单元(注意跟指导书上的不一样,改成v2和v4投支持票了,因此两个提案都能通过):

@TestpublicvoidtestMultiResult(){GeneralPollImpl<Proposal> poll =newBusinessVoting<>();Map<Voter,Double> voters =newHashMap<>();Voter v1 =newVoter("董事A");Voter v2 =newVoter("董事B");Voter v3 =newVoter("董事C");Voter v4 =newVoter("董事D");Voter v5 =newVoter("董事E");
    voters.put(v1,0.05);
    voters.put(v2,0.51);
    voters.put(v3,0.10);
    voters.put(v4,0.24);
    voters.put(v5,0.20);//对于BusinessVoting类型,默认只有一个表决项目
    poll.setInfo("HIT会议",Calendar.getInstance(),newVoteType(),1);
    poll.addVoters(voters);List<Proposal> proposalList =newArrayList<>();Proposal p0 =newProposal("给宿舍装空调",Calendar.getInstance());Proposal p1 =newProposal("增加科研经费",Calendar.getInstance());

    proposalList.add(p0);
    proposalList.add(p1);

    poll.addCandidates(proposalList);Set<VoteItem<Proposal>> supportItem =newHashSet<>(), rejectItem =newHashSet<>(), abstainItem =newHashSet<>();//为了减少代码,每个人对于两个提案的态度相同
    supportItem.add(newVoteItem<>(p0,"支持"));
    supportItem.add(newVoteItem<>(p1,"支持"));

    rejectItem.add(newVoteItem<>(p0,"反对"));
    rejectItem.add(newVoteItem<>(p1,"反对"));

    abstainItem.add(newVoteItem<>(p0,"弃权"));
    abstainItem.add(newVoteItem<>(p1,"弃权"));Vote<Proposal> voteA =newRealNameVote<>(v1, rejectItem);Vote<Proposal> voteB =newRealNameVote<>(v2, supportItem);Vote<Proposal> voteC =newRealNameVote<>(v3, rejectItem);Vote<Proposal> voteD =newRealNameVote<>(v4, supportItem);Vote<Proposal> voteE =newRealNameVote<>(v5, abstainItem);

    poll.addVote(voteA);
    poll.addVote(voteB);
    poll.addVote(voteC);
    poll.addVote(voteD);
    poll.addVote(voteE);Set<Vote<Proposal>> votes =newHashSet<>();
    votes.add(voteA);
    votes.add(voteB);
    votes.add(voteC);
    votes.add(voteD);
    votes.add(voteE);assertEquals(poll.getVotes(), votes);try{
        poll.statistics(newBusinessStatisticsStrategy());}catch(Exception e){System.out.println(e.getMessage());fail();// 不应出现异常}

    poll.selection(newBusinessSelectionStrategy());//'增'的UTF-16为0x589E,'给'的是0x7ED9,按字典序排序所以'增'在前面assertEquals("HIT会议的投票结果:\n排名\t候选者\n1\t增加科研经费\n2\t给宿舍装空调", poll.result());}

聚餐点菜的修改

得益于策略模式,修改这个也很简单。只需要创建一个新的计分策略NewDinnerStatisticsStrategy,把计分部分修改一下就可以了。用的时候拿这个策略替换原来的DinnerStatisticsStrategy。下面只给出部分代码(因为大部分都是一样的)

//...if(!ret.containsKey(candidate)) ret.put(candidate,0.0);double value = ret.get(candidate);if(voteItem.getVoteValue().equals(voteType.getSupport()))// 只计算喜欢的票数
    ret.put(candidate, value +1.0);//每次只加一//...

代表选举的修改

这个就不太好改了。这个要求我们在支持票相同时,比较反对票,反对票少的胜出。问题的关键在于,反对票我们在statistics方法里没有统计。如果我们想要获取这个信息,要么改动接口里的statistics方法和所有Poll子类的statistics方法,要么就得重新统计一遍反对票的信息。显然前一种修改的代价太大了,这里我采用的是重新统计的方法。我们可以看出这要求遴选策略的直接改动,我们新建一个NewElectionSelectionStrategy,实现SelectionStrategy,但是它的构造方法需求额外的三个参数,在构造方法里我们计算一下反对票的信息(和statistics那个Map类似):

//这是这个Strategy的repMap<Person,Double> rejectStatistics =newHashMap<>();publicNewElectionSelectionStrategy(VoteType voteType,Set<Vote<Person>> votes,Set<Vote<Person>> illegalVotes){//...}

用户要类似于下面这样调用新的策略:

poll.selection(newNewElectionSelectionStrategy(
        poll.getVoteType(), 
        poll.getVotes(),
        poll.getIllegalVotes()));

统计的具体代码就不给出了,和StatisticsStrategy的实现很类似,只不过只统计反对票的个数。
接下来还要重写这个策略的selection方法,首先重写一下TreeSet的排序规则:

Set<Person> set =newTreeSet<>(newComparator<Person>(){@Overridepublicintcompare(Person o1,Person o2){if(statistics.get(o1)> statistics.get(o2))return-1;elseif(statistics.get(o1)< statistics.get(o2))return1;//下面再比较反对票的数量if(rejectStatistics.get(o1)> rejectStatistics.get(o2))return-1;elseif(rejectStatistics.get(o1)< rejectStatistics.get(o2))return1;return o1.getName().compareTo(o2.getName());}});

接下来的流程和ElectionSelectionStrategy基本一样了。不同点就是我们需要两个变量分别记录第k个人的赞成票和反对票数量(原先只记录了赞成票),用这两个变量和第k+1个人比较。代码重复比较多,不再给出,如果有问题可以讨论一下。

其他一些问题

实验报告的最后要求我们看一下项目的Object Graph,下面说一下其中一种做法。

在这里插入图片描述
在任何一个目录下打开Git的GUI,并打开项目。右上角的Repository->Visualize All Branch History:
在这里插入图片描述
在左上角就可以看到分支图了。
在这里插入图片描述
本文成文比较匆忙,如有问题、疏漏欢迎反馈。


本文转载自: https://blog.csdn.net/wyn1564464568/article/details/125401368
版权归原作者 Castria 所有, 如有侵权,请联系我们删除。

“哈工大2022软件构造Lab3”的评论:

还没有评论