DEV Community

Cover image for Golang - How a Chef and Waiter Teach the Single Responsibility Principle
Dzung Nguyen
Dzung Nguyen

Posted on

Golang - How a Chef and Waiter Teach the Single Responsibility Principle

Welcome to the first post in my SOLID principles series for Golang! In this series, I’ll break down each principle of the SOLID design philosophy, helping you write more maintainable and scalable Go applications. We’re starting with the Single Responsibility Principle (SRP) — a foundational concept that promotes cleaner code by ensuring every module does just one thing. 🚀


🏛️ What is the Single Responsibility Principle?

The Single Responsibility Principle states:

A class, module, or function should have only one reason to change.

In simple terms, each component should focus on a single responsibility. If one piece of code is handling multiple tasks, it’s harder to maintain and extend without introducing bugs. Following SRP improves modularity, reusability, and testability.

SRP Image

👨‍🍳 A Real-World Example of SRP: The Chef and the Waiter

Imagine a busy restaurant. In this restaurant, two key people are responsible for ensuring customers have a great experience:

  • The Chef: Prepares delicious meals.
  • The Waiter: Takes orders, serves food, and handles customer requests.

Now, think about what would happen if one person had to do both jobs. If the chef had to stop cooking every time a customer arrived to take an order and serve the meal, it would:

  • Slow down the process.
  • Cause delays in food preparation.
  • Increase the chances of mistakes.

This situation is chaotic and inefficient because the same person is handling multiple unrelated responsibilities.

Applying SRP in the Restaurant

When following the Single Responsibility Principle:

  • The Chef focuses only on preparing food.
  • The Waiter focuses only on managing orders and serving customers.

By separating these roles, each person can do their job well without unnecessary interruptions. This leads to a smoother, faster, and more enjoyable dining experience for customers. 🍴✨

💻 SRP in Programming

Just like in a restaurant, where each person has a single, focused role, your code should also be structured so that each class or function handles only one responsibility. This makes your application easier to maintain, faster to change, and less prone to errors.

SRP in Action with Golang

Let’s look at an example to see how violating SRP can make code fragile and difficult to manage.

❌ Example Violating SRP

Imagine a simple order management system for a coffee shop:

package main

import "fmt"

// Order contains coffee order details.
type Order struct {
    CustomerName string
    CoffeeType   string
    Price        float64
}

// ProcessOrder performs multiple responsibilities.
func (o *Order) ProcessOrder() {
    // Handle payment processing
    fmt.Printf("Processing payment of $%.2f for %s\n", o.Price, o.CustomerName)

    // Print receipt
    fmt.Printf("Receipt:\nCustomer: %s\nCoffee: %s\nAmount: $%.2f\n", o.CustomerName, o.CoffeeType, o.Price)
}

func main() {
    order := Order{CustomerName: "John Doe", CoffeeType: "Cappuccino", Price: 4.50}
    order.ProcessOrder()
}
Enter fullscreen mode Exit fullscreen mode

Here, the Order struct is responsible for storing data, processing payments, and printing receipts. This violates SRP because it performs multiple unrelated tasks. Changes to any of these responsibilities will affect ProcessOrder, making the code less maintainable.

🛠️ Refactoring for SRP

Let’s separate the responsibilities into distinct components:

package main

import "fmt"

// Order contains coffee order details.
type Order struct {
    CustomerName string
    CoffeeType   string
    Price        float64
}

// PaymentProcessor handles payment logic.
type PaymentProcessor struct{}

func (p *PaymentProcessor) ProcessPayment(order Order) {
    fmt.Printf("Processing payment of $%.2f for %s\n", order.Price, order.CustomerName)
}

// ReceiptPrinter handles receipt printing.
type ReceiptPrinter struct{}

func (r *ReceiptPrinter) PrintReceipt(order Order) {
    fmt.Printf("Receipt:\nCustomer: %s\nCoffee: %s\nAmount: $%.2f\n", order.CustomerName, order.CoffeeType, order.Price)
}

func main() {
    order := Order{CustomerName: "John Doe", CoffeeType: "Cappuccino", Price: 4.50}

    paymentProcessor := PaymentProcessor{}
    receiptPrinter := ReceiptPrinter{}

    paymentProcessor.ProcessPayment(order)
    receiptPrinter.PrintReceipt(order)
}
Enter fullscreen mode Exit fullscreen mode

🎯 Benefits of SRP

  • Separation of Concerns: Order only stores data, PaymentProcessor handles payments, and ReceiptPrinter manages receipt generation.
  • Better Testability: You can test PaymentProcessor and ReceiptPrinter independently.
  • Easier Maintenance: Changes to receipt formatting won’t affect payment processing logic.

❓ When to Apply SRP

Look for violations like:

  • Functions or structs doing multiple unrelated tasks.
  • Modules that mix concerns, such as handling both business logic and I/O operations.

✨ Conclusion

By applying the Single Responsibility Principle, your code becomes easier to understand, maintain, and extend. This is just the beginning! Stay tuned for the next post in this series as we explore the "O" in SOLID: the Open/Closed Principle.

You can also check out my other post on Dependency Injection, which is another important technique in OOP.

Happy coding! 🎉


Follow me to stay updated with my future posts:

Top comments (0)