Index

DRY, KISS, YAGNI, and Other Principles

Java Object-Oriented Design

12.1 Avoiding Code Duplication (DRY)

One of the most fundamental principles in software engineering is DRY, which stands for Don't Repeat Yourself. Coined by Andy Hunt and Dave Thomas in The Pragmatic Programmer, the DRY principle emphasizes that "every piece of knowledge must have a single, unambiguous, authoritative representation within a system." In simpler terms, avoid code duplication—not just in lines of code, but in logic, knowledge, and behavior.

Why Duplication Is Harmful

Duplication increases the risk of bugs, bloated codebases, and inconsistent behavior. If the same logic is copied across multiple classes or methods, any change to that logic must be applied consistently in all places. Miss just one, and you’ve introduced a potential bug. Duplication also makes code harder to read, maintain, and extend.

For example, consider the following duplicate code across two methods:

public double calculateDiscountedPrice(double price) {
    double discount = price * 0.1;
    return price - discount;
}

public double calculateLoyaltyPrice(double price) {
    double discount = price * 0.1;
    return price - discount;
}

Both methods apply the same discount logic. If business rules change tomorrow (e.g., the discount becomes 15%), both methods would need to be updated. This is inefficient and error-prone.

Common Causes of Duplication

Duplication can sneak into codebases through:

In object-oriented systems, it's especially common to find the same method logic implemented in multiple classes due to improper use of inheritance or composition.

Refactoring to Eliminate Duplication

There are several strategies to remove duplication effectively:

Extract Common Logic into Methods

Refactor repeated logic into a reusable method:

private double applyDiscount(double price) {
    return price - (price * 0.1);
}

public double calculateDiscountedPrice(double price) {
    return applyDiscount(price);
}

public double calculateLoyaltyPrice(double price) {
    return applyDiscount(price);
}

Use Inheritance for Shared Behavior

If multiple classes share behavior, move it to a superclass:

public class Product {
    protected double applyDiscount(double price) {
        return price - (price * 0.1);
    }
}

public class Electronics extends Product {
    public double getPriceAfterDiscount(double price) {
        return applyDiscount(price);
    }
}

public class Clothing extends Product {
    public double getPriceAfterDiscount(double price) {
        return applyDiscount(price);
    }
}

Inheritance helps promote reuse, but use it judiciously to avoid the fragile base class problem.

Prefer Composition for Reusable Logic

When behavior doesn’t belong to a hierarchy, create a helper or utility class:

public class DiscountCalculator {
    public double applyDiscount(double price) {
        return price - (price * 0.1);
    }
}

public class Order {
    private DiscountCalculator calculator = new DiscountCalculator();

    public double getFinalPrice(double price) {
        return calculator.applyDiscount(price);
    }
}

Composition allows you to share behavior across unrelated classes without tying them into a rigid inheritance chain.

When DRY Can Go Too Far

While DRY is important, overzealous abstraction can lead to confusion. Sometimes what looks like duplication is actually similar but contextually distinct logic. If you try to unify two seemingly duplicated methods that serve different purposes, the resulting abstraction may be awkward, harder to maintain, and violate other principles like the Single Responsibility Principle (SRP).

Rule of Thumb: Only refactor duplication when the logic and intent are truly the same and likely to evolve together.

For example, trying to generalize two discount formulas that behave differently based on customer type may obscure business logic.

Summary

The DRY principle helps build cleaner, more maintainable Java code by reducing redundancy and centralizing logic. By identifying duplicated code early—whether through manual review or static analysis tools—you can refactor into smaller, reusable components using methods, inheritance, or composition. However, always balance DRY with clarity and intent. A smart developer knows not only how to eliminate duplication but also when to leave it alone.

Reflective Tip: As you review your codebase, ask: If this logic changes, how many places do I have to update? If the answer is more than one, you likely have a DRY violation.

Index

12.2 Keeping It Simple and Modular (KISS)

In software design, simplicity is not a luxury—it's a necessity. The KISS principle, which stands for Keep It Simple, Stupid, is a timeless design philosophy that encourages developers to avoid unnecessary complexity. The core idea is that most systems work best when they are kept simple rather than made complicated. Therefore, simplicity should be a primary goal in design, and unnecessary complexity should be avoided.

Why Simplicity Leads to Maintainability

Simple code is easier to read, understand, test, and maintain. It reduces the cognitive load on developers who must work with the code weeks, months, or years after it was written. Complex code, on the other hand, can lead to bugs, misunderstandings, and costly rework.

Take the following two Java methods for checking if a number is even:

Overengineered version:

public boolean isEven(int number) {
    return ((number / 2) * 2 == number) ? true : false;
}

Simple version:

public boolean isEven(int number) {
    return number % 2 == 0;
}

Both methods perform the same task, but the latter is cleaner and more intuitive. The complex version adds no value—only confusion.

Modular Design: Simplicity through Separation

Keeping it simple often goes hand-in-hand with modular design. Modular code divides responsibilities among small, focused classes that communicate through clear interfaces. This separation of concerns makes code easier to understand and evolve.

Consider a payment processing system. A non-modular design might cram all logic into a single class:

public class PaymentProcessor {
    public void processPayment(String method, double amount) {
        if (method.equals("credit")) {
            // credit payment logic
        } else if (method.equals("paypal")) {
            // PayPal logic
        } else if (method.equals("bank")) {
            // bank transfer logic
        }
    }
}

This class is hard to test, extend, or maintain. Now compare it to a modular version:

public interface PaymentMethod {
    void pay(double amount);
}

public class CreditCardPayment implements PaymentMethod {
    public void pay(double amount) {
        // credit card logic
    }
}

public class PayPalPayment implements PaymentMethod {
    public void pay(double amount) {
        // PayPal logic
    }
}

public class PaymentProcessor {
    public void process(PaymentMethod method, double amount) {
        method.pay(amount);
    }
}

This design is simpler because it's modular. Each class has one responsibility, and new payment methods can be added without changing existing code.

Simplicity vs Functionality: Finding the Balance

Keeping things simple doesn’t mean stripping out essential functionality or avoiding abstraction. Simplicity means writing the most direct, clear, and logical solution to a problem. Over-abstraction—adding unnecessary design layers or generalization—can violate KISS.

For instance, wrapping a single method call in five classes of delegation adds complexity without benefit. Likewise, introducing a full-blown strategy pattern for toggling a boolean flag is often overkill. Use patterns and principles wisely, not dogmatically.

Best Practices for KISS and Modularity

Conclusion

The KISS principle reminds us that clarity is better than cleverness. By choosing straightforward solutions and designing modular systems, developers produce code that is easier to understand, test, and evolve. In Java and object-oriented design, this often means composing systems with clear interfaces, small focused classes, and predictable behaviors. Simplicity doesn't mean less power—it means more control and reliability in the long run.

Thought Prompt: When you write code, ask yourself: “Would another developer understand this in 30 seconds?” If the answer is no, it's time to simplify.

Index

12.3 Avoiding Premature Optimization (YAGNI)

In software development, it’s tempting to think ahead—to imagine every possible feature, every potential performance bottleneck, and to begin writing code for situations that may never happen. But a key principle in agile, pragmatic programming is YAGNI, which stands for “You Aren’t Gonna Need It.”

What Is YAGNI?

YAGNI is a design principle that reminds developers not to implement something until it is actually necessary. It was popularized in the context of agile development and extreme programming (XP), where quick iteration, customer feedback, and adaptability are core values. YAGNI encourages developers to resist the urge to build features or abstractions “just in case.”

Definition: "You Aren’t Gonna Need It" means don’t add functionality until it is absolutely required.

The principle advocates for focusing on what the system needs right now, not what it might need later.

The Cost of Premature Optimization

One of the most common violations of YAGNI occurs during premature optimization—when developers try to improve performance or scalability before there's any evidence that it’s needed.

Consider the classic advice from Donald Knuth:

“Premature optimization is the root of all evil.”

Why is this a problem? Because early optimization often introduces:

Let’s look at a simple example:

Example of Premature Optimization

Suppose you’re writing a feature to store and retrieve user comments. A basic, clear implementation might store comments in a List<String>.

List<String> comments = new ArrayList<>();
comments.add("Nice article!");

But you preemptively decide that users might leave millions of comments, and build a caching system, a database connection pool, and asynchronous message queues. This not only bloats your codebase but also delays delivery and introduces new points of failure—before any real performance problem exists.

Prioritize Simplicity and Correctness First

A better approach is to:

  1. Implement the simplest solution that works correctly.
  2. Measure performance under realistic loads.
  3. Optimize only when benchmarks show actual problems.

In the previous example, the simple ArrayList may be sufficient for months. If performance issues arise later, profiling tools can help identify exactly where the bottlenecks are—maybe it’s not the comment storage at all.

Strategies to Delay Optimization Wisely

Here are ways to follow YAGNI without sacrificing long-term quality:

For instance, if you're worried about slow sorting in large datasets, use Java’s built-in Collections.sort() for now. If the list grows and slows down the application, you can then refactor to a more performant data structure or algorithm—when it’s justified.

Conclusion

YAGNI is not about ignoring future concerns—it’s about not solving problems you don’t have yet. In modern Java development, it means prioritizing simplicity and correctness, especially during early iterations. When performance or complexity becomes a real concern, you’ll have the tools, code clarity, and context to optimize effectively.

Thought Prompt: The next time you consider adding a feature “just in case,” ask yourself: “Is this solving a real, immediate problem?” If not, you probably aren’t gonna need it—yet.

Index