How Many Times Must I Tell You, “Don’t Repeat Yourself”?
I spend a significant amount of my time these days doing PC programming (as opposed to PLC programming) and I’d say the most time consuming part of my job is following the DRY Principle, aka “Don’t Repeat Yourself”.
The naive interpretation of the DRY principle is that it’s about minimizing the amount of code you write. While this may sometimes be a side effect, this isn’t the point of DRY at all. What we’re trying to do is structure the program so that there’s just one place for everything, and everything is in it’s one place. This is a lot harder than it sounds because no matter how you segment your program into modules, classes, or layers there’s always something that cuts across those boundaries.
Take logging for instance. Every part of the application needs to log things to the same log file. The naive implementation of opening a file, writing a line, and closing it is short but doesn’t follow the DRY principle because we’re repeating this simple sequence of steps all over the place. The danger is that if we change our logging strategy, we now have to change multiple places in every file.
There are probably two ways that the “logging strategy” could change:
- We change where (or if) we want to log the data, like to a database instead of a file
- We change the kinds of stuff we want to log
We can handle the first case easily by moving the logging logic into a global subroutine, or if you’re more advanced you would use something like a logging service with the service locator pattern. That would let us change where we log the data, and if we include a severity level with the logging interface, we could filter our logging by severity.
The second case is really hard to solve. We have to choose each spot in the code where we want to log information. Maybe every time I catch an error in a try/catch block, but that means every time I write a try/catch block I have to add a call to my logging service. This case violates the DRY principle because the idea is just “every time I catch an error in a try/catch block”. That idea needs to be codified in one place. Ideally I should be able to extend the try/catch block of the language and say, “any time you execute a catch I also want you to do this”. That’s not possible with .NET, that I know of anyway.
Likewise, maybe I want to know every time a user carries out any action that might affect the underlying data in the database, and I’ll have to add logging code at each of those places too. If you have all of your database access going through a single layer in your application, at least you can confine the logging to that layer, but it may consist of dozens of classes representing dozens of database tables. That’s where we need something like the ADO.NET entity framework, and have all of our database entities derive from a single base class, and then we might be able to tie our logging rules in there.
The problem is that these architectural solutions are hard. They take a lot of effort, and when you just need to add logging, it’s too easy to just go through your code and add logging wherever you need it, and you’ve created a maintenance nightmare. It’s the extremely low cost of copy and paste vs. architecture that creates this problem. If you had to pay $1 every time you copied and pasted code, I bet you’d write more maintainable software.