Lesson 2

Enhancing Functionality in a Voting System While Ensuring Backward Compatibility


Welcome back to a fascinating session where we will 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 create a simple voting system in Python with a set of basic functionalities:

  • register_candidate(self, candidate_id: str) -> bool:: This method is used for adding new candidates to our system.
  • vote(self, timestamp: int, voter_id: str, candidate_id: str) -> bool:: This method is designed to facilitate users casting their votes. Each vote is given a timestamp.
  • get_votes(self, candidate_id: str) -> int | None:: This method retrieves the total number of votes for a given candidate.
  • top_n_candidates(self, n: int) -> list[str]:: 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 Python code and begin the implementation of our starter task. Here, we use Python's built-in defaultdict as the core of our design. This dictionary-like object allows us to have dynamic lists keyed based on candidate IDs and voter IDs, which will greatly simplify our design.

1from collections import defaultdict 2 3class VotingSystem: 4 def __init__(self): 5 self.candidates = {} # Stores candidate_id as key and votes as value 6 self.voters = defaultdict(lambda: {'votes': [], 'timestamps': []}) # Tracks each voter's voting history 7 8 def register_candidate(self, candidate_id: str) -> bool: 9 if candidate_id in self.candidates: 10 return False # Candidate is already registered 11 self.candidates[candidate_id] = 0 # Initialize candidates with 0 votes 12 return True 13 14 def vote(self, timestamp: int, voter_id: str, candidate_id: str) -> bool: 15 if candidate_id not in self.candidates: 16 return False # Returns False if candidate is not registered 17 self.voters[voter_id]['votes'].append(candidate_id) # Record the vote 18 self.voters[voter_id]['timestamps'].append(timestamp) # Record the time of the vote 19 self.candidates[candidate_id] += 1 # Increment vote count for the candidate 20 return True 21 22 def get_votes(self, candidate_id: str) -> int | None: 23 return self.candidates.get(candidate_id) # Retrieve vote count for a candidate or None if not found 24 25 def top_n_candidates(self, n: int) -> list[str]: 26 return sorted(self.candidates, key=self.candidates.get, reverse=True)[:n] # Return top n candidates based on votes
Introduce New Methods

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

  • get_voting_history(self, voter_id: str) -> dict | None:: Provides a detailed voting history for a specified voter, returning a dictionary mapping each candidate to the number of times voted for by the voter in question. Returns None if the voter ID does not exist.
  • block_voter_registration(self, timestamp: int) -> bool:: Implements a mechanism to halt any new voter registrations past a specified timestamp, effectively freezing the voter list as of that moment.
  • change_vote(self, timestamp: int, voter_id: str, old_candidate_id: str, new_candidate_id: str) -> bool:: 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

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:

1 def get_voting_history(self, voter_id: str) -> dict | None: 2 if voter_id not in self.voters: 3 return None 4 voting_history = {candidate_id: self.voters[voter_id]['votes'].count(candidate_id) for candidate_id in set(self.voters[voter_id]['votes'])} 5 return voting_history # Returns a dictionary with each candidate voted for and the number of times they were voted for 6 7 def block_voter_registration(self, timestamp: int) -> bool: 8 self.block_time = timestamp # Records the timestamp after which no registration is allowed 9 return True
Updating the 'vote' Method

With the introduction of the block_voter_registration 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:

1def vote(self, timestamp: int, voter_id: str, candidate_id: str) -> bool: 2 # Check if block_time is set and if the vote attempt is after the block timestamp 3 if hasattr(self, 'block_time') and timestamp >= self.block_time: 4 return False # Vote attempt is blocked due to the registration freeze 5 6 if candidate_id not in self.candidates: 7 return False # Returns False if the candidate is not registered 8 9 self.voters[voter_id]['votes'].append(candidate_id) # Record the vote 10 self.voters[voter_id]['timestamps'].append(timestamp) # Record the time of the vote 11 self.candidates[candidate_id] += 1 # Increment vote count for the candidate 12 13 return True

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 'change_vote' Method

The change_vote 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.

1def change_vote(self, timestamp: int, voter_id: str, old_candidate_id: str, new_candidate_id: str) -> bool: 2 # Verify existence in the voting system 3 if any(cid not in self.candidates for cid in [old_candidate_id, new_candidate_id]): 4 return False 5 6 # Check if the voter_id is valid and has previously voted 7 if voter_id not in self.voters or not self.voters[voter_id]['votes']: 8 return False # Voter is either not registered or has not voted yet 9 10 # Confirm voter has voted for the old candidate 11 if old_candidate_id not in self.voters[voter_id]['votes']: 12 return False 13 14 # Ensure the operation is within the permitted timeframe 15 last_vote_index = len(self.voters[voter_id]['votes']) - 1 - self.voters[voter_id]['votes'][::-1].index(old_candidate_id) 16 if timestamp - self.voters[voter_id]['timestamps'][last_vote_index] > 86400: # 24 hours in seconds 17 return False 18 19 # Perform the vote change 20 # Remove the old vote 21 del self.voters[voter_id]['votes'][last_vote_index] 22 del self.voters[voter_id]['timestamps'][last_vote_index] 23 24 # Append the new vote 25 self.voters[voter_id]['votes'].append(new_candidate_id) 26 self.voters[voter_id]['timestamps'].append(timestamp) 27 28 # Update candidates' vote counts 29 self.candidates[old_candidate_id] -= 1 30 self.candidates[new_candidate_id] += 1 31 32 return True

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.