Imagine you're running a coffee shop ☕, and customers ask for their order receipts in different formats - some want their receipts printed, some in PDF sent over Email, and one guy asks for a receipt over SMS. Now, imagine that you have written this billing software to support a printed receipt.🫣
Every time a new type of receipt is requested, you rewrite your entire billing system. Sounds painful, right? Well, that's precisely what happened in our Go code when UserService
was initially responsible for exporting users as JSON and CSV, and now we onboard a client who wants this in XML. Let's look at the initial UserService
.
Initial UserService: Without using Strategy Design Pattern
Imagine you have a UserService
that fetches user data and allows exporting it in different formats. Right now, you support JSON and CSV, but what if tomorrow you need XML, PDF, or Parquet?
var (
Err_NotSupportedExportType = errors.New("Not Supported export Type")
)
type IUserPersistence interface {
ListUsers(paginationObj models.Pagination) []models.UserModel
}
type UserService struct {
userPersistence IUserPersistence
}
func InitUserService(userPersistence IUserPersistence) *UserService {
return &UserService{
userPersistence: userPersistence,
}
}
func (userSvc *UserService) GetUsers(paginationObj models.Pagination) []models.UserModel {
return userSvc.userPersistence.ListUsers(paginationObj)
}
func (userSvc *UserService) ExportUsers(paginationObj models.Pagination,
exportConfig models.ExportConfig) (string, error) {
users := userSvc.GetUsers(paginationObj)
switch exportConfig.Type {
case enums.ExportType_Json:
jsonData, err := json.Marshal(users)
return string(jsonData), err
case enums.ExportType_CSV:
sb := strings.Builder{}
sb.WriteString("id,name,email\n")
for _, u := range users {
sb.WriteString(fmt.Sprintf("%d,%s,%s\n", u.Id, u.Name, u.Email))
}
return sb.String(), nil
default:
return "", Err_NotSupportedExportType
}
}
The Problem with This Approach
With the current implementation, every time you add a new export format, you modify the ExportUsers
method. This violates two key SOLID principles:
-
Single Responsibility Principle (SRP) -
UserService
should focus only on fetching users, not handling export logic. - Open/Closed Principle (OCP) - The class should be open for extension but closed for modification. Right now, adding a new export type means editing existing code, increasing the risk of breaking something.
Issues with this approach:
- 💔
UserService
becomes a bottleneck. - 💀 Every new format requires modifying existing code.
- 💥 One small change could break everything.
- 😭 Teams adding new export formats need to touch your code.
- 😫 Testing becomes harder as the service grows.
How Can We Fix This?
Instead of making UserService
aware of every export format, we need a pluggable solution. This is where the Strategy Pattern shines! ✨
💡 Introducing the Strategy Pattern
Think of it like choosing a shipping method on Amazon: You pick Standard Shipping, Express, or Same-Day - but Amazon doesn't care how it's delivered. The delivery company handles that.
Similarly, our UserService should just fetch users and let the export strategy decide how to format them.
Are you ready to start the code repair 👩🏻🔧?
Step 1: Define a Common Export Interface
// Strategy interface
type IUserExport interface {
Export(data []models.UserModel) (string, error)
}
Implement Concrete Strategies
// CSV Export: Concrete Strategy
type CsvExportStrategy struct {
}
func (e *CsvExportStrategy) Export(data []models.UserModel) (string, error) {
if len(data) == 0 {
return "", errors.New("No data")
}
sb := strings.Builder{}
sb.WriteString(data[0].GetHeader())
for _, u := range data {
sb.WriteString(u.GetRow())
}
return sb.String(), nil
}
// JSON Export: Concrete Strategy
type JSONExportStrategy struct {
}
func (e *JSONExportStrategy) Export(data []models.UserModel) (string, error) {
if len(data) == 0 {
return "", errors.New("No data")
}
jsonData, err := json.Marshal(data)
return string(jsonData), err
}
Step 3: Create a Strategy Builder
var (
Err_NotSupportedExportType = errors.New("Not Supported export Type")
)
func UserExportBuilder(config *models.ExportConfig) (IUserExport, error) {
if config.Type == enums.ExportType_CSV {
return &CsvExportStrategy{}, nil
} else if config.Type == enums.ExportType_Json {
return &JSONExportStrategy{}, nil
}
return nil, fmt.Errorf("%w, %d", Err_NotSupportedExportType, config.Type)
}
Step 4: Update UserService 🥁
func (userSvc *UserService) ExportUsers(paginationObj models.Pagination,
exportConfig exportstrategy.IUserExport) (string, error) {
users := userSvc.GetUsers(paginationObj)
return exportConfig.Export(users)
}
Now, ExportUsers
does not care how data is exported. It only retrieves users and sends the data to the correct export strategy.
Step 5: Adding a New Format (XML)
// XML Export: Concrete Strategy
type XMLExportStrategy struct {
}
func (e *XMLExportStrategy) Export(data []models.UserModel) (string, error) {
if len(data) == 0 {
return "", errors.New("No data")
}
xmlData, err := xml.Marshal(data)
return string(xmlData), err
}
Step 6: Update the Strategy Builder
func UserExportBuilder(config *models.ExportConfig) (IUserExport, error) {
switch config.Type {
case enums.ExportType_CSV:
return &CsvExportStrategy{}, nil
case enums.ExportType_Json:
return &JSONExportStrategy{}, nil
case enums.ExportType_Xml:
return &XMLExportStrategy{}, nil
default:
return nil, fmt.Errorf("%w, %d", Err_NotSupportedExportType, config.Type)
}
}
You can find this code on my Github: https://github.com/architagr/design_patterns/tree/main/golang/behavioral/strategy
🚀 Final Thoughts & Takeaways
Before: Every new format meant editing UserService
, risking bugs.
After: Adding new formats is just creating a new strategy, with no changes to the core logic.
Note:
If your if-else
statements are growing faster than your stress levels, it's time for the Strategy Pattern.
Stay Connected!
💡 Follow me on LinkedIn: Archit Agarwal
🎥 Subscribe to my YouTube: The Exception Handler
📬 Sign up for my newsletter: The Weekly Golang Journal
✍️ Follow me on Medium: @architagr
👨💻 Join my subreddit: r/GolangJournal
Top comments (0)