What are object oriented design principles?

What are object oriented design principles?

January 7, 2025

sweinfo-dump

Object oriented design principles are guidelines that were formulated to create clean, maintainable, and secure code that is easy to understand owing to a standardized structure. These solve common problems and define optimal approaches to different scenarios we might come across while development.

We have the following types:

  • creational patterns: Focus on the creation of objects.
  • structural patterns: Deal with object composition and relationships.
  • behavioral patterns: Define how objects interact and communicate.

In this blogpost i want to discuss 10 common/important patterns.

creational patterns

singleton pattern

a singleton is a class that can be instantiated only once.

an example for this could be

public class Singleton {
	private static Singleton instance;
	public static int sharedCounter = 0;
	private Singleton {
		// prevents instantiation from other classes
	}
	public static getInstance() {
		if (instance == null) {
			synchronized (Singleton.class) {
				if (instance == null) {
					instance = new Singleton();
				}
			}
		}
		return instance;
	} 

	public synchronized void incrementCounter() { 
		sharedCounter++; 
	}
}

public class Main { 
	public static void main(String[] args) {
		Thread thread1 = new Thread (() -> {
			Singleton s1 = Singleton.getInstance();
			s1.incrementCounter();
			System.out.println("thread 1 increments to:" + Singleton.sharedCounter);
		});

		Thread thread2 = new Thread (() -> {
			Singleton s2 = Singleton.getInstance();
			s2.incrementCounter();
			System.out.println("thread 2 increments to:" + Singleton.sharedCounter);
		});

		thread1.start(); 
		thread2.start();
	}
}

/**
 * Main when run will print the following:
 * thread 1 increments to: 1 
 * thread 2 increments to: 2
 */
  • class has a static instance. will hold only one instance
  • private constructor prevents creation of object with new keyword
  • we have a static counter, manipulating which reflects all references to the singleton object
  • there is a double-checked locking mechanism to ensure that only one instance is created, even in multithreaded environments.
  • synchronization ensures that only one thread can initialize the instance if multiple threads call getInstance() simultaneously.

prototype pattern

a prototype pattern is an alternate way to implement inheritance, instead of inheriting functionality from a class, it comes from an object thats already been created.

used when creating an object is expensive or time-consuming. Instead of creating a new object from scratch, the Prototype Pattern allows you to copy or clone an existing object, creating a duplicate that can be modified if needed.

an example could be

public interface Prototype {
	Prototype clone();
}

public class ConcretePrototype implements Prototype {
	private String name;
	public ConcretePrototype(String name) {
		this.name = name;
	}

	@Override
	pubilc Prototype clone () {
		return new ConcretePrototype(this.name);
	}
	
	public String getName() { 
		return name; 
	} 
	public void setName(String name) { 
		this.name = name; 
	}
}

public class Main { 
	public static void main(String[] args) {
		ConcretePrototype original = new ConcretePrototype("Original");
		ConcretePrototype clone = (ConcretePrototype) original.clone(); 
		clone.setName("Clone");

		System.out.println("Original Object: " + original.getName());
		System.out.println("Cloned Object: " + clone.getName());
	}
}

/*
 * Main when run will print the following:
 * Original Object: Original
 * Cloned Object: Clone
 */
  • prototype interface defines a clone() method that must be implemented by any class that wants to use the prototype pattern
  • concrete prototype implements the clone() method to create a copy of its class

builder pattern

in the builder pattern we create an object step by step with methods as opposed to with a constructor. it separates the construction process of an object from its representation, allowing the same construction process to create different representations.

an example could be

public class Computer {
	private String processor;
	private int ram;
	private int storage;

	private Computer (Builder builder) {
		this.processor = builder.processor;
		this.ram = builder.ram;
		this.storage = builder.storage;
	}

	public static class Builder {
		private String processor;
		private int ram;
		private int storage;

		public Builder setProcessor(String processor) {
			this.processor = processor;
			return this;
		}

		public Builder setRam(int ram) { 
			this.ram = ram; 
			return this; 
		} 

		public Builder setStorage(int storage) { 
			this.storage = storage; 
			return this; 
		}
	
		public Computer build() { 
			return new Computer(this); 
		}
	}
}

public class Main {
    public static void main(String[] args) {
        Computer gamingPC = new Computer.Builder()
                .setProcessor("Intel Core i9")
                .setRam(32)
                .setStorage(1024)
                .build();

        Computer officePC = new Computer.Builder()
                .setProcessor("Intel Core i5")
                .setRam(16)
                .setStorage(512)
                .build();

        // Print the created objects
        System.out.println("Gaming PC: " + gamingPC);
        System.out.println("Office PC: " + officePC);
    }
}

/*
 * Main when run will have follwing attributes
 * gamingPC: Processor=Intel Core i9, RAM=32GB, Storage=1024GB
 * officePC: Processor=Intel Core i5, RAM=16GB, Storage=512GB
 */
  • primarily used when an object has many attributes, and creating a constructor for each configuration or permutation is impractical

factory pattern

provides an interface for creating objects in a super class but allows subclasses to alter the type of objects that will be created. It delegates the responsibility of instantiating objects to factory classes.

we have the following types

  1. Simple factory
  2. Factory method
  3. Abstract factory

an example could be

public interface Shape {
    void draw();
}

public class Circle implements Shape {
    @Override
    public void draw() {
        System.out.println("Drawing a Circle.");
    }
}

public class Rectangle implements Shape {
    @Override
    public void draw() {
        System.out.println("Drawing a Rectangle.");
    }
}

public abstract class ShapeFactory {
    public abstract Shape createShape();
}

public class CircleFactory extends ShapeFactory {
    @Override
    public Shape createShape() {
        return new Circle();
    }
}

public class RectangleFactory extends ShapeFactory {
    @Override
    public Shape createShape() {
        return new Rectangle();
    }
}

public class Main {
    public static void main(String[] args) {
        ShapeFactory circleFactory = new CircleFactory();
        Shape circle = circleFactory.createShape();
        circle.draw();

        ShapeFactory rectangleFactory = new RectangleFactory();
        Shape rectangle = rectangleFactory.createShape();
        rectangle.draw();
    }
}
  • use when the exact type of object to create is determined at runtime.
  • use when the instantiation logic is complex and requires abstraction.
  • use when you want to centralize object creation to maintain consistency.

structural patterns

facade pattern

a facade is pattern that provides a simplified interface to a larger body of code, such as a complex subsystem or library. It hides the complexities of the system by providing a unified, high-level interface for easier access.

an example could be

// low level subsystems
public class CPU {
    public void start() {
        System.out.println("CPU started.");
    }

    public void execute() {
        System.out.println("CPU executing instructions.");
    }

    public void shutdown() {
        System.out.println("CPU shut down.");
    }
}

public class Memory {
    public void load() {
        System.out.println("Memory loaded.");
    }
}

public class HardDrive {
    public void read() {
        System.out.println("HardDrive read operation.");
    }
}

// facade class
public class ComputerFacade {
    private CPU cpu;
    private Memory memory;
    private HardDrive hardDrive;

    public ComputerFacade() {
        this.cpu = new CPU();
        this.memory = new Memory();
        this.hardDrive = new HardDrive();
    }

    public void startComputer() {
        System.out.println("Starting the computer...");
        cpu.start();
        memory.load();
        hardDrive.read();
        cpu.execute();
        System.out.println("Computer started successfully.");
    }

    public void shutdownComputer() {
        System.out.println("Shutting down the computer...");
        cpu.shutdown();
        System.out.println("Computer shut down successfully.");
    }
}

// client access
public class Main {
    public static void main(String[] args) {
        ComputerFacade computer = new ComputerFacade();

        computer.startComputer();

        System.out.println();

        computer.shutdownComputer();
    }
}

/*
 * Main when run will have follwing output
 * Starting the computer...
 * CPU started.
 * Memory loaded.
 * HardDrive read operation.
 * CPU executing instructions.
 * Computer started successfully.
 * 
 * Shutting down the computer...
 * CPU shut down.
 * Computer shut down successfully.*/
  • the subsystem consists of internal complexity
  • facade provides a simplified interface to interact with the subsystem
  • client has access to Facade via Main
  • Provides a simple interface to access complex subsystems so client doesn't interact with the subsystem directly, promoting loose coupling.

proxy pattern

proxy provides a surrogate or placeholder for another object to control access to it. The proxy object acts as an intermediary between the user and the actual object.

there are multiple types

  1. Virtual: controls access to a resource that is expensive to create or load, such as a large file or a database connection.
  2. Protection: controls access to an object by enforcing permissions.
  3. Remote: represents an object that is located on a remote server.
  4. Smart: adds additional functionality like reference counting, logging, or caching.

an example could be

public interface ExpensiveObject {
    void process();
}

public class ExpensiveObjectImpl implements ExpensiveObject {
    public ExpensiveObjectImpl() {
        heavyInitialization();
    }

    @Override
    public void process() {
        System.out.println("Processing completed.");
    }

    private void heavyInitialization() {
        System.out.println("Loading resources for ExpensiveObject...");
    }
}

public class ExpensiveObjectProxy implements ExpensiveObject {
    private ExpensiveObjectImpl expensiveObject;

    @Override
    public void process() {
        if (expensiveObject == null) {
            expensiveObject = new ExpensiveObjectImpl(); // Lazy initialization
        }
        expensiveObject.process();
    }
}

public class Main {
    public static void main(String[] args) {
        ExpensiveObject obj = new ExpensiveObjectProxy();

        // First call triggers the initialization
        System.out.println("First call:");
        obj.process();

        // Second call uses the already initialized object
        System.out.println("\nSecond call:");
        obj.process();
    }
}

/*
 * Main when run yields the following output
 * First call:
 * Loading resources for ExpensiveObject...
 * Processing completed.
 * 
 * Second call:
 * Processing completed.
 */
  • delays expensive operations until they are actually neede
  • restricts or manages access to the real object

behavioural patterns

iterator pattern

allows to traverse through a collection of objects based on some arbitrary logic

iterator defines interface for accessing and traversing elements of a collection

an example could be

public interface Iterator<T> {
    boolean hasNext();
    T next();
}

public interface IterableCollection<T> {
    Iterator<T> createIterator();
}

public class ListIterator<T> implements Iterator<T> {
    private List<T> items;
    private int position = 0;

    public ListIterator(List<T> items) {
        this.items = items;
    }

    @Override
    public boolean hasNext() {
        return position < items.size();
    }

    @Override
    public T next() {
        if (!hasNext()) {
            throw new IllegalStateException("No more elements");
        }
        return items.get(position++);
    }
}

import java.util.ArrayList;
import java.util.List;

public class NameCollection implements IterableCollection<String> {
    private List<String> names = new ArrayList<>();

    public void addName(String name) {
        names.add(name);
    }

    @Override
    public Iterator<String> createIterator() {
        return new ListIterator<>(names);
    }
}

public class Main {
    public static void main(String[] args) {
        NameCollection nameCollection = new NameCollection();
        nameCollection.addName("Alice");
        nameCollection.addName("Bob");
        nameCollection.addName("Charlie");

        Iterator<String> iterator = nameCollection.createIterator();

        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }
}

/*
 * Main when run gives the following output
 * Alice 
 * Bob
 * Charlie
 */
  • NameCollection holds the collection of items and provides a method to create an iterator
  • ListIterator implements logic for traversing
  • a user can use these to traverse through their objects.

observer pattern

an observer establishes a one-to-many dependency between objects. When the subject object changes state, all its dependent/observer objects are notified and updated automatically.

an example could be

public interface Subject { 
	void attach(Observer observer); 
	void detach(Observer observer); 
	void notifyObservers(); 
}

public interface Observer {
    void update(String message);
}

public class NewsAgency implements Subject {
    private List<Observer> observers = new ArrayList<>();
    private String latestNews;

    @Override
    public void attach(Observer observer) {
        observers.add(observer);
    }

    @Override
    public void detach(Observer observer) {
        observers.remove(observer);
    }

    @Override
    public void notifyObservers() {
        for (Observer observer : observers) {
            observer.update(latestNews);
        }
    }

    public void setNews(String news) {
        this.latestNews = news;
        notifyObservers();
    }
}

public class NewsSubscriber implements Observer {
    private String name;

    public NewsSubscriber(String name) {
        this.name = name;
    }

    @Override
    public void update(String message) {
        System.out.println(name + " received news update: " + message);
    }
}

public class Main {
    public static void main(String[] args) {
        NewsAgency agency = new NewsAgency();

        // Create observers
        NewsSubscriber subscriber1 = new NewsSubscriber("Alice");
        NewsSubscriber subscriber2 = new NewsSubscriber("Bob");

        // Attach observers
        agency.attach(subscriber1);
        agency.attach(subscriber2);

        // Publish news
        agency.setNews("Breaking News: Observer Pattern in Action!");

        // Detach one observer and publish more news
        agency.detach(subscriber1);
        agency.setNews("Another Update: Alice won't receive this.");
    }
}
  • subject maintains a list of observers and provides methods to attach/detach them, notifies all observers when state changes.
  • observer implements the update() method to react to changes in the subject.
  • a user can create a multiple subjects and observers and attach the observers to the subject and triggers updates by changing the subject’s state.
  • this is similar to pub-sub but not exactly like it as there are key differences

mediator pattern

defines a pattern to encapsulate the way objects interact with one another. It promotes loose coupling by preventing objects from referring to each other explicitly and allows their interaction to be centralized.

an example could be

public interface Mediator {
    void sendMessage(String message, Colleague colleague);
}

public abstract class Colleague {
    protected Mediator mediator;

    public Colleague(Mediator mediator) {
        this.mediator = mediator;
    }

    public abstract void receive(String message);
}

public class User extends Colleague {
    private String name;

    public User(Mediator mediator, String name) {
        super(mediator);
        this.name = name;
    }

    public void send(String message) {
        System.out.println(this.name + " sends: " + message);
        mediator.sendMessage(message, this);
    }

    @Override
    public void receive(String message) {
        System.out.println(this.name + " receives: " + message);
    }
}

import java.util.ArrayList;
import java.util.List;

public class ChatMediator implements Mediator {
    private List<Colleague> colleagues;

    public ChatMediator() {
        this.colleagues = new ArrayList<>();
    }

    public void addColleague(Colleague colleague) {
        colleagues.add(colleague);
    }

    @Override
    public void sendMessage(String message, Colleague sender) {
        for (Colleague colleague : colleagues) {
            if (colleague != sender) { // Prevent the sender from receiving their own message
                colleague.receive(message);
            }
        }
    }
}

public class Main {
    public static void main(String[] args) {
        // Create the mediator
        ChatMediator mediator = new ChatMediator();

        // Create colleagues
        User user1 = new User(mediator, "Alice");
        User user2 = new User(mediator, "Bob");
        User user3 = new User(mediator, "Charlie");

        // Add colleagues to the mediator
        mediator.addColleague(user1);
        mediator.addColleague(user2);
        mediator.addColleague(user3);

        // Users send messages
        user1.send("Hi everyone!");
        user2.send("Hello Alice!");
    }
}

/*
Main when run gives the following output

 * Alice sends: Hi everyone!
 * Bob receives: Hi everyone!
 * Charlie receives: Hi everyone!
 * Bob sends: Hello Alice!
 * Alice receives: Hello Alice!
 * Charlie receives: Hello Alice!
 */
  • mediator manages communication between multiple Colleague objects.
  • centralizes the interaction logic, making the system easier to modify and extend.
  • colleagues rely on the mediator for communication instead of interacting with each other directly.
  • user creates the mediator and colleagues.

state pattern

state pattern allows an object to alter its behavior when its internal state changes. The pattern encapsulates state-specific behavior into separate classes, making it easy to add new states or change existing ones without modifying the main object.

an example could be

public interface State {
    void handle(Context context);
}

public class StateA implements State {
    @Override
    public void handle(Context context) {
        System.out.println("StateA handling request. Switching to StateB.");
        context.setState(new StateB());
    }
}

public class StateB implements State {
    @Override
    public void handle(Context context) {
        System.out.println("StateB handling request. Switching to StateA.");
        context.setState(new StateA());
    }
}

public class Context {
    private State currentState;

    public Context(State initialState) {
        this.currentState = initialState;
    }

    public void setState(State state) {
        this.currentState = state;
    }

    public void request() {
        currentState.handle(this);
    }
}

public class Main {
    public static void main(String[] args) {
        State initialState = new StateA();

        Context context = new Context(initialState);

        context.request();
        context.request();
        context.request();
    }
}

/*
 * Main when run gives the following ouput
 * StateA handling request. Switching to StateB.
 * StateB handling request. Switching to StateA.
 * StateA handling request. Switching to StateB.
 */
  • state interface defines the method (handle) that each state must implement.
  • state classes implement the behavior specific to each state and determine the next state.
  • context holds a reference to the current state.
  • delegates the behavior to the current state and facilitates state transitions.

hi pss you should consider subscribing to my blog. click here to do so.