说明

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

任务1

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

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

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

public VoteType() {options.put("支持", 1);//支持options.put("反对", -1);//反对options.put("弃权", 0);//弃权support = "支持";assert checkRep();
}

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

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

我的RI是:选项应该至少有2个,并且长度≤5\le 5≤5,不能出现空白符(任务12正则表达式中的要求)。

private boolean checkRep() {for(String str: options.keySet()) {if(str.length() > 5 || !str.matches("\\S+")) return false;}return options.size() >= 2;
}

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

@Test
public void test() {VoteType voteType = new VoteType();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这些简单的方法就不给出了。

private boolean checkRep() {return value.matches("\\S+") && value.length() <= 5;
}/*** 创建一个投票项对象 例如:针对候选对象“张三”,投票选项是“支持”** @param candidate 所针对的候选对象* @param value     所给出的投票选项*/
public VoteItem(C candidate, String value) {this.candidate = candidate;this.value = value;assert checkRep();if(!checkRep()) {throw new IllegalArgumentException("非法输入:选项不合法");}
}

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

@Test
public void test() {VoteItem<String> voteItem1 = new VoteItem<>("Alice", "支持");VoteItem<String> voteItem2 = new VoteItem<>("Bob", "支持");VoteItem<String> voteItem3 = new VoteItem<>("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计数
static private int num = 0;
//用于标记投票的序号,引入这个变量是因为两个几乎同时创建的不同Vote的Date是一样的,会导致它们被判定为同一个Vote
private int id;
// 一个投票人对所有候选对象的投票项集合
private Set<VoteItem<C>> voteItems = new HashSet<>();
// 投票时间
private Calendar date = Calendar.getInstance();
//...//构造方法
public Vote(Set<VoteItem<C>> voteItems) {this.id = ++Vote.num;this.voteItems.addAll(voteItems);assert checkRep();
}

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

@Test
public void test() {Set<VoteItem<String>> voteItems = new HashSet<>();voteItems.add(new VoteItem<String>("Alice", "支持"));voteItems.add(new VoteItem<String>("Bob", "支持"));voteItems.add(new VoteItem<String>("Cathy", "反对"));Vote<String> vote = new Vote<String>(voteItems);assertTrue(vote.candidateIncluded("Alice"));assertTrue(vote.candidateIncluded("Bob"));assertFalse(vote.candidateIncluded("Tracy"));assertEquals(voteItems, vote.getVoteItems());
}

任务10

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

public class RealNameVote<C> extends Vote<C>{//投票人private Voter voter;public RealNameVote(Voter voter, Set<VoteItem<C>> voteItems) {super(voteItems);this.voter = voter;}public Voter getVoter() {return this.voter;}
}

GeneralPollImpl

任务4-任务9即这次实验的主体部分。我做这个实验的顺序是前面的Immutable ADT→\rightarrow→GeneralPollImpl→\rightarrow→三种子类型→\rightarrow→在此基础上完成任务11-14.本博客也按照从一般(Poll)到特殊(三个具体场景)安排。JUnit的部分代码会放在后面。

Poll接口

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

public static <C> Poll<C> create() {return (Poll<C>) new Election();
}

GeneralPollImpl

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

Rep的补充与修改

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

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

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

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

setInfo

方法原型:

public void setInfo(String name, Calendar date, VoteType type, int quantity);

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

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

addVoters

方法原型:

public void addVoters(Map<Voter, Double> voters);

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

public void addVoters(Map<Voter, Double> voters) {this.voters = new HashMap<>(voters);checkRep();
}

addCandidates

方法原型:

public void addCandidates(List<C> candidates);

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

public void addCandidates(List<C> candidates) {//用HashSet去一下重this.candidates = new ArrayList<>(new HashSet<>(candidates));checkRep();
}

addVote

方法原型:

public void addVote(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*/
protected boolean isLegal(Vote<C> vote) {Set<VoteItem<C>> items = vote.getVoteItems();// 投票各选项if(items.size() != candidates.size()) {return false;// 投票的对象数或多或少}//接下来检查这些对象是否只出现过一次且选项合法Set<C> appeared = new HashSet<>();for(VoteItem<C> item: items) {if(appeared.contains(item.getCandidate())) {return false;// 已经出现过了}if(!candidates.contains(item.getCandidate())) {return false;// 该对象不是候选对象}if(!voteType.checkLegality(item.getVoteValue())) {return false;// 不是合法选项}appeared.add(item.getCandidate());}return true;
}

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

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

statistics

方法原型:

public void statistics(StatisticsStrategy ss);

这个方法统计得分,将结果存在rep的statistics(Map)里。我们在这里只实现检查合法性的功能,把计分的功能委托给StatisticsStrategy(策略模式),稍后实现。合法性的判断规则如下:

这里有个问题:匿名投票对这两点的判断只能是对投票个数的判断,因为我们无法把投票区分开来。因此,我们提取出来一个检查合法性的方法checkVotes,只判断投票个数:

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

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

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

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

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

继承结构:

selection

方法原型:

public void selection(SelectionStrategy ss);

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

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

selection的代码:

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

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

result

方法原型:

public String result();

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

Set<C> candidates = new TreeSet<>(new Comparator<C>() {@Overridepublic int compare(C o1, C o2) {return (int)(results.get(o1) - results.get(o2));}
});candidates.addAll(results.keySet());

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

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

全部代码:

public String result() {if(results.size() == 0) return "本次投票未选出有效对象";Set<C> candidates = new TreeSet<>(new Comparator<C>() {@Overridepublic int compare(C o1, C o2) {return (int)(results.get(o1) - results.get(o2));}});candidates.addAll(results.keySet());int rank = 0;StringBuilder stringBuilder = new StringBuilder(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 instanceof RealNameVote)) {throw new NotRealNameException("投票必须为实名!");
}
//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 = new HashSet<>(), duplicateVoters = new HashSet<>();
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())) return false; // 并非所有人都投票了

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

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

DinnerOrder

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

Election

这部分我们只需要重写addVote。原因在这里:

在应用2(即Election)中我们需要判断选票中的支持票是否超过k。这个很好判断,只需要统计一下传入的Vote中支持票个数即可。

public void addVote(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 票内包含的赞成票数*/
private int supportCount(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的逻辑(这是一个稍微麻烦一些的),其它两个可以仿照写出。

public Map<Dish, Double> statistics(VoteType voteType, Map<Voter, Double> voters, Set<Vote<Dish>> votes, Set<Vote<Dish>> illegalVotes) {Map<Dish, Double> ret = new HashMap<>();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 = new HashMap<>();Set<Person> set = new TreeSet<>(new Comparator<Person>() {@Overridepublic 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+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方法:

public void accept(Visitor visitor) {visitor.visit(this);
}

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

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

public interface Visitor<C> {//直接对投票进行访问,进行信息统计public void visit(GeneralPollImpl<C> poll);//获取统计信息public double getData();
}

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

public class VoteLegalVisitor<C> implements Visitor<C> {private double data;@Overridepublic void visit(GeneralPollImpl<C> poll) {data =  1.0 - 1.0 * poll.getIllegalVotes().size() / poll.getVotes().size();}@Overridepublic double getData() {return this.data;}
}

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

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

任务12

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

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

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

两种。要求每个选项不能出现空白符,长度≤5.\le 5.≤5.我们可以先用String的split方法先将各个选项分割开:

public VoteType(String regex) {// split的参数是一个正则表达式,‘|’需要转义String[] inputOptions = regex.split("\\|");//...
}

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

//接上文
//判断是哪种情况
int mode = 0;
if(inputOptions.length < 2) {throw new IllegalArgumentException("非法输入:选项少于两个");
}
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) {throw new IllegalArgumentException("非法输入:格式不一致");}if(m1.group(1).length() >= 5)throw new IllegalArgumentException("非法输入:选项名过长");options.put(m1.group(1), Integer.valueOf(m1.group(2)));}else if(m2.matches()) {if(mode == 0) mode = 2;if(mode != 2) {throw new IllegalArgumentException("非法输入:格式不一致");}if(m2.group(1).length() >= 5)throw new IllegalArgumentException("非法输入:选项名过长");options.put(m2.group(1), 1);// 默认所有选项的权值都为1}else {throw new IllegalArgumentException("非法输入:正则表达式不匹配");}}
}

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

任务13

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

BusinessVoting

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

public class BusinessVotingApp {public static void main(String[] args) {GeneralPollImpl<Proposal> poll = new BusinessVoting<>();Map<Voter, Double> voters = new HashMap<>();Voter v1 = new Voter("董事A");Voter v2 = new Voter("董事B");Voter v3 = new Voter("董事C");Voter v4 = new Voter("董事D");Voter v5 = new Voter("董事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(), new VoteType(), 1);poll.addVoters(voters);List<Proposal> proposalList = new ArrayList<>();Proposal p0 = new Proposal("给宿舍装空调", Calendar.getInstance());proposalList.add(p0);poll.addCandidates(proposalList);Set<VoteItem<Proposal>> supportItem = new HashSet<>(), rejectItem = new HashSet<>(), abstainItem = new HashSet<>();supportItem.add(new VoteItem<>(p0, "支持"));rejectItem.add(new VoteItem<>(p0, "反对"));abstainItem.add(new VoteItem<>(p0, "弃权"));Vote<Proposal> voteA = new RealNameVote<>(v1, rejectItem);Vote<Proposal> voteB = new RealNameVote<>(v2, supportItem);Vote<Proposal> voteC = new RealNameVote<>(v3, supportItem);Vote<Proposal> voteD = new RealNameVote<>(v4, rejectItem);Vote<Proposal> voteE = new RealNameVote<>(v5, abstainItem);poll.addVote(voteA);poll.addVote(voteB);poll.addVote(voteC);poll.addVote(voteD);poll.addVote(voteE);try {poll.statistics(new BusinessStatisticsStrategy());} catch(Exception e) {System.out.println(e.getMessage());}poll.selection(new BusinessSelectionStrategy());System.out.println(poll.result());}
}

Election

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


//origin
public class ElectionApp {public static void main(String[] args) {// 创建2个投票人Voter vr1 = new Voter("v1");Voter vr2 = new Voter("v2");// 设定2个投票人的权重Map<Voter, Double> weightedVoters = new HashMap<>();weightedVoters.put(vr1, 1.0);weightedVoters.put(vr2, 1.0);// 设定投票类型Map<String, Integer> types = new HashMap<>();types.put("支持", 1);types.put("反对", -1);types.put("弃权", 0);VoteType vt = new VoteType(types);// 创建候选对象:候选人Person p1 = new Person("ABC", 19);Person p2 = new Person("DEF", 20);Person p3 = new Person("GHI", 21);// 创建投票项,前三个是投票人vr1对三个候选对象的投票项,后三个是vr2的投票项VoteItem<Person> vi11 = new VoteItem<>(p1, "支持");VoteItem<Person> vi12 = new VoteItem<>(p2, "反对");VoteItem<Person> vi13 = new VoteItem<>(p3, "支持");Set<VoteItem<Person>> vote1 = new HashSet<>(), vote2 = new HashSet<>();vote1.add(vi11);vote1.add(vi12);vote1.add(vi13);VoteItem<Person> vi21 = new VoteItem<>(p1, "反对");VoteItem<Person> vi22 = new VoteItem<>(p2, "弃权");VoteItem<Person> vi23 = new VoteItem<>(p3, "弃权");//结果://p1-1票//p2-0票//p3-1票vote2.add(vi21);vote2.add(vi22);vote2.add(vi23);// 创建2个投票人vr1、vr2的选票Vote<Person> rv1 = new Vote<Person>(vote1);Vote<Person> rv2 = new Vote<Person>(vote2);// 创建投票活动Poll<Person> poll = Poll.create();// 设定投票基本信息:名称、日期、投票类型、选出的数量poll.setInfo("Vote", Calendar.getInstance(), vt, 2);// 增加投票人及其权重poll.addVoters(weightedVoters);//增加候选人List<Person> candidates = new ArrayList<>();candidates.add(p1);candidates.add(p2);candidates.add(p3);poll.addCandidates(candidates);// 增加三个投票人的选票poll.addVote(rv1);poll.addVote(rv2);// 按规则计票try {poll.statistics(new ElectionStatisticsStrategy());} catch(Exception e) {System.out.println(e.getMessage());e.printStackTrace();}// 按规则遴选poll.selection(new ElectionSelectionStrategy());// 输出遴选结果System.out.println(poll.result());}}

DinnerOrder

这个用的是指导书上的:

选出的菜应该是ABCD。

public class DinnerOrderApp {public static void main(String[] args) {GeneralPollImpl<Dish> poll = new DinnerOrder<>();Map<Voter, Double> voters = new HashMap<>();Voter v1 = new Voter("爷爷");Voter v2 = new Voter("爸爸");Voter v3 = new Voter("妈妈");Voter v4 = new Voter("儿子");voters.put(v1, 4.0);voters.put(v2, 1.0);voters.put(v3, 2.0);voters.put(v4, 2.0);poll.setInfo("家庭聚会", Calendar.getInstance(), new VoteType("\"喜欢\"(2)|\"不喜欢\"(0)|\"无所谓\"(1)"), 4);poll.addVoters(voters);List<Dish> dishList = new ArrayList<>();Dish A = new Dish("A", 1);Dish B = new Dish("B", 2);Dish C = new Dish("C", 3);Dish D = new Dish("D", 4);Dish E = new Dish("E", 5);Dish F = new Dish("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 = new HashSet<>(), item2 = new HashSet<>(), item3 = new HashSet<>(), item4 = new HashSet<>();item1.add(new VoteItem<>(A, "喜欢"));item1.add(new VoteItem<>(B, "喜欢"));item1.add(new VoteItem<>(C, "无所谓"));item1.add(new VoteItem<>(D, "无所谓"));item1.add(new VoteItem<>(E, "不喜欢"));item1.add(new VoteItem<>(F, "不喜欢"));item2.add(new VoteItem<>(A, "无所谓"));item2.add(new VoteItem<>(B, "喜欢"));item2.add(new VoteItem<>(C, "喜欢"));item2.add(new VoteItem<>(D, "喜欢"));item2.add(new VoteItem<>(E, "不喜欢"));item2.add(new VoteItem<>(F, "喜欢"));item3.add(new VoteItem<>(A, "喜欢"));item3.add(new VoteItem<>(B, "不喜欢"));item3.add(new VoteItem<>(C, "不喜欢"));item3.add(new VoteItem<>(D, "不喜欢"));item3.add(new VoteItem<>(E, "喜欢"));item3.add(new VoteItem<>(F, "不喜欢"));item4.add(new VoteItem<>(A, "喜欢"));item4.add(new VoteItem<>(B, "无所谓"));item4.add(new VoteItem<>(C, "喜欢"));item4.add(new VoteItem<>(D, "喜欢"));item4.add(new VoteItem<>(E, "喜欢"));item4.add(new VoteItem<>(F, "不喜欢"));Vote<Dish> vote1 = new RealNameVote<>(v1, item1);Vote<Dish> vote2 = new RealNameVote<>(v2, item2);Vote<Dish> vote3 = new RealNameVote<>(v3, item3);Vote<Dish> vote4 = new RealNameVote<>(v4, item4);poll.addVote(vote1);poll.addVote(vote2);poll.addVote(vote3);poll.addVote(vote4);try {poll.statistics(new DinnerStatisticsStrategy());} catch (Exception e) {System.out.println(e.getMessage());}poll.selection(new DinnerSelectionStrategy());System.out.println(poll.result());}
}

任务14

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

git checkout -b change

我们有三个子任务:

我们先看1和3.这两个都比较好改:

商业表决的修改

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

@Test
public void testMultiResult() {GeneralPollImpl<Proposal> poll = new BusinessVoting<>();Map<Voter, Double> voters = new HashMap<>();Voter v1 = new Voter("董事A");Voter v2 = new Voter("董事B");Voter v3 = new Voter("董事C");Voter v4 = new Voter("董事D");Voter v5 = new Voter("董事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(), new VoteType(), 1);poll.addVoters(voters);List<Proposal> proposalList = new ArrayList<>();Proposal p0 = new Proposal("给宿舍装空调", Calendar.getInstance());Proposal p1 = new Proposal("增加科研经费", Calendar.getInstance());proposalList.add(p0);proposalList.add(p1);poll.addCandidates(proposalList);Set<VoteItem<Proposal>> supportItem = new HashSet<>(), rejectItem = new HashSet<>(), abstainItem = new HashSet<>();//为了减少代码,每个人对于两个提案的态度相同supportItem.add(new VoteItem<>(p0, "支持"));supportItem.add(new VoteItem<>(p1, "支持"));rejectItem.add(new VoteItem<>(p0, "反对"));rejectItem.add(new VoteItem<>(p1, "反对"));abstainItem.add(new VoteItem<>(p0, "弃权"));abstainItem.add(new VoteItem<>(p1, "弃权"));Vote<Proposal> voteA = new RealNameVote<>(v1, rejectItem);Vote<Proposal> voteB = new RealNameVote<>(v2, supportItem);Vote<Proposal> voteC = new RealNameVote<>(v3, rejectItem);Vote<Proposal> voteD = new RealNameVote<>(v4, supportItem);Vote<Proposal> voteE = new RealNameVote<>(v5, abstainItem);poll.addVote(voteA);poll.addVote(voteB);poll.addVote(voteC);poll.addVote(voteD);poll.addVote(voteE);Set<Vote<Proposal>> votes = new HashSet<>();votes.add(voteA);votes.add(voteB);votes.add(voteC);votes.add(voteD);votes.add(voteE);assertEquals(poll.getVotes(), votes);try {poll.statistics(new BusinessStatisticsStrategy());} catch(Exception e) {System.out.println(e.getMessage());fail(); // 不应出现异常}poll.selection(new BusinessSelectionStrategy());//'增'的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的rep
Map<Person, Double> rejectStatistics = new HashMap<>();public NewElectionSelectionStrategy(VoteType voteType, Set<Vote<Person>> votes, Set<Vote<Person>> illegalVotes) {//...
}

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

poll.selection(new NewElectionSelectionStrategy(poll.getVoteType(), poll.getVotes(),poll.getIllegalVotes())
);

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

Set<Person> set = new TreeSet<>(new Comparator<Person>() {@Overridepublic 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;//下面再比较反对票的数量if(rejectStatistics.get(o1) > rejectStatistics.get(o2)) return -1;else if(rejectStatistics.get(o1) < rejectStatistics.get(o2)) return 1;return o1.getName().compareTo(o2.getName());}
});

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

其他一些问题

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


在任何一个目录下打开Git的GUI,并打开项目。右上角的Repository->Visualize All Branch History:

在左上角就可以看到分支图了。

本文成文比较匆忙,如有问题、疏漏欢迎反馈。

哈工大2022软件构造Lab3相关推荐

  1. 软件构造 Lab3 CircularOrbit 实验日记

    软件构造 Lab3 CircularOrbit 实验日记 SC Lab3 CircularOrbit 实验日记(持更)week6-week10 日期 时间段 计划任务 实际完成情况 2019-04-0 ...

  2. 哈工大2021软件构造lab1总结

    哈工大2021软件构造lab1总结 作为软件构造的第一次实验,感觉内容本身不是很难,里面功能的实现用上学期在数据结构和算法分析两门课里学到的知识就可以解决(尽管其实已经忘没了).这次实验主要目的还是准 ...

  3. 哈工大2021软件构造实验3心得(1)-进行GUI设计

    哈工大2021软件构造实验3心得(1)-进行GUI设计 最近,笔者完成下窝工的软构实验三.在完成过程中,有很多坑想要记录一下. 顺便方便一下后来的窝工学子. 此Blog主要讨论如何在Eclipse里面 ...

  4. 2021哈工大软件构造Lab3

    2021年春季学期 计算学部<软件构造>课程 Lab 3实验报告 实验源码:https://github.com/1190200817/SC_Lab3 目录 1 实验目标概述··· 1 2 ...

  5. 哈工大软件构造lab3

    2020年春季学期 计算机学院<软件构造>课程 Lab 3实验报告 1 实验目标概述 1 2 实验环境配置 1 3 实验过程 1 3.1 待开发的三个应用场景 1 3.2 面向可复用性和可 ...

  6. [HITSC]哈工大2020春软件构造Lab3实验报告

    Github地址 1 实验目标概述 本次实验覆盖课程第 3.4.5 章的内容,目标是编写具有可复用性和可维护 性的软件,主要使用以下软件构造技术: 子类型.泛型.多态.重写.重载 继承.代理.组合 常 ...

  7. HIT 软件构造 lab3实验报告

    2020年春季学期 计算机学院<软件构造>课程 Lab 3实验报告 姓名 赵旭东 学号 1180300223 班号 1803002 电子邮件 1264887178@qq.com 手机号码 ...

  8. 哈工大2020软件构造Lab2 Problem3 Playing Chess 架构设计思路

    哈工大2020春软件构造实验2 Problem 3 Playing Chess 架构设计思路 问题简述 整体结构 ADT功能设计 功能实现路径 问题简述: 设计一款棋类游戏,同时支持国际象棋(Ches ...

  9. 2022 - 软件构造复习

    软件生命周期 一个软件产品或软件系统经历孕育.诞生.成长.成熟.衰亡等阶段,一般称为软件生存周期(软件生命周期). 根据软件所处的状态和特征,划分软件生存周期. 需求定义.软件设计.软件实现.软件维护 ...

最新文章

  1. 2022-2028年中国废旧塑料回收产业研究及前瞻分析报告
  2. python语言依赖平台吗_在大型项目上,Python 是个烂语言吗?
  3. matlab mesh与surf比较
  4. nvcc gcc g++混合编译器编程
  5. mysql5.1修改登陆密码_mysql 5.1版本修改密码及远程登录mysql数据库的方法
  6. recvfrom 无法接收 icmp 差错数据包_利用ICMP隧道技术实现C2通信
  7. 轻松带你学习java-agent
  8. Java:Swing篇,实现JList、JTextArea的自动滚动,实时刷新功能
  9. iQOO Neo 855竞速版来了:今年最后一款骁龙855 Plus手机
  10. 韩梦飞沙Android应用集合 想法
  11. 【Henu ACM Round#19 A】 Vasya the Hipster
  12. [Icehouse][cinder] volume状态为 error_deleting无法删除 的解决方案
  13. API之实用工具Postman 使用方法
  14. oracle最简单的分页sql语句,oracle中分页sql语句
  15. Kademlia、DHT、KRPC、BitTorrent 协议、DHT Sniffer
  16. React antd的table表格之嵌套表格
  17. kodi 自动升级_如何设置您的Kodi库以自动更新
  18. 模式识别--绪论 什么是模式识别?模式识别的主要方法及具体应用
  19. linux ubuntu木马,Ubuntu病毒查杀 ClamAV 简介以及适用范围
  20. 通过几道CTF题学习yii2框架

热门文章

  1. kali linux的初学者之路(笔记)
  2. java xstream_XStream 用法汇总
  3. 专业测试我也能够做 教您如何自己测试PC性能
  4. ibm服务器 显示器不亮,IBM液晶显示器黑屏故障原因、检测分析及维修方法
  5. 如何确定windows弹出的广告窗口是哪个程序导致并找出来
  6. ChatGPT专业应用:采访大纲自动生成
  7. vue声明周期_Vue生命周期的理解
  8. 19、商品微服务-srv层实现
  9. fl studio多少钱?2023年有必要买正版fl studio吗 ?
  10. 羞羞电量插件v1.0安卓版