DRY stands for "Don't Repeat Yourself". It's a principle in software development that aims to reduce repetition of code. The idea is that every piece of knowledge must have a single, unambiguous, authoritative representation within a system.
Many programmers (us included) think that this principle is held a little too seriously by some. It's not always a good idea to abstract everything and make everything modular. Sometimes, it's better to repeat yourself a little bit to make the code more readable and maintainable.
Often, code needs subtle changes that abstractions can make increasingly difficult to implement. It's a balance that you must find in your codebase. Sometimes, it's better to be WET (Write Everything Twice) than DRY.
WET is a bit tongue-in-cheek, but it's a good reminder for duplicating first, and only merging similar code written in multiple places once it's necessary.
Going DRY too early is a form of Premature Optimization. that can lead to more problems than it solves by making your code modular, but at a cost.
Suppose I have a special logic to load the next level in my platformer game. If the player finished the level at 100%, I want to take them to a special secret level. If they didn't 100% the level, but still got more than 500 points, I want them to go to a bonus level where they can get a heart.
func pick_next_level() -> void:
if player_points >= 1000:
load_level(SPECIAL_SECRET_LEVEL)
elif player_points > 500:
load_level(BONUS_HEART_LEVEL)
else:
load_level(LEVEL_2)
Since I'm doing that in every level, I decide to keep things DRY and abstract a utility function. My idea is that in every level, I'll just declare some rules like this:
var next_level_rules := {
1000: SPECIAL_SECRET_LEVEL,
500: BONUS_HEART_LEVEL,
0: LEVEL_2
}
To read this configuration object, I make this:
func pick_level() -> void:
for amount in next_level_rules:
if player_points >= amount:
var level_scene := next_level_rules[amount]
load_level(level_scene)
return
load_level(next_level_rules[0])
I turned my code to declarative code and abstracted away my logic. Now, I only need to specify the next_level_rules
object in each level! It's more flexible right? Well...
Through playtesting, I realize that, instead of bonus levels anytime you acquire 500 points, it'd be much nicer if they happened only when you found the special star item in a level. Now, I need to change my function to accomodate for that. Darn it. Let's change how this configuration object works:
var _default := [0, ""]
var next_level_rules := {
[1000, "points"]: SPECIAL_SECRET_LEVEL,
[1, "stars"]: BONUS_HEART_LEVEL,
_default: LEVEL_2
}
func pick_level() -> void:
for item in next_level_rules:
var amount: int = item[0]
var kind: String = item[1]
if kind == "points":
if player_points >= amount:
var level_scene := next_level_rules[amount]
load_level(level_scene)
elif kind == "stars":
if player_stars >= amount:
var level_scene := next_level_rules[amount]
load_level(level_scene)
load_level(next_level_rules[_default])
Well, that became much less readable. I'm going to need to write some documentation to explain to others (and myself in the future) how it works.
But wait, the designer is saying that on level 5, they want to make it that, if you get all three stars and also 100% the level, you get a super ultra special level. Oh no! Well,I'm a pro, so I sit to do it. It takes around 50 lines of code and 2 classes now, ... but it works well.
But, I have a bug. In some specific situation on level 5, the algorithm doesn't work, and it is loading an incorrect level. How should I debug? Since the pick_level
function runs a loop, I can't really inspect what it is doing. I'm going to need to invent my special debugging techniques. I might do something like:
func pick_level() -> void:
if current_level == 5:
prints("player score on level 5: ", player_points, "points", player_stars, "stars")
for item in next_level_rules:
But sometimes, this isn't enough. The bug could be in any of the other files I've created to keep my code DRY! Because of all the Indirection, I am not sure where to look and have to open multiple files.
Now contrast what would've happened if I did not abstract anything. When my designer comes to tell me we need stars, I just change my first code to:
func pick_next_level() -> void:
if player_points >= 1000:
load_level(SPECIAL_SECRET_LEVEL)
elif player_stars > 1:
load_level(BONUS_HEART_LEVEL)
else:
load_level(LEVEL_2)
When later, I get the instruction for the special level 5? No problemo:
func pick_next_level() -> void:
if player_stars > 1 and player_points >= 1000:
load_level(SUPER_SPECIAL_SECRET_LEVEL)
elif player_points >= 1000:
load_level(SPECIAL_SECRET_LEVEL)
elif player_stars > 1:
load_level(BONUS_HEART_LEVEL)
else:
load_level(LEVEL_2)
I get a bug on level 5? I can just set a breakpoint in the level 5 function, and inspect the state there.
DRY can make the code more rigid. Writing code is already an abstraction to computation, and each new abstraction added on top of it reduces just a little bit the scope of what you can do.
Keeping your code simple, linear, stupid, increases flexibility and allows for much easier special cases. It also reduces indirection, and makes code easier to debug.
When wanting to build abstractions, always consider the trade-offs, and don't do it without a reason.
Legos are modular, and by using little lego bricks, you can build very cool things very quickly. But as much as lego offers you freedom, it can only approximate shapes; and some structures remain forever impossible. Using legos allows new things, but restricts others.
NOTE:One big mistake is to consider code as immuable. It's perfectly normal, and even desirable, to start with no abstractions, then, once your program takes shape, build some abstractions that allow you to go faster, and then even later, when the abstractions become a bad fit, to remove them and replace them with imperative code.Good code is done through iterations!
See Also
Related terms in the Glossary