Lesson 1
Introducing Complex Features While Maintaining Backward Compatibility in Go
Introduction

Welcome to today's lesson, where we will address a common challenge in software engineering: introducing complex features while preserving backward compatibility. Our focus will be on a Potluck Dinner organization system, where we will manage participants and their respective dishes for each round. Get ready for an exciting journey through Go programming, step-by-step analysis, and strategic thinking. Let's dive into our adventure!

Starter Task Review

Initially, our Potluck Dinner organization system allows us to add and remove participants and manage their respective dishes for each round. There are three essential functions:

  • AddParticipant(memberId string) bool: This function adds a participant. If a participant with the given memberId already exists, it won't create a new one but will return false. Otherwise, it will add the member and return true.
  • RemoveParticipant(memberId string) bool: This function removes a participant with the given memberId. If the participant exists, the system will remove them and return true. Otherwise, it will return false. When removing a participant, you need to remove their dish if they brought one.
  • AddDish(memberId string, dishName string) bool: This function enables each participant to add their dishes for every round. If a participant has already added a dish for this round OR if the memberId isn't valid, it will return false. Otherwise, it will add the dish for the respective participant's round and return true.

Let's write our Go code, which implements these functions as per our initial state:

Go
1package main 2 3import "fmt" 4 5type Potluck struct { 6 participants map[string]bool 7 dishes map[string]string 8} 9 10func NewPotluck() *Potluck { 11 return &Potluck{ 12 participants: make(map[string]bool), 13 dishes: make(map[string]string), 14 } 15} 16 17func (p *Potluck) AddParticipant(memberId string) bool { 18 if p.participants[memberId] { 19 return false 20 } 21 p.participants[memberId] = true 22 return true 23} 24 25func (p *Potluck) RemoveParticipant(memberId string) bool { 26 if !p.participants[memberId] { 27 return false 28 } 29 delete(p.participants, memberId) 30 delete(p.dishes, memberId) 31 return true 32} 33 34func (p *Potluck) AddDish(memberId, dishName string) bool { 35 if _, exists := p.participants[memberId]; !exists || p.dishes[memberId] != "" { 36 return false 37 } 38 p.dishes[memberId] = dishName 39 return true 40}

In this code, we used Go's map to store participant IDs and their respective dish names. With this foundation laid, let's introduce some advanced functionalities.

Introducing Advanced Functionalities

Our Potluck Dinner organization system is currently simple but practical. To make it even more exciting, we're going to introduce a "Dish of the Day" feature. This feature will enable participants to vote, and the dish receiving the most votes will be declared the "Dish of the Day."

To add this feature, we will define two new functions:

  • Vote(memberId, voteId string) bool: This function will allow a participant to cast a vote for a dish. Each participant can cast a vote only once per round. If a participant tries to vote again or if the memberId isn't valid, it should return false.
  • DishOfTheDay() string: This function will calculate and return the "Dish of the Day" based on the votes received. If multiple dishes tie for the highest number of votes, the dish brought by the participant who joined first acquires precedence. If there are no votes, the function returns an empty string.
Building Advanced Features: Step 1: Struct Initialization

We need to extend our existing Potluck struct to accommodate these new features. In Go, constructors are not explicit, so we'll modify the struct initialization and setup:

Go
1type Potluck struct { 2 participants map[string]int64 3 dishes map[string]string 4 votes map[string]string 5} 6 7func NewPotluck() *Potluck { 8 return &Potluck{ 9 participants: make(map[string]int64), 10 dishes: make(map[string]string), 11 votes: make(map[string]string), 12 } 13}

We've altered our participants data structure into a map[string]int64, where the key is the participant's ID and the value is the join time (using the current time).

Step 2: Implementing the vote function
Go
1func (p *Potluck) Vote(memberId, voteId string) bool { 2 if _, exists := p.participants[memberId]; exists && p.votes[memberId] == "" { 3 p.votes[memberId] = voteId 4 return true 5 } 6 return false 7}

The Vote function ensures that a participant can only vote once per round and that the memberId is valid before adding the vote.

Step 3: Implementing the dishOfTheDay function
Go
1func (p *Potluck) DishOfTheDay() string { 2 if len(p.votes) == 0 { 3 return "" 4 } 5 6 voteCount := make(map[string]int) 7 for _, vote := range p.votes { 8 voteCount[vote]++ 9 } 10 11 maxVotes := 0 12 for _, count := range voteCount { 13 if count > maxVotes { 14 maxVotes = count 15 } 16 } 17 18 maxVoteDishes := make(map[string]bool) 19 for dish, count := range voteCount { 20 if count == maxVotes { 21 maxVoteDishes[dish] = true 22 } 23 } 24 25 earliestJoinTime := int64(^uint64(0) >> 1) 26 dishOfTheDayMember := "" 27 28 for memberId := range maxVoteDishes { 29 if p.participants[memberId] < earliestJoinTime { 30 earliestJoinTime = p.participants[memberId] 31 dishOfTheDayMember = memberId 32 } 33 } 34 35 return fmt.Sprintf("Participant: '%s', Dish: '%s'", dishOfTheDayMember, p.dishes[dishOfTheDayMember]) 36}

The DishOfTheDay function calculates votes for each dish, determines the dish with the maximum votes, and, in the event of a tie, selects the earliest joiner to determine the winner.

Step 4: Updating Previous Functions for Compatibility and Integration

As we introduce the two advanced functionalities, our existing system needs to be adeptly updated to ensure seamless integration while maintaining backward compatibility. Here's how we've refined the previous functions:

The AddParticipant function now stores the join time of a participant in a map to utilize this information for the "Dish of the Day" feature.

Go
1import "time" 2 3func (p *Potluck) AddParticipant(memberId string) bool { 4 if _, exists := p.participants[memberId]; exists { 5 return false 6 } 7 p.participants[memberId] = time.Now().Unix() 8 return true 9}
Step 5: Adjustments in removeParticipant

The original RemoveParticipant function has been modified to consider the impact of removing a participant who might have already voted or added a dish.

Go
1func (p *Potluck) RemoveParticipant(memberId string) bool { 2 if _, exists := p.participants[memberId]; !exists { 3 return false 4 } 5 delete(p.participants, memberId) 6 delete(p.dishes, memberId) 7 delete(p.votes, memberId) 8 return true 9}
Ensuring Backward Compatibility
  • Participants Data Structure Change: By transitioning from using simple boolean flags to storing join times in a map[string]int64, we retain unique identification while adding join time functionality. This change maintains compatibility as external handling of participant IDs remains unchanged.

  • Function Signature Consistency: All function signatures (AddParticipant, RemoveParticipant, AddDish) remain unchanged to ensure seamless integration with existing client code.

Final Implementation

Combining all our steps, this is the final implementation of our Potluck system in Go with all the required features:

Go
1package main 2 3import ( 4 "fmt" 5 "time" 6) 7 8type Potluck struct { 9 participants map[string]int64 10 dishes map[string]string 11 votes map[string]string 12} 13 14func NewPotluck() *Potluck { 15 return &Potluck{ 16 participants: make(map[string]int64), 17 dishes: make(map[string]string), 18 votes: make(map[string]string), 19 } 20} 21 22func (p *Potluck) AddParticipant(memberId string) bool { 23 if _, exists := p.participants[memberId]; exists { 24 return false 25 } 26 p.participants[memberId] = time.Now().Unix() 27 return true 28} 29 30func (p *Potluck) RemoveParticipant(memberId string) bool { 31 if _, exists := p.participants[memberId]; !exists { 32 return false 33 } 34 delete(p.participants, memberId) 35 delete(p.dishes, memberId) 36 delete(p.votes, memberId) 37 return true 38} 39 40func (p *Potluck) AddDish(memberId, dishName string) bool { 41 if _, exists := p.participants[memberId]; !exists || p.dishes[memberId] != "" { 42 return false 43 } 44 p.dishes[memberId] = dishName 45 return true 46} 47 48func (p *Potluck) Vote(memberId, voteId string) bool { 49 if _, exists := p.participants[memberId]; exists && p.votes[memberId] == "" { 50 p.votes[memberId] = voteId 51 return true 52 } 53 return false 54} 55 56func (p *Potluck) DishOfTheDay() string { 57 if len(p.votes) == 0 { 58 return "" 59 } 60 61 voteCount := make(map[string]int) 62 for _, vote := range p.votes { 63 voteCount[vote]++ 64 } 65 66 maxVotes := 0 67 for _, count := range voteCount { 68 if count > maxVotes { 69 maxVotes = count 70 } 71 } 72 73 maxVoteDishes := make(map[string]bool) 74 for dish, count := range voteCount { 75 if count == maxVotes { 76 maxVoteDishes[dish] = true 77 } 78 } 79 80 earliestJoinTime := int64(^uint64(0) >> 1) 81 dishOfTheDayMember := "" 82 83 for memberId := range maxVoteDishes { 84 if p.participants[memberId] < earliestJoinTime { 85 earliestJoinTime = p.participants[memberId] 86 dishOfTheDayMember = memberId 87 } 88 } 89 90 return fmt.Sprintf("Participant: '%s', Dish: '%s'", dishOfTheDayMember, p.dishes[dishOfTheDayMember]) 91} 92 93func main() { 94 fmt.Println("Go Potluck System Initialized!") 95}
Lesson Summary

Bravo! You've successfully completed the task of introducing complex features while preserving backward compatibility using Go. This skill is invaluable in real-life software engineering scenarios where existing codebases can be extensive, and breaking changes can be problematic. Strengthen this ability with even more practice and explore similar challenges. I'll see you in the next lesson! Happy coding!

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