Design Principles - Java

Design Principles - Java

Design principles are fundamental guidelines and best practices that help software developers create well-structured, maintainable, and efficient software systems. These principles guide the design and architecture of software, making it easier to understand, extend, and adapt.

1. SOLID Principles:

  • Single Responsibility Principle (SRP): A class should have only one reason to change. Advantages: Improved maintainability, easier debugging, and reduced code coupling.

   // Before SRP
class PaymentProcessor {
    public void processPayment(Payment payment) {
        //processing the payment
    }
    
    public void generatePaymentReceipt(Payment payment) {
        // generating a payment receipt
    }
}

// After SRP
class PaymentProcessor {
    public void processPayment(Payment payment) {
        // processing the payment
    }
}

class ReceiptGenerator {
    public void generatePaymentReceipt(Payment payment) {
        // generating a payment receipt
    }
}        

  • Open-Closed Principle (OCP): Software entities (classes, modules, functions) should be open for extension but closed for modification. Advantages: Facilitates code reuse, minimizes risk of introducing new bugs, and supports a modular design.

// Before OCP
class PaymentValidator {
    public boolean validatePayment(Payment payment) {
        // validating the payment
    }
}

// After OCP
interface PaymentValidator {
    boolean validatePayment(Payment payment);
}

class CreditCardPaymentValidator implements PaymentValidator {
    @Override
    public boolean validatePayment(Payment payment) {
        // validating credit card payments
    }
}

class PayPalPaymentValidator implements PaymentValidator {
    @Override
    public boolean validatePayment(Payment payment) {
        //  validating PayPal payments
    }
}        

  • Liskov Substitution Principle (LSP): Objects of a superclass should be replaceable with objects of a subclass without affecting program correctness. Advantages: Enhances polymorphism and promotes consistency in object-oriented design.

class PaymentProcessor {
    public void processPayment(Payment payment) {
        //  processing the payment
    }
}

class CreditCardPaymentProcessor extends PaymentProcessor {
    @Override
    public void processPayment(Payment payment) {
        //  credit card payment processing
    }
}

class PayPalPaymentProcessor extends PaymentProcessor {
    @Override
    public void processPayment(Payment payment) {
        //  PayPal payment processing
    }
}        

  • Interface Segregation Principle (ISP): Clients should not be forced to depend on interfaces they do not use. Advantages: Avoids unnecessary dependencies, leads to smaller, focused interfaces, and improves code maintainability.

interface OnlinePayment {
    void processOnlinePayment();
}

interface OfflinePayment {
    void processOfflinePayment();
}

class CreditCardPayment implements OnlinePayment {
    @Override
    public void processOnlinePayment() {
        //  processing credit card payment online
    }
}

class CashPayment implements OfflinePayment {
    @Override
    public void processOfflinePayment() {
        //  processing cash payment offline
    }
}        

  • Dependency Inversion Principle (DIP): High-level modules should not depend on low-level modules. Both should depend on abstractions. Advantages: Promotes decoupling, facilitates unit testing, and enables flexibility in choosing implementations.

class PaymentGateway {
    private PaymentProcessor processor;
    
    public PaymentGateway(PaymentProcessor processor) {
        this.processor = processor;
    }
    
    public void processPayment(Payment payment) {
        processor.processPayment(payment);
    }
}        

2. DRY (Don't Repeat Yourself):

  • Principle: Avoid duplicating code or logic in multiple places in your codebase. Advantages: Reduces code maintenance efforts, lowers the risk of inconsistencies, and improves code readability.Before

//Before applying DRY
class Payment {
    private double amount;
    private String paymentMethod;

    // others

    public boolean isValid() {
        if (amount <= 0) {
            System.out.println("Invalid payment amount.");
            return false;
        }

        if (paymentMethod == null || paymentMethod.isEmpty()) {
            System.out.println("Invalid payment method.");
            return false;
        }

        // Additional validation for this payment method
        if (paymentMethod.equals("CreditCard")) {
            // Credit card  validation
            if (!validateCreditCard()) {
                System.out.println("Credit card validation failed.");
                return false;
            }
        } else if (paymentMethod.equals("PayPal")) {
            // PayPal  validation
            if (!validatePayPal()) {
                System.out.println("PayPal validation failed.");
                return false;
            }
        }

        return true;
    }

    // Validation methods for specific payment methods
    private boolean validateCreditCard() {
        // Validation  for credit card payments
        return true;
    }

    private boolean validatePayPal() {
        // Validation  for PayPal payments
        return true;
    }
}

class PaymentProcessor {
    public void processPayment(Payment payment) {
        if (payment.isValid()) {
            //  processing the payment
            System.out.println("Payment processed successfully.");
        } else {
            System.out.println("Payment validation failed. Unable to process payment.");
        }
    }
}        

After Applying DRY

class Payment {
    private double amount;
    private String paymentMethod;

    // others

    public boolean isValid() {
        // Common payment validation  
        if (amount <= 0) {
            System.out.println("Invalid payment amount.");
            return false;
        }

        if (paymentMethod == null || paymentMethod.isEmpty()) {
            System.out.println("Invalid payment method.");
            return false;
        }

        return true;
    }
}

class PaymentProcessor {
    public void processPayment(Payment payment) {
        if (payment.isValid()) {
            // processing the payment
            System.out.println("Payment processed successfully.");
        } else {
            System.out.println("Payment validation failed. Unable to process payment.");
        }
    }
}        

3. KISS (Keep It Simple, Stupid):

  • Principle: Favor simplicity in design and implementation. Advantages: Easier maintenance, better code comprehension, and reduced risk of introducing bugs.Before KISS:In the absence of KISS, you might have complex and convoluted code that attempts to handle various edge cases and unnecessary complexities:

class ComplexPaymentProcessor {
    public void processPayment(Payment payment) {
        if (payment == null) {
            System.out.println("Invalid payment. Cannot process.");
            return;
        }

        if (payment.getAmount() <= 0) {
            System.out.println("Invalid payment amount.");
            return;
        }

        if (payment.getPaymentMethod() == null || payment.getPaymentMethod().isEmpty()) {
            System.out.println("Invalid payment method.");
            return;
        }

        // Complex payment processing logic involving multiple if-else statements, loops, and conditional checks.
        // Handling various edge cases and exceptions here.

        System.out.println("Payment processed successfully.");
    }
}        

After KISS:

With the KISS principle applied, you simplify the code and remove unnecessary complexities:

class SimplePaymentProcessor {
    public void processPayment(Payment payment) {
        if (payment == null) {
            System.out.println("Invalid payment. Cannot process.");
            return;
        }

        if (!payment.isValid()) {
            System.out.println("Invalid payment.");
            return;
        }

        // Simplified payment processing  without unnecessary complexities.
        System.out.println("Payment processed successfully.");
    }
}        


4. YAGNI (You Ain't Gonna Need It):

  • Principle: Avoid adding functionality or complexity until it's necessary. Advantages: Prevents over-engineering, reduces development time, and keeps the codebase focused on current requirements.

Before Yagini

class PaymentProcessor {
    public void processPayment(Payment payment) {
        // Complex logic for payment processing
        if (payment.isValid()) {
            // Additional features that are not currently needed but were added "just in case."
            // These features introduce unnecessary complexity.
            if (payment.isFraudulent()) {
                System.out.println("Potential fraudulent payment detected.");
                // Take action to handle fraud (e.g., block payment).
            }

            if (payment.needsConfirmation()) {
                System.out.println("Payment confirmation required.");
                // Send confirmation request to the user.
            }

            // More unnecessary features and complexity...
        } else {
            System.out.println("Payment validation failed. Unable to process payment.");
        }
    }
}        

After Applying Yagini

class SimplePaymentProcessor {
    public void processPayment(Payment payment) {
        if (payment == null || !payment.isValid()) {
            System.out.println("Invalid payment. Cannot process.");
            return;
        }

        // Simplified payment processing  without adding unnecessary features.
        System.out.println("Payment processed successfully.");
    }
}        

5. Composition Over Inheritance:

  • Principle: Prefer building complex objects or behaviors by composing simpler, reusable components rather than relying solely on inheritance. Advantages: Promotes code reusability, flexibility, and avoids tight coupling.

Before Composition Over Inheritance:

In situations where inheritance is used excessively, you might end up with a complex inheritance hierarchy that becomes challenging to manage:

class Payment {
    // Common payment properties and methods
}

class CreditCardPayment extends Payment {
    // Credit card payment-specific properties and methods
}

class PayPalPayment extends Payment {
    // PayPal payment-specific properties and methods
}

class BitcoinPayment extends Payment {
    // Bitcoin payment-specific properties and methods
}        

After Composition Over Inheritance:

With the "Composition Over Inheritance" principle applied, you can simplify the design using composition:

class Payment {
    // Common payment properties and methods
}

class PaymentMethod {
    private Payment payment;
    
    public PaymentMethod(Payment payment) {
        this.payment = payment;
    }
    
    public void processPayment() {
        //  processing payment using the contained Payment instance
    }
    
    // Additional methods related to payment methods...
}
/* In this "after" example, you create a PaymentMethod class that contains an instance of the Payment class. Instead of relying on an extensive inheritance hierarchy, you use composition to represent different payment methods. Each payment method can have its own logic encapsulated within its processPayment method, and you can add more payment methods without altering the existing code. */        

6. Separation of Concerns (SoC):

  • Principle: Divide your system into distinct, independent modules or components, each addressing a specific concern or responsibility. Advantages: Enhances maintainability, promotes code organization, and allows for parallel development.Befor SoC

class PaymentProcessor {
    public void processPayment(Payment payment) {
        // Payment validation
        if (payment == null || !payment.isValid()) {
            System.out.println("Invalid payment. Cannot process.");
            return;
        }

        // Payment processing
        try {
            // payment processing  
            System.out.println("Payment processed successfully.");
        } catch (PaymentException e) {
            System.out.println("Payment processing failed: " + e.getMessage());
        }
    }
}        

After SOC

class PaymentValidator {
    public boolean validatePayment(Payment payment) {
        if (payment == null || !payment.isValid()) {
            return false;
        }
        return true;
    }
}

class PaymentProcessor {
    public void processPayment(Payment payment) {
        PaymentValidator validator = new PaymentValidator();
        
        if (!validator.validatePayment(payment)) {
            System.out.println("Invalid payment. Cannot process.");
            return;
        }

        // Payment processing
        try {
            // payment processing  
            System.out.println("Payment processed successfully.");
        } catch (PaymentException e) {
            System.out.println("Payment processing failed: " + e.getMessage());
        }
    }
}        

7. Law of Demeter (LoD) - "Don't Talk to Strangers":

  • Principle: A module should only communicate with its immediate dependencies and not with the dependencies of its dependencies. Advantages: Reduces coupling between components, makes code more robust to changes in dependencies, and improves testability.Before LOD

class PaymentProcessor {
    public void processPayment(Customer customer, Order order) {
        // Tight coupling: Accessing properties of nested objects
        String customerName = customer.getName();
        String orderStatus = order.getStatus();

        // Complex payment processing  using customerName and orderStatus
        // ...
    }
}        

After LOD

class Customer {
    private String name;

    public String getName() {
        return name;
    }
}

class Order {
    private String status;

    public String getStatus() {
        return status;
    }
}

class PaymentProcessor {
    public void processPayment(Customer customer, Order order) {
        // Reduced coupling: Accessing properties via methods
        String customerName = customer.getName();
        String orderStatus = order.getStatus();

        // Simplified payment processing  using customerName and orderStatus
        // ...
    }
}        

8. Fail-Fast Principle:

  • Principle: Detect and report errors as soon as they occur, rather than allowing them to propagate through the system. Advantages: Faster bug identification and debugging, leading to more reliable software.

class PaymentProcessor {
    public void processPayment(Payment payment) {
        // Payment validation
        if (payment == null || !payment.isValid()) {
            // Continue processing even though payment is invalid
            System.out.println("Payment validation failed, but processing continues.");
        }

        // Payment processing  
        try {
            // Complex payment processing  
            System.out.println("Payment processed successfully.");
        } catch (PaymentException e) {
            System.out.println("Payment processing failed: " + e.getMessage());
        }
    }
}        

After Applying Principle

class PaymentProcessor {
    public void processPayment(Payment payment) {
        // Payment validation
        if (payment == null || !payment.isValid()) {
            System.out.println("Payment validation failed. Cannot process.");
            return; // Fail-fast: Stop processing immediately
        }

        // Payment processing  
        try {
            // Complex payment processing  
            System.out.println("Payment processed successfully.");
        } catch (PaymentException e) {
            System.out.println("Payment processing failed: " + e.getMessage());
        }
    }
}        


Well written article RamiReddy P. Insightful and also highly beneficial for junior engineers looking to expand their knowledge.

To view or add a comment, sign in

More articles by RamiReddy P.

  • Monoliths are under rated!

    Despite the rise of microservices and other distributed architectures, monolithic architecture is still a good choice…

    3 Comments
  • AWS Lambda Best Practices

    Lambda pricing is calculated as a combination of: 1. Total number of requests 2.

  • Exploring Java's Modern Features: From Java 7 to Java 17

    Here is the features matrix that provides information about the availability of features starting from a specific…

Others also viewed

Explore content categories