Design patterns are proven solutions to common software design problems, facilitating code reusability, maintainability, and scalability. In the context of object-oriented programming (OOP), they provide a way to write code that others (or you in the future!) can easily understand and work with.
Design patterns in OOP are generally divided into three main categories:
Let’s break down each category with examples in Java.
The Singleton Pattern ensures that a class has only one instance and provides a global point of access to it. This is particularly useful when managing shared resources, like database connections.
Example:
public class Singleton { private static Singleton instance; private Singleton() { // private constructor to restrict instantiation } public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
Usage:
public class Main { public static void main(String[] args) { Singleton singleton = Singleton.getInstance(); // Use the singleton instance } }
The Factory Method Pattern defines an interface for creating an object but allows subclasses to alter the type of created objects. This pattern promotes loose coupling.
Example:
abstract class Vehicle { public abstract void drive(); } class Car extends Vehicle { public void drive() { System.out.println("Driving a car"); } } class Bike extends Vehicle { public void drive() { System.out.println("Riding a bike"); } } abstract class VehicleFactory { public abstract Vehicle createVehicle(); } class CarFactory extends VehicleFactory { public Vehicle createVehicle() { return new Car(); } } class BikeFactory extends VehicleFactory { public Vehicle createVehicle() { return new Bike(); } }
Usage:
public class Main { public static void main(String[] args) { VehicleFactory factory = new CarFactory(); Vehicle vehicle = factory.createVehicle(); vehicle.drive(); // Output: Driving a car } }
The Adapter Pattern allows incompatible interfaces to work together. It acts as a bridge between two incompatible interfaces.
Example:
interface MediaPlayer { void play(String audioType, String fileName); } interface AdvancedMediaPlayer { void playVlc(String fileName); void playMp4(String fileName); } class VlcPlayer implements AdvancedMediaPlayer { public void playVlc(String fileName) { System.out.println("Playing vlc file. Name: " + fileName); } public void playMp4(String fileName) {} } class Mp4Player implements AdvancedMediaPlayer { public void playVlc(String fileName) {} public void playMp4(String fileName) { System.out.println("Playing mp4 file. Name: " + fileName); } } class MediaAdapter implements MediaPlayer { AdvancedMediaPlayer advancedMediaPlayer; public MediaAdapter(String audioType) { if (audioType.equalsIgnoreCase("vlc")) { advancedMediaPlayer = new VlcPlayer(); } else if (audioType.equalsIgnoreCase("mp4")) { advancedMediaPlayer = new Mp4Player(); } } public void play(String audioType, String fileName) { if (audioType.equalsIgnoreCase("vlc")) { advancedMediaPlayer.playVlc(fileName); } else if (audioType.equalsIgnoreCase("mp4")) { advancedMediaPlayer.playMp4(fileName); } } }
Usage:
public class Main { public static void main(String[] args) { MediaPlayer player = new MediaAdapter("vlc"); player.play("vlc", "song.vlc"); } }
The Observer Pattern defines a one-to-many dependency between objects, allowing one object (the subject) to notify multiple observers about changes in its state.
Example:
import java.util.ArrayList; import java.util.List; interface Observer { void update(String message); } class ConcreteObserver implements Observer { private String name; public ConcreteObserver(String name) { this.name = name; } public void update(String message) { System.out.println(name + " received: " + message); } } class Subject { private List<Observer> observers = new ArrayList<>(); public void attach(Observer observer) { observers.add(observer); } public void notify(String message) { for (Observer observer : observers) { observer.update(message); } } }
Usage:
public class Main { public static void main(String[] args) { Subject subject = new Subject(); Observer observer1 = new ConcreteObserver("Observer 1"); Observer observer2 = new ConcreteObserver("Observer 2"); subject.attach(observer1); subject.attach(observer2); subject.notify("Hello Observers!"); } }
The Strategy Pattern allows for the algorithm to vary independently from the clients that use it. In simpler terms, it enables you to select an algorithm's behavior at runtime.
Example:
interface Strategy { void execute(); } class ConcreteStrategyA implements Strategy { public void execute() { System.out.println("Executing Strategy A"); } } class ConcreteStrategyB implements Strategy { public void execute() { System.out.println("Executing Strategy B"); } } class Context { private Strategy strategy; public void setStrategy(Strategy strategy) { this.strategy = strategy; } public void executeStrategy() { strategy.execute(); } }
Usage:
public class Main { public static void main(String[] args) { Context context = new Context(); context.setStrategy(new ConcreteStrategyA()); context.executeStrategy(); // Output: Executing Strategy A context.setStrategy(new ConcreteStrategyB()); context.executeStrategy(); // Output: Executing Strategy B } }
Since design patterns play a pivotal role in enhancing software design in Java, understanding and implementing these patterns will undoubtedly improve your coding practices. Remember that each pattern has its use case, and selecting the appropriate pattern can help streamline your development process.
As you progress through your journey in object-oriented programming, you'll find these design patterns are not just theoretical concepts but practical tools that can significantly enhance your coding efficiency and project maintainability.
24/09/2024 | Java
16/10/2024 | Java
16/10/2024 | Java
11/12/2024 | Java
23/09/2024 | Java
16/10/2024 | Java
23/09/2024 | Java
24/09/2024 | Java
23/09/2024 | Java
24/09/2024 | Java
16/10/2024 | Java
16/10/2024 | Java