Clean Code Series: Eliminating Duplication (The DRY Principle)


Hey there, dev! That time you thought, "Oh, it's just a tiny piece of code, I'll just copy it here quickly, no big deal"... Ever stopped to think about the snowball effect that small, seemingly harmless act can create? Well, code duplication is like that guest who comes for a week and ends up staying for a month: sneaky, and by the time you realize it, it has taken over your house (or your codebase).
But hold on, all is not lost! To combat this silent villain, we have one of the most powerful mantras in software development: DRY – Don't Repeat Yourself. And you can bet that Uncle Bob, even though DRY is the star of "The Pragmatic Programmer," abhors duplication in "Clean Code" – it's the famous "smell": Duplicated Code. Shall we understand why this principle is so crucial and how it can save your project (and your sanity)?
1. DRY: Unveiling the Secret of Unique and Powerful Code
What does this DRY thing mean in real life? The classic definition is: "Every piece of knowledge must have a single, unambiguous, authoritative representation within a system." Translating to plain English: every little piece of knowledge or logic in your system should live in one single place, without ambiguity, and be the official source of truth.
And we're not just talking about that identical block of code you blatantly copied. DRY goes beyond that! It fights the duplication of:
- Business logic.
- Algorithms.
- Constants and magic values.
- Data structures.
- Configurations.
But why on earth is duplication so bad?
- Maintenance Nightmare: Need to fix a bug or change a rule? If that logic is scattered in 10 different places, get ready for a frustrating treasure hunt and a high chance of forgetting a spot. Result? Inconsistencies and more bugs!
- Bloated and Potentially Slow Code: More code means more to read, more to compile, more to test, and sometimes, even an impact on performance.
- Difficulty in Understanding: When the same logic appears in slightly different forms in various places, it becomes hard to understand which is the "correct version" or the expected behavior.
- Resistance to Change: A codebase full of duplications becomes a minefield. Any change is a risk.
2. The Many Faces of Duplication (And How Not to Fall for Its Traps)
Duplication is a master of disguise. Keep an eye out:
- The Classic Copy-Paste: The most obvious one. You select a snippet, CTRL+C, find another place that needs "something similar," CTRL+V. Sometimes with a small tweak to "disguise" it.
- The "Made-Up" Duplication:
- Similar Logic, Small Differences: Two functions that do 90% the same thing but with one or two different lines. The temptation to duplicate and alter is strong, but dangerous.
- Ghost Data Structures: Multiple classes or structs representing the same entity in slightly different ways.
- Magic Values Everywhere: That string or number appearing in several
if
s or calculations without any explanation of what it means. If you need to change it, you know the drill, right? - Fragmented Business Knowledge: A customer's validation rules, for example, scattered across the UI layer, the API, and the service. Which one is the source of truth?
Ever caught yourself justifying duplicating an entire function because of a single different line? Yep, it happens in the best of (code) families.
3. Anti-Repetition Toolkit: Your Weapons in the Fight for DRY Code
Enough suffering! We have an arsenal of strategies to send duplication into outer space:
- Functions and Methods: Your first and most powerful line of defense. See logic repeating? Encapsulate it in a well-named function and reuse it!
- Classes, Structs, and Abstractions: Model your domain so that knowledge is centralized. Create classes that represent unique concepts and carry their own responsibilities.
- Inheritance with Wisdom (or Composition as a Powerful Alternative): Inheritance can help reuse code, but be careful not to create fragile hierarchies and excessive coupling. Often, composition (where a class has another) is a more flexible and safer way to achieve reuse.
- Modules, Packages, and Libraries: Group related and reusable functionalities that can be imported and used in different parts of your system or even in other projects.
- Templates and Code Generators (Use With Moderation!): For code that is truly boilerplate and follows a very rigid pattern, templates can help. But be careful not to generate complexity or code that no one understands how it works "under the hood."
- Centralized Configurations: Constants, URLs, API keys—everything configuration-related should come from a single place (config files, environment variables, etc.).
- Design Principles (SOLID, for example): Following good design principles naturally leads to less duplicated and more modular code.
Practical Refactoring Example (Go):
Imagine we have two functions to process orders, one for regular orders and another for VIP orders, with a lot of repeated logic:
// Before: Lots of logic duplication
// Assuming Order and Item are defined structs
// type Order struct {
// ID string
// Items []Item
// Total float64
// }
// type Item struct {
// // item fields
// }
// func isStockAvailable(items []Item) bool { /* ... */ return true }
// func calculateTotal(items []Item) float64 { /* ... */ return 100.0 }
// var fmt = import("fmt") // Mocking for example
// var errors = import("errors") // Mocking for example
func processRegularOrder(order Order) error {
fmt.Println("Validating order items:", order.ID)
// ... item validation logic ...
if !isStockAvailable(order.Items) {
return errors.New("insufficient stock")
}
fmt.Println("Calculating order total:", order.ID)
total := calculateTotal(order.Items)
// ... apply common discounts ...
order.Total = total
fmt.Println("Registering order in the system:", order.ID)
// ... logic to save to database ...
fmt.Println("Regular order processed:", order.ID)
return nil
}
func processVipOrder(order Order) error {
fmt.Println("Validating VIP order items:", order.ID) // Slight difference in log
// ... item validation logic (IDENTICAL) ...
if !isStockAvailable(order.Items) {
return errors.New("insufficient stock for VIP") // Slight difference in msg
}
fmt.Println("Calculating VIP order total:", order.ID)
total := calculateTotal(order.Items)
// ... apply common discounts ...
// ... apply ADDITIONAL VIP DISCOUNTS ...
order.Total = total * 0.9 // Example VIP discount
fmt.Println("Registering VIP order in the system with priority:", order.ID) // Difference in log and maybe in save logic
// ... logic to save to database with priority ...
fmt.Println("VIP order processed:", order.ID)
return nil
}
"Before" Analysis: Notice how much code is almost identical? Validation, base total calculation, and the general structure. If item validation changes, we have to remember to change it in both places!
Refactoring to DRY Code:
// After: Common logic extracted
// Assuming Order and Item are defined structs
// type Order struct {
// ID string
// Items []Item
// Total float64
// }
// type Item struct {
// // item fields
// }
// func isStockAvailable(items []Item) bool { /* ... */ return true }
// func calculateTotal(items []Item) float64 { /* ... */ return 100.0 }
// var fmt = import("fmt") // Mocking for example
// var errors = import("errors") // Mocking for example
func validateItemsAndStock(items []Item) error {
fmt.Println("Validating items and stock...")
// ... item validation logic ...
if !isStockAvailable(items) {
return errors.New("insufficient stock")
}
return nil
}
func calculateBaseTotal(items []Item) float64 {
fmt.Println("Calculating base total...")
// ... logic to calculate total WITHOUT specific discounts ...
return calculateTotal(items)
}
func saveOrder(order Order, isVip bool) error {
if isVip {
fmt.Println("Registering VIP order in the system with priority:", order.ID)
// ... logic to save to database with priority ...
} else {
fmt.Println("Registering order in the system:", order.ID)
// ... logic to save to database ...
}
return nil
}
type OrderProcessor struct {
// dependencies like notification service, etc.
}
func (op *OrderProcessor) Process(order Order, isVip bool) error {
if err := validateItemsAndStock(order.Items); err != nil {
// Could add more context to the error here if needed
return fmt.Errorf("validation for order %s failed: %w", order.ID, err)
}
baseTotal := calculateBaseTotal(order.Items)
order.Total = baseTotal
if isVip {
fmt.Println("Applying VIP discounts for order:", order.ID)
order.Total = baseTotal * 0.9 // Example VIP discount
} else {
// ... apply common discounts if any ...
}
if err := saveOrder(order, isVip); err != nil {
return fmt.Errorf("failed to save order %s: %w", order.ID, err)
}
fmt.Printf("Order %s (VIP: %t) processed successfully!\n", order.ID, isVip)
return nil
}
"After" Analysis: We extracted the common parts (validateItemsAndStock
, calculateBaseTotal
, saveOrder
) into helper functions. The Process
function now orchestrates the flow, applying specific logic (VIP discount) when necessary. Much cleaner and safer to maintain, don't you agree?
4. The DRY Family: Meet WET and SPOT
DRY isn't alone in this fight:
- WET (Write Everything Twice... or We Enjoy Typing 😉): This is DRY's arch-nemesis. If you find yourself writing the same thing (or almost the same thing) multiple times, you're swimming in WET code. And believe me, that pool isn't refreshing in the long run.
- SPOT (Single Point of Truth): A very close cousin and a fundamental goal. It means that every piece of information or business rule in your system should have ONE SINGLE place where it's defined and maintained as the absolute truth. The DRY principle is one of the main tools to achieve SPOT. If you don't repeat yourself, it's easier to ensure information is correct and consistent in a single point.
Conclusion: One Code, Many Reuses, Zero Headaches!
My dear dev, embracing DRY isn't just a good practice; it's an investment in your peace of mind and your project's health. Less duplication means code that's easier to understand, faster to modify, less prone to bugs, and much more elegant.
So, the challenge is on: in your next feature, your next code review, put on your DRY detective glasses and mercilessly hunt down any duplication that crosses your path. Your "future self" (and your entire team) will thank you with a warm cup of coffee! ☕
📣 The Conversation Doesn't Stop Here!
What are your ninja strategies for keeping code DRY and avoiding WET? Ever faced a duplication monster that gave you chills? Share your stories and tips in the comments!
👉 Follow me on Hashnode (or wherever you're reading this) and let's continue this chat on LinkedIn!
🔖 Next Chapter in the Clean Code Saga: Get ready to master the art of handling the unexpected! We'll dive into the world of elegant and robust Error Handling. Don't miss it!
Subscribe to my newsletter
Read articles from Phelipe Rodovalho directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
