This is my solution to the following challenge:
Given the recent buzz around the New York City mayoral election and its use of ranked-choice voting (a basic explanation of which can be found here), we have created a project to simulate the ballot counting and show how a majority winner would not necessarily win a ranked-choice election. The task is to implement SimpleElection.CountVotes() and RankedChoiceElection.CountVotes() in such a way that their respective ElectionRunners will return the correct winner of the election based on the voting system (you may also want to take a look at ElectionRunner.cs). Changes can be made to anything in the solution, including the BallotGenerators, which currently just generate 100,000 random votes. The submission should include a short explanation of your solution and any unit tests you used along the way.
The codebase provided is here.
I used Test-Driven Development in every line of code added, and all commits follow this convention with some personal variations. In the beginning, I pushed first the test and then the feature in order to emphasize TDD usage, but then I pushed both the test and feature together for simplicity. The code was written using DDD, since I captured the ubiquitous language in this election's bounded context.
- I created a new repo with the codebase and xUnit as testing framework.
- Constructors must return complete and valid instances of objects. In order to have complete and valid instances of
SimpleElection
I added the preconditions "must have ballots", "must have candidates", "people can't vote more than once" and "voted candidates must be valid" in the constructor. - I developed
SimpleElection
behavior that states that the winner is whose have more votes in both absolute majority and non-absolute majority cases. Furthermore, for simplicity, I decided that on tie an exception was thrown. - In order to have complete and valid instances of
RankedChoiceElection
I added the preconditions "must have ballots", "must have candidates", "people can't vote more than once", "voted candidates must be valid", "all ballots must have same votes quantity", "ballots' votes must have different ranks", "ranks must be between one and votes length", "ballots must have same voter" and "ballots' votes must have different candidates". - I developed
RankedChoiceElection
behavior that states that the candidate with an absolute majority in first-preference votes wins. Since here, I captured the concept of "first-preference vote" from ubiquitous language. - I developed
RankedChoiceBallot
behavior that describes that when a candidate is removed, the rank of the others is modified. Also, I add here a useful getter for checking if a ballot has a candidate or not. - I noticed that in the ubiquitous language was an object that represents the different rounds in
RankedChoiceElection
. I called itRankedChoiceElectionRound
, and it's behavior is: "in every round, there is a winner and a loser based on first-preference votes count; winner could have an absolute majority or not, and in next round, the loser (i.e. the candidate with fewest first-preference votes) is removed from election". I developed that behavior and then,RankedChoiceElection
collaborates withRankedChoiceElectionRound
inCountVotes
creating a new round and asking for the next until an absolute majority winner appears. I made a design decision for simplicity that states that: if we have all the candidates ordered by first-preference votes count descending and there is a tie between the last two candidates, the last is the loser.
- Use value objects for ids instead of ints to increase code declarativity, e.g. having a value object called
CandidateId
that under the hood holds the int value. - Some methods should be divided into smaller, e.g.
RankedChoiceElectionRound.CalculateWinnerAndLoser
- The preconditions described in
RankedChoiceElectionTest.BallotsMustHaveSameVoter
,RankedChoiceElectionTest.BallotsMustHaveDifferentCandidates
,RankedChoiceElectionTest.BallotsVotesMustHaveDifferentRanks
andRankedChoiceElectionTest.BallotsMustHaveRanksBetweenOneAndVotesLength
are checked inRankedChoiceElection
but must be checked inRankedChoiceBallot
. The tests must be moved fromRankedChoiceElectionTest
toRankedChoiceBallotTest
and the logic fromRankedChoiceElection
constructor toRankedChoiceBallot
constructor. RankedChoiceElectionRound
has no precondition checks in their constructor for candidates or ballots. Surely it should have some checks similar toRankedChoiceElection
checks.- An interesting feature is a configurable on-tie action. In both voting systems, I made my design decisions for tie scenarios, but it could be configurable.