DRY (Don’t Repeat Yourself)

The DRY principle, which stands for Don’t Repeat Yourself, is a principle in software engineering that emphasizes avoiding code duplication. It basically states that every piece of knowledge within a system should have a single, unambiguous, authoritative representation.

Imagine you’re writing a recipe for chocolate chip cookies. If you need to measure 1 cup of flour in three different parts of the recipe, you wouldn’t write “1 cup of flour” three times. Instead, you’d declare it once at the beginning and then simply refer to it when needed. This saves you time and effort, and it also makes the recipe easier to read and maintain.

The same logic applies to software code. Repeating code not only wastes time and makes the code harder to understand, but it also increases the risk of errors. If you change something in one place, you have to remember to change it everywhere else it’s repeated.

There are many ways to implement the DRY principle in your code. Here are a few examples:

Use functions

If you find yourself writing the same block of code in multiple places, put it in a function and call it wherever you need it. Take for example a simple MathOperations class as shown below.

public class MathOperations {
        
    public int sum(int a, int b) {
        return a + b;
    }

    public double average(int a, int b) {
        return (a + b) / 2;
    }
}

You can see that a+b has been repeated in both the methods, we can remove this repetition by modiyfing our code as shown below:

public class MathOperations {

    public int sum(int a, int b) {
        return a + b;
    }

    public double average(int a, int b) {
        return sum(a, b) / 2; // reused
    }
}

Here a bit more complicated example, try and refactor the below method using DRY principle.

public static void printGuessStatistics(char candidate, int count) {
    String number;
    String verb;
    String pluralModifier;

    if (count == 0) {
        number = "no";
        verb = "are";
        pluralModifier = "s";
    } else if (count == 1) {
        number = "1";
        verb = "is";
        pluralModifier = "";
    } else {
        number = Integer.toString(count);
        verb = "are";
        pluralModifier = "s";
    }

    System.out.println(String.format("There %s %s %s%s", verb, number, candidate, pluralModifier));
}

Here’s the refactored code using DRY principle

public static void printGuessStatistics(char candidate, int count) {
    String number = formatCount(count);
    String verb = count == 1 ? "is" : "are";
    String pluralModifier = count != 1 ? "s" : "";

    System.out.println(String.format("There %s %s %s%s", verb, number, candidate, pluralModifier));
}

private static String formatCount(int count) {
    if (count == 0) {
        return "no";
    } else if (count == 1) {
        return "1";
    } else {
        return Integer.toString(count);
    }
}

Explanation of Changes:

Extracted Common Logic: The formatting logic for number, verb, and pluralModifier has been extracted into a separate private function formatCount().

Ternary Operators for Conciseness: Ternary operators are used to conditionally assign values to verb and pluralModifier based on count, making the code more concise.

Single String.format() Call: The final string formatting is now done in a single String.format() call, using the variables directly.

Use classes

Classes can group related functions and data together, which can help to reduce code duplication. Here’s an example illustrating how we can achieve this and adhere to the DRY principle:

Imagine you’re building a program to manage a library’s book collection. You need to perform various operations on books, such as:

  • Creating new book objects
  • Getting and setting book properties (title, author, ISBN, etc.)
  • Displaying book information
  • Checking book availability

Non-DRY Approach:

Without classes, you might create separate functions for each operation, leading to code duplication:

String createBookString(String title, String author, String ISBN) {
    // Create a string representation of a book
}

void displayBook(String title, String author, String ISBN) {
    // Print book information
}

boolean isBookAvailable(String title, String author, String ISBN) {
    // Check if book is available
}

DRY Approach with Classes:

By creating a Book class, you can encapsulate book-related data and actions together:

class Book {
    private String title;
    private String author;
    private String ISBN;
    private boolean isAvailable = true;

    public Book(String title, String author, String ISBN) {
        this.title = title;
        this.author = author;
        this.ISBN = ISBN;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    // ... other getters and setters for author, ISBN, etc.

    public void displayBookInfo() {
        System.out.println("Title: " + title);
        System.out.println("Author: " + author);
        System.out.println("ISBN: " + ISBN);
        // ... other book information
    }

    public boolean isAvailable() {
        return isAvailable;
    }

    public void setAvailable(boolean available) {
        isAvailable = available;
    }
}

Now, you can create book objects and perform operations directly on them:

Book book1 = new Book("The Lord of the Rings", "J.R.R. Tolkien", "1234567890");
// Output: Title: The Lord of the Rings, Author: J.R.R. Tolkien, ISBN: 1234567890
book1.displayBookInfo(); 
// Mark book as unavailable
book1.setAvailable(false); 

Use constants

If you have a value that is used in multiple places, make it a constant. This will make your code more readable and prevent typos.

Here’s an example demonstrating how using constants for repeated values can improve code readability, maintainability, and prevent typos, adhering to the DRY principle:

Without Constants:

public class InvoiceCalculator {
    public double calculateTotal(double subtotal) {
        double taxRate = 0.07;  // Sales tax rate
        double shippingFee = 5.99;  // Fixed shipping fee

        double taxAmount = subtotal * taxRate;
        double total = subtotal + taxAmount + shippingFee;

        return total;
    }

    public void printInvoice(double total) {
        System.out.println("Subtotal: $" + total);
        System.out.println("Tax (7%): $" + (total * 0.07));  // Typo: tax rate hardcoded again
        System.out.println("Shipping: $5.99");
        System.out.println("Total: $" + total);
    }
}

With Constants:

public class InvoiceCalculator {
    public static final double TAX_RATE = 0.07;  // Declare constants
    public static final double SHIPPING_FEE = 5.99;

    public double calculateTotal(double subtotal) {
        double taxAmount = subtotal * TAX_RATE;
        double total = subtotal + taxAmount + SHIPPING_FEE;

        return total;
    }

    public void printInvoice(double total) {
        System.out.println("Subtotal: $" + total);
        System.out.println("Tax (7%): $" + (total * TAX_RATE));  // Use the constant
        System.out.println("Shipping: $" + SHIPPING_FEE);
        System.out.println("Total: $" + total);
    }
}

Use abstractions

Abstractions can help you to hide the implementation details of your code, which can make it easier to reuse and maintain.

Here’s an example demonstrating how abstractions can help hide implementation details, making code more reusable and maintainable, adhering to the DRY principle. Imagine you’re building a program that interacts with different types of databases (MySQL, PostgreSQL, etc.). You want to write code that can work with any database without needing to change the core logic for each type.

Without Abstraction:

Without abstraction, you might have separate functions for each database type, leading to code duplication and tight coupling:

public interface Database {
    void connect();
    void executeQuery(String query);
    void disconnect();
}

Then, create concrete implementations of this interface for each database type:

public class MySQLDatabase implements Database {
    // Implementation for MySQL-specific connection, query execution, etc.
}

public class PostgreSQLDatabase implements Database {
    // Implementation for PostgreSQL-specific connection, query execution, etc.
}

Now, your main code can work with any database through the common interface, without knowing the underlying implementation:

public void saveData(Data data, Database database) {
    database.connect();
    database.executeQuery("INSERT INTO ...");  // Use a generic query
    database.disconnect();
}

Following the DRY principle is a great way to write cleaner, more maintainable, and less error-prone code. It may take some effort to get used to it at first, but the benefits are well worth it in the long run.