Mixing DRY and DAMP

Posted on 2024-08-28
software-engineering design-patterns
DRY aka "Don't Repeat Yourself" was first coined in the book Pragmatic Programmer by Andrew Hunt and David Thomas. Here is the snippet from the book explaining the principle:
Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.
Why do we call it DRY?
DRY—Don't Repeat Yourself
"Don't Repeat Yourself" is just a short and punchy statement so it sticks around in the head. The principle is based on three clear callouts: Single, Unambiguous, and Authoritative.
DAMP aka "Descriptive And Meaningful Phrases", was coined by Jay Fields. He published a blog post comparing the two approaches and proposing that DAMP is required in scenarios where you are working with DSL. I want to widen the lens here a bit and propose that they are complimentary. DAMP emphasises being descriptive and meaningful, so does DRY on being unambiguous. Focusing on the intent they both want the intent of the outcome clear and self-explanatory.
Show-Time!
Here is a sample code: class `ProductOffer` which stores the `actualPrice` and the `discountPercentage`. The class is responsible for displaying formatted Prices and calculating the finalPrice.
            
    class ProductOffer(float actualPrice, float discountPercentage)
    {
        public float ActualPrice { get; } = actualPrice;
        public float DiscountPercentage { get; } = discountPercentage;

        public float FinalPrice()
        {
            return ActualPrice - (ActualPrice * DiscountPercentage / 100);
        }

        public float Discount()
        {
            return (ActualPrice * DiscountPercentage / 100);
        }


        public string FormattedActualPrice()
        {
            return "AUD" + ActualPrice.ToString("0.00");
        }

        public string FormattedDiscount()
        {
            return "AUD" + (ActualPrice * DiscountPercentage / 100).ToString("0.00");
        }

        public string FormattedFinalPrice()
        {
            return "AUD" + (ActualPrice - (ActualPrice * DiscountPercentage / 100)).ToString("0.00");
        }
    }
            
        
This is not great as there are quite a few duplications in the code. The formatting itself is copy-pasted in each method, prefix of currency is present in each of the methods responsible for formatting. And finally, the discount and the final prices are calculated in 2 different places.
Let's start by DRYing out some of these duplications:
            
    class ProductOffer(float actualPrice, float discountPercentage)
    {
       public float ActualPrice { get; } = actualPrice;
       public float DiscountPercentage { get; } = discountPercentage;

       public float FinalPrice()
       {
           return ActualPrice - (ActualPrice * DiscountPercentage / 100);
       }


       public float Discount()
       {
           return (ActualPrice * DiscountPercentage / 100);
       }


       public string FormattedActualPrice()
       {
           return FormattedPrice(ActualPrice);
       }


       public string FormattedDiscount()
       {
           return FormattedPrice(Discount());
       }


       public string FormattedFinalPrice()
       {
           return FormattedPrice(FinalPrice());
       }


       private static string FormattedPrice(float price)
       {
           return "AUD" + price.ToString("0.00");
       }
    }
            
        
That is looking better for sure, but is it??
While I have definitely DRYed it out, and we aren't seeing any duplication, I would argue whether it is "unambiguous" and is "single". Also going by DAMP principles, neither was the first approach descriptive and meaningful, nor I find this one adding a fair amount of meaning. The reason I make this argument is we have taken a simplistic view of the class `ProductOffer`. Let's broaden our view, and take the lens of the application which has other classes such as `Order`. There would be an overall price associated with the `Order`, and `OrderLineItem` would also store the pricing information from a point-in-time aspect. All these classes would be duplicating the code.
As a developer when I came across the first code snippet, I quickly identified duplication and I extracted it out. I created an abstraction. It is in this case "the wrong abstraction". This takes me to call out another related principle AHA aka "Avoid Hasty Abstractions". The principle calls out for Optimize for change first and Avoid premature optimisations.
With this new-found mindset, we will write another iteration of the above with the new context:
            
    class Price(float value, string currency)
    {
       public readonly float Value = value;
       public readonly string Currency = currency;


       public static Price operator +(Price first, Price second)
       {
           ValidateCompatibleCurrency(first, second);
           return new Price(first.Value + second.Value, first.Currency);
       }


       public static Price operator -(Price first, Price second)
       {
           ValidateCompatibleCurrency(first, second);
           return new Price(first.Value + second.Value, first.Currency);
       }


       public static Price operator *(Price price, float multiplier)
       {
           return new Price(price.Value * multiplier, price.Currency);
       }


       public string Format()
       {
           return Currency + Value.ToString("0.00");
       }


       private static void ValidateCompatibleCurrency(Price first, Price second)
       {
           if (first.Currency != second.Currency)
               throw new InvalidOperationException("Cannot add prices with different currencies");
       }
    }


    class ProductOffer(Price actualPrice, float discountPercentage)
    {
       public Price FinalPrice()
       {
           return actualPrice - (actualPrice * (discountPercentage / 100));
       }


       public Price Discount()
       {
           return actualPrice * (discountPercentage / 100);
       }
    }
            
        
This code above removes the need for all the formatting functions as the `Format()` now abstracts the formatting away (You can even choose to abstract it by overriding `ToString()`, which I didn't prefer for myself as it hides away the formatting). One may think, the line of codes in the examples have increased, BUT this is reusable code, which would, as discussed above, be reused by `Order` and `OrderLineItem`.
I would happily say it is now - DRY and DAMP 😛
Ultimately...
These compliment each other, rather than contradict - There was a huge upsurge on DRY production code and DAMP unit tests, and as Vladimir points out in his blog it is a false dichotomy. You can DRY out your unit tests and also have them fairly descriptive as they serve as live documentation. This can be applied to any aspect of the code, not just unit tests.
These are guiding principles not lines in stones - You should understand what works for the codebase you own. It is okay to duplicate and sit on the duplication for a bit, until you feel right to abstract out. By being mindful of the decisions you make and the impact they can have, iterative steps often navigate us to the right directions.