Lesson 2
Managing a Voting System using Java
Introduction

Welcome back to another exciting session where we learn about enhancing existing functionality without causing regressions. Our scenario today involves designing a voting system. We'll start with the basic implementation of the voting system and gradually introduce additional elements of complexity.

Starter Task Review

In our initial task, we created a simple voting system in Java with a set of basic functionalities:

  • boolean registerCandidate(String candidateId):: This method is used for adding new candidates to our system.
  • boolean vote(long timestamp, String voterId, String candidateId):: This method facilitates users casting their votes. Each vote is given a timestamp.
  • Integer getVotes(String candidateId):: This method retrieves the total number of votes for a given candidate.
  • List<String> topNCandidates(int n):: We also want to add a leaderboard functionality to our system. This method returns the top n candidates sorted by the number of votes.
Initial Solution Development

Let's jump into the Java code and begin the implementation of our starter task. Here, we use Java's built-in HashMap and ArrayList as the core of our design. These collections allow us to have dynamic lists keyed based on candidate IDs and voter IDs, which will greatly simplify our design.

Java
1import java.util.*; 2 3public class VotingSystem { 4 private Map<String, Integer> candidates; // Stores candidate_id as key and votes as value 5 private Map<String, VotingHistory> voters; // Tracks each voter's voting history 6 7 public VotingSystem() { 8 this.candidates = new HashMap<>(); // Initialize candidates map 9 this.voters = new HashMap<>(); // Initialize voters map 10 } 11 12 public boolean registerCandidate(String candidateId) { 13 if (candidates.containsKey(candidateId)) { 14 return false; // Candidate is already registered 15 } 16 candidates.put(candidateId, 0); // Initialize candidates with 0 votes 17 return true; 18 } 19 20 public boolean vote(long timestamp, String voterId, String candidateId) { 21 if (!candidates.containsKey(candidateId)) { 22 return false; // Return false if candidate is not registered 23 } 24 VotingHistory voterHistory = voters.computeIfAbsent(voterId, k -> new VotingHistory()); 25 voterHistory.getVotes().add(candidateId); // Record the vote 26 voterHistory.getTimestamps().add(timestamp); // Record the time of the vote 27 candidates.put(candidateId, candidates.get(candidateId) + 1); // Increment vote count for the candidate 28 return true; 29 } 30 31 public Integer getVotes(String candidateId) { 32 return candidates.get(candidateId); // Retrieve vote count for a candidate or null if not found 33 } 34 35 public List<String> topNCandidates(int n) { 36 return candidates.entrySet().stream() 37 .sorted((entry1, entry2) -> entry2.getValue().compareTo(entry1.getValue())) 38 .limit(n) 39 .map(Map.Entry::getKey) 40 .toList(); // Return top n candidates based on votes 41 } 42 43 private static class VotingHistory { 44 private final List<String> votes; 45 private final List<Long> timestamps; 46 47 public VotingHistory() { 48 this.votes = new ArrayList<>(); 49 this.timestamps = new ArrayList<>(); 50 } 51 52 public List<String> getVotes() { 53 return votes; 54 } 55 56 public List<Long> getTimestamps() { 57 return timestamps; 58 } 59 } 60}
Introduce New Methods

Now that we have a basic voting system, our goal is to enhance this system with additional functionalities:

  • Map<String, Integer> getVotingHistory(String voterId):: Provides a detailed voting history for a specified voter, returning a map with candidates and the number of votes they received from the voter. Returns null if the voter ID does not exist.
  • boolean blockVoterRegistration(long timestamp):: Implements a mechanism to halt any new voter registrations past a specified timestamp, effectively freezing the voter list as of that moment.
  • boolean changeVote(long timestamp, String voterId, String oldCandidateId, String newCandidateId):: Enables voters to change their vote from one candidate to another, given the change is made within a 24-hour window from their last vote, ensuring both the old and new candidates are registered, and that the voter initially voted for the old candidate.
Implementing New Methods: 'getVotingHistory' and 'blockVoterRegistration'

We proceed to enhance our existing VotingSystem class to accommodate the new functionalities.

First, let's incorporate the methods to get the voting history and to block further voter registrations:

Java
1 private Long blockTime = null; // Time after which registrations are blocked 2 3 public Map<String, Integer> getVotingHistory(String voterId) { 4 if (!voters.containsKey(voterId)) { 5 return null; 6 } 7 VotingHistory voterHistory = voters.get(voterId); 8 Map<String, Integer> votingHistory = new HashMap<>(); 9 for (String candidateId : voterHistory.getVotes()) { 10 votingHistory.put(candidateId, votingHistory.getOrDefault(candidateId, 0) + 1); 11 } 12 return votingHistory; // Returns a map with each candidate voted for and the number of times they were voted for 13 } 14 15 public boolean blockVoterRegistration(long timestamp) { 16 this.blockTime = timestamp; // Records the timestamp after which no registration is allowed 17 return true; 18 }
Updating the 'vote' Method

With the introduction of the blockVoterRegistration functionality, we must revisit our vote method to ensure it respects the new rules set by this feature. Specifically, we need to ensure that no votes are cast after the voter registration has been blocked. This is critical in maintaining the integrity of the voting system, especially in scenarios where registration deadlines are enforced. Here's how we modify the vote method to incorporate this change:

Java
1 public boolean vote(long timestamp, String voterId, String candidateId) { 2 // Check if blockTime is set and if the vote attempt is after the block timestamp 3 if (blockTime != null && timestamp >= blockTime) { 4 return false; // Vote attempt is blocked due to the registration freeze 5 } 6 7 if (!candidates.containsKey(candidateId)) { 8 return false; // Return false if the candidate is not registered 9 } 10 11 VotingHistory voterHistory = voters.computeIfAbsent(voterId, k -> new VotingHistory()); 12 voterHistory.getVotes().add(candidateId); // Record the vote 13 voterHistory.getTimestamps().add(timestamp); // Record the time of the vote 14 candidates.put(candidateId, candidates.get(candidateId) + 1); // Increment vote count for the candidate 15 16 return true; 17 }

This update ensures that our voting system behaves as expected, even with the new functionality to block further voter registrations beyond a certain timestamp. It's a perfect demonstration of how new features can necessitate revisits and revisions to existing code to enhance functionality while ensuring backward compatibility.

Implementing 'changeVote' Method

The changeVote method allows voters to change their vote, adhering to specific rules. Here's a step-by-step explanation of implementing this functionality:

  • Verify Candidate and Voter Validity: Check if both the old and new candidate IDs exist in the system, and verify that the voter has previously voted for the old candidate.

  • Timestamp Constraints: Ensure that the voter is trying to change their vote within an allowable timeframe after their initial vote.

  • Update Votes: If all conditions are met, subtract one vote from the old candidate, add one vote to the new candidate, and update the voter's voting record.

Java
1 public boolean changeVote(long timestamp, String voterId, String oldCandidateId, String newCandidateId) { 2 // Verify existence in the voting system 3 if (!candidates.containsKey(oldCandidateId) || !candidates.containsKey(newCandidateId)) { 4 return false; 5 } 6 7 // Check if the voter_id is valid and has previously voted 8 if (!voters.containsKey(voterId) || voters.get(voterId).getVotes().isEmpty()) { 9 return false; // Voter is either not registered or has not voted yet 10 } 11 12 VotingHistory voterHistory = voters.get(voterId); 13 // Confirm voter has voted for the old candidate 14 if (!voterHistory.getVotes().contains(oldCandidateId)) { 15 return false; 16 } 17 18 // Ensure the operation is within the permitted timeframe 19 int lastVoteIndex = voterHistory.getVotes().lastIndexOf(oldCandidateId); 20 if (timestamp - voterHistory.getTimestamps().get(lastVoteIndex) > 86400) { // 24 hours in seconds 21 return false; 22 } 23 24 // Perform the vote change 25 // Remove the old vote 26 voterHistory.getVotes().remove(lastVoteIndex); 27 voterHistory.getTimestamps().remove(lastVoteIndex); 28 29 // Append the new vote 30 voterHistory.getVotes().add(newCandidateId); 31 voterHistory.getTimestamps().add(timestamp); 32 33 // Update candidates' vote counts 34 candidates.put(oldCandidateId, candidates.get(oldCandidateId) - 1); 35 candidates.put(newCandidateId, candidates.get(newCandidateId) + 1); 36 37 return true; 38 }
Summary

Congratulations! You've successfully enhanced the voting system by adding functionalities to view voting history, block new candidate registrations, and, most importantly, enable vote changes under specific conditions. Each of these features was developed with careful consideration to maintain the integrity and backward compatibility of the system. Continue exploring with practice sessions and further experimentation to refine your skills in developing complex, functionality-rich applications. Happy coding!

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.