Java Design Principles and Patterns



1. Encapsulation

Encapsulation is a fundamental principle of Object-Oriented Programming (OOP) that involves bundling the data (fields) and methods (functions) that operate on the data into a single unit or class. It restricts direct access to some components and helps in achieving a modular and secure structure.

  • Private Access Modifier: Declares fields as private, ensuring they can only be accessed through methods.
  • Getter and Setter Methods: Provides controlled access to private fields:
    
    class Person {
        private String name;
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    }
                    

2. Interfaces

Interfaces define a contract or blueprint for classes, specifying methods that must be implemented. They play a crucial role in achieving abstraction and polymorphism in Java.

  • Declaring Interfaces: Interfaces are declared using the interface keyword.
  • Implementing Interfaces: Classes use the implements keyword to provide concrete implementations.
  • Extending Interfaces: Interfaces can extend other interfaces to build complex contracts.

Example:


interface Animal {
    void sound();
}

class Dog implements Animal {
    public void sound() {
        System.out.println("Woof!");
    }
}
        

3. Object Composition vs Inheritance

Java supports both inheritance and composition as ways to achieve code reuse. Understanding their differences helps make better design decisions:

  • Inheritance: Represents an "is-a" relationship, such as Dog is an Animal. It should be used when classes share a significant amount of common behavior.
  • Composition: Represents a "has-a" relationship, such as Car has an Engine. It is more flexible than inheritance.

Example of Composition:


class Engine {
    void start() {
        System.out.println("Engine starts");
    }
}

class Car {
    private Engine engine = new Engine();

    void drive() {
        engine.start();
        System.out.println("Car is driving");
    }
}
        

4. Polymorphism

Polymorphism allows methods to take many forms, enabling flexibility and reusability in code. It can be achieved through method overriding and interfaces.

  • Automatic Casting: The runtime determines the correct method to call based on the object type.
  • Explicit Casting: Used when converting a superclass reference back to a subclass.
  • Compile-time vs Runtime Errors: Ensures the program avoids type mismatches during runtime.

Example:


class Animal {
    void sound() { System.out.println("Some sound"); }
}

class Cat extends Animal {
    @Override
    void sound() { System.out.println("Meow"); }
}

Animal a = new Cat();
a.sound(); // Outputs: Meow
        

5. Lambda Expressions

Lambdas were introduced in Java 8 to provide a concise way of writing anonymous functions. They simplify functional programming in Java.

  • Syntax Options: (parameters) -> { body }
  • Usage in Java 8: Commonly used with the Stream API and functional interfaces like Runnable, Comparator, and Predicate.

Example:


List names = Arrays.asList("Alice", "Bob", "Charlie");
names.forEach(name -> System.out.println(name));
        

6. Design Principles

Design principles are guidelines that help developers create scalable and maintainable code.

  • Established Ideas: Concepts like DRY (Don't Repeat Yourself) and SOLID principles.
  • General Solutions: Avoid hardcoding and aim for modular code design.

7. Creational Patterns

Creational patterns deal with object creation mechanisms to promote flexibility and reuse.

  • Singleton Pattern: Ensures only one instance of a class is created during runtime.
  • Immutable Object Pattern: Ensures an object's state cannot be changed after its creation.

Example of Singleton:


class Singleton {
    private static Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}
        

Example of Immutable Object Pattern:


// Immutable class
public final class Person {
    private final String name;
    private final int age;

    // Constructor to initialize fields
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // Getter methods
    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + '}';
    }

    public static void main(String[] args) {
        // Creating an immutable object
        Person person = new Person("Alice", 25);

        // Trying to modify the object's state
        System.out.println(person); // Output: Person{name='Alice', age=25}

        // person.name = "Bob"; // Compilation Error: Field is final
        // person.age = 30;    // Compilation Error: Field is final

        // Object remains unchanged
        System.out.println(person); // Output: Person{name='Alice', age=25}
    }
}

        

Post a Comment

0 Comments