此博客内容为哈工大2022春季学期软件构造Lab3:Reusability and Maintainability oriented Software Construction,文章为个人记录,不保证正确性,仅供练习和思路参考,请勿抄袭。实验所需文件可以从这里获取(若打不开可以复制到浏览器)。
实验环境:IntelliJ IDEA 2022.1(Ultimate Edition)
注:由于博客主要为思路提示,部分实验要求没有提及,请以实验指导书的要求为准;博客与指导书相比可能顺序略有调整;由于本文是完成整个任务后所写,一些后面补充的rep/方法可能会出现在前面。框架里的JUnit是JUnit5可能会报错,可以按照Lab2改成JUnit4.
这部分要求完成VoteType的测试用例以及实现代码等。这个类里只有一个从String到Integer的Map,(key,value)表示值为key的选项得分为value。我们需要补充的有构造方法以及两个辅助方法、对hashCode和equals的重写。这部分都不难实现。由于后面判断合法性时需要统计支持票的数量,任务14还需要统计反对票的数量,又因为任务12让我们支持正则表达式的输入,我们没法确定支持票/反对票一定是哪个选项,所以我在这还额外记录了一下哪个选项代表支持(比如用户把2分的“赞成”作为支持票,而不是指导书里给的1分的“支持”),哪个选项代表反对(这部分只在任务14的change branch中出现)。
public class VoteType {
// key为选项名、value为选项名对应的分数
private Map 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(Maporigin) { 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;
}
public VoteItem(C candidate, String value) {
this.candidate = candidate;
this.value = value;
assert checkRep();
if(!checkRep()) {
throw new IllegalArgumentException("非法输入:选项不合法");
}
}
测试单元(也是原先框架里有的):
@Test
public void test() {
VoteItem voteItem1 = new VoteItem<>("Alice", "支持");
VoteItem voteItem2 = new VoteItem<>("Bob", "支持");
VoteItem 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> voteItems = new HashSet<>(); // 投票时间 private Calendar date = Calendar.getInstance(); //... //构造方法 public Vote(Set > voteItems) { this.id = ++Vote.num; this.voteItems.addAll(voteItems); assert checkRep(); }
至于RI,我认为这层没什么好检查的。VoteItem已经保证了自己的合法性,candidate需要与特定的投票关联到一起才会产生“合法”的意义,因此我就让checkRep始终返回true了。
测试单元:
@Test
public void test() {
Set> voteItems = new HashSet<>();
voteItems.add(new VoteItem("Alice", "支持"));
voteItems.add(new VoteItem("Bob", "支持"));
voteItems.add(new VoteItem("Cathy", "反对"));
Vote vote = new Vote(voteItems);
assertTrue(vote.candidateIncluded("Alice"));
assertTrue(vote.candidateIncluded("Bob"));
assertFalse(vote.candidateIncluded("Tracy"));
assertEquals(voteItems, vote.getVoteItems());
}
任务10
因为实名投票就是在匿名投票上的扩展,就把这部分提到前面了。对于指导书中的三个场景,商业决议和晚餐点菜都是实名的,而刚才的Vote是匿名的。因此我们需要扩展Vote类型,使其能额外携带一个投票人Voter的信息。指导书里提示可以用继承/装饰器模式实现,这里我为了简便直接使用继承实现了:
public class RealNameVoteGeneralPollImplextends Vote { //投票人 private Voter voter; public RealNameVote(Voter voter, Set > voteItems) { super(voteItems); this.voter = voter; } public Voter getVoter() { return this.voter; } }
任务4-任务9即这次实验的主体部分。我做这个实验的顺序是前面的Immutable ADT → rightarrow →GeneralPollImpl → rightarrow →三种子类型 → rightarrow →在此基础上完成任务11-14.本博客也按照从一般(Poll)到特殊(三个具体场景)安排。JUnit的部分代码会放在后面。
Poll接口里面有一个静态create方法需要补充。只需要返回任意一个子类型(虽然这时候还没实现)即可。我们返回Election类型。
public staticGeneralPollImplPoll create() { return (Poll ) new Election(); }
我们把三个子类中共性的地方留到GeneralPollImpl中实现,在具体的子类里再特殊调整(Override)一下。如果有需要也可以把这个类设成抽象的,部分方法留到子类中再实现。
Rep的补充与修改由于我们在计票的时候需要判定哪些票是不合法的(而不能直接删除它们!),我们需要拿一个集合保存一下不合法的票。
//增加的rep:为了标记选票的合法性,用Set记录不合法的选票 protected Set> illegalVotes = new HashSet<>();
此外,以下的rep被我修改成了protected(而非原来的private),以便在子类中进行查询。
// 投票人集合,key为投票人,value为其在本次投票中所占权重 protected MapsetInfovoters; // 拟选出的候选对象最大数量(赋了一个大的初值) protected int quantity = Integer.MAX_VALUE; // 所有选票集合 protected Set > votes = new HashSet<>(); // 计票结果,key为候选对象,value为其得分 protected Map statistics;
方法原型:
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(Mapvoters);
这个方法没什么说的,防御性拷贝一下就好了。
public void addVoters(MapaddCandidatesvoters) { this.voters = new HashMap<>(voters); checkRep(); }
方法原型:
public void addCandidates(Listcandidates);
这个方法的参数给的是一个List,有可能会有重复的candidate。我先用Set去了一下重:
public void addCandidates(ListaddVotecandidates) { //用HashSet去一下重 this.candidates = new ArrayList<>(new HashSet<>(candidates)); checkRep(); }
方法原型:
public void addVote(Votevote);
这个方法把一个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,sin T Rightarrow S=T. ∣S∣=∣T∣且 ∀s∈S,s∈T⇒S=T.
对于(3),在检查(1)(2)的过程中进行检验即可。
对于(4),我们可以维护一个appeared集合判断某个候选对象是否已经出现过。如果当前候选对象已经出现过,则返回false。
如果上面的不合法情况都不满足,最终返回true。把上面的语言翻译成代码,封装成一个isLegal方法:
protected boolean isLegal(Votevote) { Set > items = vote.getVoteItems();// 投票各选项 if(items.size() != candidates.size()) { return false;// 投票的对象数或多或少 } //接下来检查这些对象是否只出现过一次且选项合法 Set appeared = new HashSet<>(); for(VoteItem 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(Votestatisticsvote) { //该投票不合法 if(!isLegal(vote)) illegalVotes.add(vote); //加入投票列表中 votes.add(vote); checkRep(); }
方法原型:
public void statistics(StatisticsStrategy ss);
这个方法统计得分,将结果存在rep的statistics(Map)里。我们在这里只实现检查合法性的功能,把计分的功能委托给StatisticsStrategy(策略模式),稍后实现。合法性的判断规则如下:
这里有个问题:匿名投票对这两点的判断只能是对投票个数的判断,因为我们无法把投票区分开来。因此,我们提取出来一个检查合法性的方法checkVotes,只判断投票个数:
protected boolean checkVotes(Set> 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的接口和其继承结构:
public Mapstatistics(VoteType voteType, Map voters, Set > votes, Set > illegalVotes);
继承结构:
方法原型:
public void selection(SelectionStrategy ss);
这个方法根据上面得到的statistics计算本次投票的结果,保存在results(rep)中(Map
public Mapselection(Map statistics, int maximum);
selection的代码:
public void selection(SelectionStrategy ss) {
results = ss.selection(statistics, quantity);
checkRep();
}
selection的具体实现我们留到后面。
result方法原型:
public String result();
该方法根据selection的结果返回一个表示投票结果的字符串。selection返回的结果是一个从候选人到其排名的映射。我们首先根据这个Map的value来排序(排名的数字越小越靠前):
Setcandidates = new TreeSet<>(new Comparator () { @Override public 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("%dt%sn",++rank, candidate.toString()));
}
全部代码:
public String result() {
if(results.size() == 0) return "本次投票未选出有效对象";
Set candidates = new TreeSet<>(new Comparator() {
@Override
public 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("%dt%sn",++rank, candidate.toString()));
}
return stringBuilder.toString().trim();
}
其它方法
除了上面需要的方法,为了任务11的visitor模式(或者为了测试方便)还需要加一些getter方法:
visitor的accept方法可以暂时忽略。
此外还有一个toString方法,我设置的格式是打印这个投票的全部信息,大概长这个样子:
我们检查一下上面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)、投了多次票的投票人 SetvotedVoters = new HashSet<>(), duplicateVoters = new HashSet<>(); for(Vote 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(VoteDinnerOrdervote: votes) { Voter voter = ((RealNameVote) vote).getVoter(); if(duplicateVoters.contains(voter)) // 该投票人投了多次票 illegalVotes.add(vote); } return true;// 标记完非法票后,返回true
这部分和BusinessVoting是一样的,因为都是实名制投票,唯一要修改的地方就是把Proposal换成Dish。
Election这部分我们只需要重写addVote。原因在这里:
在应用2(即Election)中我们需要判断选票中的支持票是否超过k。这个很好判断,只需要统计一下传入的Vote中支持票个数即可。
public void addVote(Votevote) { if(!isLegal(vote)) illegalVotes.add(vote); if(supportCount(vote) > this.quantity) illegalVotes.add(vote); votes.add(vote); }
supportCount(vote)返回vote中包含的赞成票数量:
private int supportCount(Votevote) { int count = 0; Set > voteItems = vote.getVoteItems(); for(VoteItem item: voteItems) if(item.getVoteValue().equals(voteType.getSupport())) count++; return count; }
getSupport是VoteType类的getter方法,返回该选项类型的支持选项字符串(如“支持”/“赞成”)。
策略模式的具体实现 StatisticsStrategy这一部分是计分策略。这里只给出DinnerStatisticsStrategy的逻辑(这是一个稍微麻烦一些的),其它两个可以仿照写出。
public MapSelectionStrategystatistics(VoteType voteType, Map voters, Set > votes, Set > illegalVotes) { Map ret = new HashMap<>(); for(Vote vote: votes) { if(illegalVotes.contains(vote)) continue;// 非法票 Voter voter = ((RealNameVote)vote).getVoter();// 向下转型,获取投票人 double weight = voters.get(voter);// 该投票人对应权值 //该投票人投的各票 Set > voteItems = vote.getVoteItems(); for(VoteItem voteItem: voteItems) { Dish candidate = voteItem.getCandidate(); //如果还没统计到,初始化值为0 if(!ret.containsKey(candidate)) ret.put(candidate, 0.0); double value = ret.get(candidate); ret.put(candidate, value + weight * voteType.getScoreByOption(voteItem.getVoteValue())); } } return ret; }
这里也只给出一个Election的,其它的只会比这个简单。
首先按照得分高低对所有候选人排序(放到TreeSet里):
Mapresults = new HashMap<>(); 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+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; Iteratoriterator = 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{ //直接对投票进行访问,进行信息统计 public void visit(GeneralPollImpl poll); //获取统计信息 public double getData(); }
由于我们的ADT里有两个Set
下面我们针对特定的需求编写特定的Visitor,这个也很简单,我们已经统计过了非法选票的集合:
public class VoteLegalVisitorimplements Visitor { private double data; @Override public void visit(GeneralPollImpl poll) { data = 1.0 - 1.0 * poll.getIllegalVotes().size() / poll.getVotes().size(); } @Override public 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);// 带数字版本的Matcher
Matcher m2 = regexWithoutNum.matcher(option);// 不带数字版本的Matcher
if(m1.matches()) {
// 初始化一下mode
if(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就是把前面完成的任务放到三个app文件里试一下。我直接把JUnit的测试搬过来了,JUnit的测试也是拿的指导书上的例子。为了测一个程序需要写不少代码,也只是调用之前写的函数,没什么意思,我直接把测试用例放在这里。
BusinessVoting这个测试对应下图。(这个股权给的总共加起来110%了,但是还是将错就错吧)结果应该是提案没有通过。
public class BusinessVotingApp {
public static void main(String[] args) {
GeneralPollImpl poll = new BusinessVoting<>();
Map 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 proposalList = new ArrayList<>();
Proposal p0 = new Proposal("给宿舍装空调", Calendar.getInstance());
proposalList.add(p0);
poll.addCandidates(proposalList);
Set> 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 voteA = new RealNameVote<>(v1, rejectItem);
Vote voteB = new RealNameVote<>(v2, supportItem);
Vote voteC = new RealNameVote<>(v3, supportItem);
Vote voteD = new RealNameVote<>(v4, rejectItem);
Vote 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 weightedVoters = new HashMap<>();
weightedVoters.put(vr1, 1.0);
weightedVoters.put(vr2, 1.0);
// 设定投票类型
Map 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 vi11 = new VoteItem<>(p1, "支持");
VoteItem vi12 = new VoteItem<>(p2, "反对");
VoteItem vi13 = new VoteItem<>(p3, "支持");
Set> vote1 = new HashSet<>(), vote2 = new HashSet<>();
vote1.add(vi11);vote1.add(vi12);vote1.add(vi13);
VoteItem vi21 = new VoteItem<>(p1, "反对");
VoteItem vi22 = new VoteItem<>(p2, "弃权");
VoteItem vi23 = new VoteItem<>(p3, "弃权");
//结果:
//p1-1票
//p2-0票
//p3-1票
vote2.add(vi21);vote2.add(vi22);vote2.add(vi23);
// 创建2个投票人vr1、vr2的选票
Vote rv1 = new Vote(vote1);
Vote rv2 = new Vote(vote2);
// 创建投票活动
Poll poll = Poll.create();
// 设定投票基本信息:名称、日期、投票类型、选出的数量
poll.setInfo("Vote", Calendar.getInstance(), vt, 2);
// 增加投票人及其权重
poll.addVoters(weightedVoters);
//增加候选人
List 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 poll = new DinnerOrder<>();
Map 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 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> 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 vote1 = new RealNameVote<>(v1, item1);
Vote vote2 = new RealNameVote<>(v2, item2);
Vote vote3 = new RealNameVote<>(v3, item3);
Vote 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 poll = new BusinessVoting<>();
Map 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 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> 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 voteA = new RealNameVote<>(v1, rejectItem);
Vote voteB = new RealNameVote<>(v2, supportItem);
Vote voteC = new RealNameVote<>(v3, rejectItem);
Vote voteD = new RealNameVote<>(v4, supportItem);
Vote voteE = new RealNameVote<>(v5, abstainItem);
poll.addVote(voteA);
poll.addVote(voteB);
poll.addVote(voteC);
poll.addVote(voteD);
poll.addVote(voteE);
Set> 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候选者n1t增加科研经费n2t给宿舍装空调", 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 MaprejectStatistics = new HashMap<>(); public NewElectionSelectionStrategy( VoteType voteType, Set > votes, Set > illegalVotes) { //... }
用户要类似于下面这样调用新的策略:
poll.selection( new NewElectionSelectionStrategy( poll.getVoteType(), poll.getVotes(), poll.getIllegalVotes() ) );
统计的具体代码就不给出了,和StatisticsStrategy的实现很类似,只不过只统计反对票的个数。
接下来还要重写这个策略的selection方法,首先重写一下TreeSet的排序规则:
Setset = 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; //下面再比较反对票的数量 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:
在左上角就可以看到分支图了。
本文成文比较匆忙,如有问题、疏漏欢迎反馈。



