说明
此博客内容为哈工大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就可以了,这里不再给出。
测试单元,这个是框架里自带的:
@Testpublic 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("非法输入:选项不合法");}}
测试单元(也是原先框架里有的):
@Testpublic 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是一样的,会导致它们被判定为同一个Voteprivate 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了。
测试单元:
@Testpublic 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(); //如果还没统计到,初始化值为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;}
SelectionStrategy
这里也只给出一个Election的,其它的只会比这个简单。
首先按照得分高低对所有候选人排序(放到TreeSet里):
Map<Person, Double> results = new HashMap<>();Set<Person> set = new TreeSet<>(new Comparator<Person>() { @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;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; @Override public void visit(GeneralPollImpl<C> 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);// 带数字版本的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。
//originpublic 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投支持票了,因此两个提案都能通过):
@Testpublic 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的repMap<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>() { @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:
在左上角就可以看到分支图了。
本文成文比较匆忙,如有问题、疏漏欢迎反馈。