Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Understanding DRY Principle: When Duplicate Code Doesn't Violate It

Tech 1

Introduction

The DRY principle (Don't Repeat Yourself) advocates avoiding duplicate code in software development. However, many developers misunderstand what constitutes "repetition" in this context. Simply having two identical code blocks does not necessarily violate DRY. Conversely, different code segments may still violate it if they perform the same functional task.

Logical Duplication in Code

Consider the following example:

public class UserValidator {
    public void validate(String username, String password) {
        if (!isUsernameValid(username)) {
            throw new InvalidUsernameException();
        }
        if (!isPasswordValid(password)) {
            throw new InvalidPasswordException();
        }
    }

    private boolean isUsernameValid(String username) {
        if (StringUtils.isBlank(username)) return false;
        int length = username.length();
        if (length < 4 || length > 64) return false;
        if (!StringUtils.isAllLowerCase(username)) return false;
        for (int i = 0; i < length; ++i) {
            char c = username.charAt(i);
            if (!(c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '.') {
                return false;
            }
        }
        return true;
    }

    private boolean isPasswordValid(String password) {
        if (StringUtils.isBlank(password)) return false;
        int length = password.length();
        if (length < 4 || length > 64) return false;
        if (!StringUtils.isAllLowerCase(password)) return false;
        for (int i = 0; i < length; ++i) {
            char c = password.charAt(i);
            if (!(c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '.') {
                return false;
            }
        }
        return true;
    }
}

At first glance, isUsernameValid and isPasswordValid appear to be duplicates, violating DRY. A naive refactoring would merge them into a single method:

public class UserValidatorV2 {
    public void validate(String username, String password) {
        if (!isUsernameOrPasswordValid(username)) {
            throw new InvalidUsernameException();
        }
        if (!isUsernameOrPasswordValid(password)) {
            throw new InvalidPasswordException();
        }
    }

    private boolean isUsernameOrPasswordValid(String input) {
        // Shared validation logic
        return true;
    }
}

However, this approach is flawed because these functions serve different semantic purposes—validating usernames versus passwords. Even though their current logic matches, future changes might make their validation rules diverge. Thus, merging them prematurely breaks the principle of separation of concerns.

Instead, extract shared logic into reusable helper methods:

private boolean isAlphaNumericDotOnly(String input) {
    // Check if string contains only lowercase letters, digits, and dots
    return true;
}

Semantic Repetition

In another case, consider two functions performing the same functionality under different names:

public boolean isValidIpAddress(String ip) {
    if (StringUtils.isBlank(ip)) return false;
    String regex = "^(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|[1-9])\\.(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)\\.(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)\\.(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)$";
    return ip.matches(regex);
}

public boolean checkIfIpValid(String ip) {
    if (StringUtils.isBlank(ip)) return false;
    String[] parts = StringUtils.split(ip, '.');
    if (parts.length != 4) return false;
    for (int i = 0; i < 4; ++i) {
        int value;
        try {
            value = Integer.parseInt(parts[i]);
        } catch (NumberFormatException e) {
            return false;
        }
        if (value < 0 || value > 255) return false;
        if (i == 0 && value == 0) return false;
    }
    return true;
}

These functions differ in implementation but perform the same logical operation—validating an IP address. This constitutes semantic duplication and violates DRY. Only one function should exist for consistent use throughout the system.

Execution Duplication

Consider this scenario involving redundant operations:

public class UserService {
    private UserRepository repo;

    public User login(String email, String password) {
        boolean exists = repo.checkUserExists(email, password);
        if (exists) throw new AuthenticationFailureException();
        User user = repo.getUserByEmail(email);
        return user;
    }
}

public class UserRepository {
    public boolean checkUserExists(String email, String password) {
        if (!EmailValidator.validate(email)) {
            throw new InvalidEmailException();
        }
        if (!PasswordValidator.validate(password)) {
            throw new InvalidPasswordException();
        }
        // Database query
    }

    public User getUserByEmail(String email) {
        if (!EmailValidator.validate(email)) {
            throw new InvalidEmailException();
        }
        // Database query
    }
}

Here, email validation occurs twice—once during checkUserExists, and again during getUserByEmail. To adhere to DRY, move validation to the service layer and eliminate redundant database calls:

public class UserService {
    private UserRepository repo;

    public User login(String email, String password) {
        if (!EmailValidator.validate(email)) {
            throw new InvalidEmailException();
        }
        if (!PasswordValidator.validate(password)) {
            throw new InvalidPasswordException();
        }
        User user = repo.getUserByEmail(email);
        if (user == null || !password.equals(user.getPassword())) {
            throw new AuthenticationFailureException();
        }
        return user;
    }
}

Enhancing Code Reusability

Code reusability is a key quality metric. Strategies to improve it include:

  1. Reducing Coupling: Lower coupling makes extraction easier.
  2. Single Responsibility Principle: Smaller, focused modules are more reusable.
  3. Modularization: Encapsulate independent functionalities into modules.
  4. Separation of Business and Non-Business Logic: Isolate generic components.
  5. Push Common Code Downward: Place reusable logic at lower layers.
  6. Use Inheritance, Polymorphism, Abstraction: Promote reuse through design patterns.
  7. Apply Design Patterns: Templates and others enhance modularity.

Additionally, programming constructs like generics can aid reusability. While building reusable code is challenging with out clear demand, the Rule of Three suggests waiting until a third instance before refactoring for reuse.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.