哈工大2022软件构造Lab3

说明

此博客内容为哈工大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 sS,sTS=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>() {
    
    
	@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("%d\t%s\n",++rank, candidate.toString()));
}

全部代码:

public String result() {
    
    
	if(results.size() == 0) return "本次投票未选出有效对象";

	Set<C> candidates = new TreeSet<>(new Comparator<C>() {
    
    
		@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("%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);// 带数字版本的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

任务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>() {
    
    
    @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:
在这里插入图片描述
在左上角就可以看到分支图了。
在这里插入图片描述
本文成文比较匆忙,如有问题、疏漏欢迎反馈。

猜你喜欢

转载自blog.csdn.net/wyn1564464568/article/details/125401368