Table of contents
No headings in the article.
Design patterns are a proven solution to common software development problems. They help you create reliable, reusable, and maintainable code. In this blog post, we will explore the most used design patterns and why they are so important.
- Singleton Pattern
The Singleton pattern is one of the most used design patterns in software development. It is used when you need to ensure that only one instance of a class exists in your program. This pattern is useful for managing shared resources such as databases, logging systems, and configuration files.
Here is an example in java
Sure, here are some examples of the Singleton, Factory, and Adapter patterns in Java:
csharpCopy codepublic class Singleton {
private static Singleton instance = null;
private Singleton() {
// Private constructor to prevent instantiation from outside
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
// Other methods and properties
}
- Factory Pattern
The Factory pattern is used to create objects without exposing the instantiation logic to the client. This pattern provides an interface for creating objects and allows subclasses to decide which class to instantiate. The Factory pattern is useful for managing object creation in complex systems.
Here is an example in Java
Factory Pattern:
typescriptCopy codepublic interface Shape {
void draw();
}
public class Circle implements Shape {
@Override
public void draw() {
System.out.println("Circle drawn.");
}
}
public class Rectangle implements Shape {
@Override
public void draw() {
System.out.println("Rectangle drawn.");
}
}
public class ShapeFactory {
public static Shape getShape(String shapeType) {
if (shapeType.equalsIgnoreCase("CIRCLE")) {
return new Circle();
} else if (shapeType.equalsIgnoreCase("RECTANGLE")) {
return new Rectangle();
} else {
return null;
}
}
}
// Usage:
Shape circle = ShapeFactory.getShape("CIRCLE");
circle.draw(); // Output: Circle drawn.
Shape rectangle = ShapeFactory.getShape("RECTANGLE");
rectangle.draw(); // Output: Rectangle drawn.
- Adapter Pattern
The Adapter pattern is used to convert the interface of one class into another interface that clients expect. This pattern allows incompatible classes to work together by wrapping the interface of one class with another. The Adapter pattern is useful for integrating legacy code with new code or for integrating third-party libraries.
Here is an Example in Java
typescriptCopy codepublic interface MediaPlayer {
void play(String audioType, String fileName);
}
public interface AdvancedMediaPlayer {
void playVlc(String fileName);
void playMp4(String fileName);
}
public class VlcPlayer implements AdvancedMediaPlayer {
@Override
public void playVlc(String fileName) {
System.out.println("Playing vlc file: " + fileName);
}
@Override
public void playMp4(String fileName) {
// Do nothing
}
}
public class Mp4Player implements AdvancedMediaPlayer {
@Override
public void playVlc(String fileName) {
// Do nothing
}
@Override
public void playMp4(String fileName) {
System.out.println("Playing mp4 file: " + fileName);
}
}
public class MediaAdapter implements MediaPlayer {
private AdvancedMediaPlayer advancedMediaPlayer;
public MediaAdapter(String audioType) {
if (audioType.equalsIgnoreCase("VLC")) {
advancedMediaPlayer = new VlcPlayer();
} else if (audioType.equalsIgnoreCase("MP4")) {
advancedMediaPlayer = new Mp4Player();
}
}
@Override
public void play(String audioType, String fileName) {
if (audioType.equalsIgnoreCase("VLC")) {
advancedMediaPlayer.playVlc(fileName);
} else if (audioType.equalsIgnoreCase("MP4")) {
advancedMediaPlayer.playMp4(fileName);
}
}
}
public class AudioPlayer implements MediaPlayer {
private MediaAdapter mediaAdapter;
@Override
public void play(String audioType, String fileName) {
if (audioType.equalsIgnoreCase("MP3")) {
System.out.println("Playing mp3 file: " + fileName);
} else if (audioType.equalsIgnoreCase("VLC") || audioType.equalsIgnoreCase("MP4")) {
mediaAdapter = new MediaAdapter(audioType);
mediaAdapter.play(audioType, fileName);
} else {
System.out.println("Invalid media type: " + audioType);
}
}
}
// Usage:
MediaPlayer audioPlayer = new AudioPlayer();
audioPlayer.play("MP3", "song.mp3"); // Output: Playing mp3 file: song.mp3
audioPlayer.play("VLC", "song.vlc"); // Output: Playing vlc file: song.vlc
audioPlayer.play("MP4", "song.mp4"); // Output: Playing mp4 file: song.mp4
audioPlayer.play("W
- Observer Pattern
The Observer pattern is used when you need to notify multiple objects about changes to an object. This pattern defines a one-to-many relationship between objects, where one object changes state, and all its dependents are notified automatically. The Observer pattern is useful for building event-driven systems.
Here is example in java
Observer Pattern:
javaCopy codeimport java.util.ArrayList;
import java.util.List;
public interface Subject {
void registerObserver(Observer observer);
void removeObserver(Observer observer);
void notifyObservers();
}
public interface Observer {
void update(String message);
}
public class NewsAgency implements Subject {
private List<Observer> observers = new ArrayList<>();
private String news;
@Override
public void registerObserver(Observer observer) {
observers.add(observer);
}
@Override
public void removeObserver(Observer observer) {
observers.remove(observer);
}
@Override
public void notifyObservers() {
for (Observer observer : observers) {
observer.update(news);
}
}
public void setNews(String news) {
this.news = news;
notifyObservers();
}
}
public class NewsChannel implements Observer {
private String news;
@Override
public void update(String message) {
this.news = message;
System.out.println("NewsChannel received news: " + news);
}
}
// Usage:
NewsAgency newsAgency = new NewsAgency();
NewsChannel newsChannel = new NewsChannel();
newsAgency.registerObserver(newsChannel);
newsAgency.setNews("New article published.");
newsAgency.removeObserver(newsChannel);
newsAgency.setNews("Breaking news.");
Decorator Pattern:
- Decorator Pattern
The Decorator pattern is used to add new functionality to an existing object dynamically. This pattern allows you to add new behaviors to an object by wrapping it with another object. The Decorator pattern is useful for adding features to objects without modifying their code.
Here is an Example in Java
javaCopy codepublic interface Pizza {
String getDescription();
double getPrice();
}
public class MargheritaPizza implements Pizza {
@Override
public String getDescription() {
return "Margherita Pizza";
}
@Override
public double getPrice() {
return 8.99;
}
}
public abstract class PizzaDecorator implements Pizza {
protected Pizza pizza;
public PizzaDecorator(Pizza pizza) {
this.pizza = pizza;
}
@Override
public String getDescription() {
return pizza.getDescription();
}
@Override
public double getPrice() {
return pizza.getPrice();
}
}
public class CheeseDecorator extends PizzaDecorator {
public CheeseDecorator(Pizza pizza) {
super(pizza);
}
@Override
public String getDescription() {
return pizza.getDescription() + ", extra cheese";
}
@Override
public double getPrice() {
return pizza.getPrice() + 1.50;
}
}
// Usage:
Pizza pizza = new MargheritaPizza();
System.out.println(pizza.getDescription() + " - Price: " + pizza.getPrice());
Pizza cheesePizza = new CheeseDecorator(new MargheritaPizza());
System.out.println(cheesePizza.getDescription() + " - Price: " + cheesePizza.getPrice());
Strategy Pattern:
- Strategy Pattern
The Strategy pattern is used when you need to change the behavior of an object at runtime. This pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. The Strategy pattern is useful for systems that require different algorithms for different situations.
here is an example in java
here's an example of the Strategy pattern in Java:
typescriptCopy codepublic interface EncryptionStrategy {
String encrypt(String plaintext);
}
public class AESEncryptionStrategy implements EncryptionStrategy {
@Override
public String encrypt(String plaintext) {
System.out.println("Encrypting plaintext using AES encryption.");
// Actual AES encryption logic goes here
return "AES-encrypted string";
}
}
public class BlowfishEncryptionStrategy implements EncryptionStrategy {
@Override
public String encrypt(String plaintext) {
System.out.println("Encrypting plaintext using Blowfish encryption.");
// Actual Blowfish encryption logic goes here
return "Blowfish-encrypted string";
}
}
public class Encryptor {
private EncryptionStrategy strategy;
public void setStrategy(EncryptionStrategy strategy) {
this.strategy = strategy;
}
public String encrypt(String plaintext) {
return strategy.encrypt(plaintext);
}
}
// Usage:
Encryptor encryptor = new Encryptor();
EncryptionStrategy aesEncryptionStrategy = new AESEncryptionStrategy();
EncryptionStrategy blowfishEncryptionStrategy = new BlowfishEncryptionStrategy();
encryptor.setStrategy(aesEncryptionStrategy);
String aesEncryptedString = encryptor.encrypt("Hello, world!");
encryptor.setStrategy(blowfishEncryptionStrategy);
String blowfishEncryptedString = encryptor.encrypt("Hello, world!");
In this example, the EncryptionStrategy
interface defines the strategy for encrypting a plaintext string. The AESEncryptionStrategy
and BlowfishEncryptionStrategy
classes implement the EncryptionStrategy
interface with the actual encryption logic for AES and Blowfish algorithms, respectively.
The Encryptor
class has a reference to an EncryptionStrategy
object, which can be set to an instance of either AESEncryptionStrategy
or BlowfishEncryptionStrategy
. When the encrypt
method is called, it delegates the encryption task to the strategy object that has been set, without knowing the specific encryption algorithm being used.
This allows the Encryptor
class to be flexible and easily adaptable to different encryption algorithms, without the need for extensive changes to the code.
- Command Pattern
The Command pattern is used to encapsulate a request as an object, thereby allowing you to parameterize clients with different requests, queue or log requests, and support undoable operations. This pattern is useful for implementing complex features like undo/redo functionality, logging, and transactional behavior.
Here is an example in Java
javaCopy codepublic interface Command {
void execute();
}
public class Light {
public void turnOn() {
System.out.println("Light is turned on");
}
public void turnOff() {
System.out.println("Light is turned off");
}
}
public class LightOnCommand implements Command {
private Light light;
public LightOnCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.turnOn();
}
}
public class LightOffCommand implements Command {
private Light light;
public LightOffCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.turnOff();
}
}
public class RemoteControl {
private Command command;
public void setCommand(Command command) {
this.command = command;
}
public void pressButton() {
command.execute();
}
}
// Usage:
Light light = new Light();
Command lightOnCommand = new LightOnCommand(light);
Command lightOffCommand = new LightOffCommand(light);
RemoteControl remoteControl = new RemoteControl();
remoteControl.setCommand(lightOnCommand);
remoteControl.pressButton();
remoteControl.setCommand(lightOffCommand);
remoteControl.pressButton();
Conclusion
Design patterns are an essential part of software development. They provide a way to solve common problems and create code that is reusable, reliable, and maintainable. The Singleton pattern, Factory pattern, Adapter pattern, Observer pattern, Decorator pattern, Strategy pattern, and Command pattern are some of the most used design patterns in software development. By understanding these patterns and how to use them, you can improve the quality of your code and become a better programmer.