Demystifying Design Patterns in Software Development — Engineering

Ahsi Dev
8 min readAug 9, 2023

--

Design patterns and its types with coding samples in Java

Introduction:

  • Briefly explain the importance of design patterns in writing maintainable and efficient code.
  • Mention the challenges developers face and how design patterns provide solutions.
  • Set the tone for the article by highlighting the goal of breaking down complex concepts with easy-to-understand explanations and real-world code samples.

Sections:

  1. Understanding the Essence of Design Patterns
  • Defining design patterns and their role in software development.
  • Exploring the benefits of incorporating design patterns into your coding repertoire.

2. Unveiling the Types of Design Patterns

  • Detailing Creational, Structural, and Behavioral design patterns.
  • Illustrating the purpose and characteristics of each pattern category.

3. Coding Samples and Applications

  • Providing coding examples for key design patterns, including Singleton, Factory Method, Abstract Factory, Adapter, Decorator, Observer, Strategy, Composite, and Command.
  • Demonstrating how each pattern solves specific software design challenges.

— — —

Section 1: Understanding Design Patterns

Subsection 1.1: What are Design Patterns?

  • Define design patterns as reusable solutions to common software design problems.
  • Mention that design patterns are not code snippets but rather general guidelines.
  • Emphasize how design patterns promote code reusability, maintainability, and scalability.

Subsection 1.2: Why Use Design Patterns?

  • Discuss the benefits of using design patterns, including improved code quality, modularity, and easier collaboration among developers.
  • Highlight the reduction of reinventing the wheel and the acceleration of development.

Section 2: Types of Design Patterns

Subsection 2.1: Creational Patterns

Singleton:

  • Explain the concept of a single instance throughout the application.
  • Provide a code example in a popular programming language (e.g., Java or Python).
public class Singleton {
private static Singleton instance;

private Singleton() {
// Private constructor to prevent instantiation
}

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

public void showMessage() {
System.out.println("Singleton instance is working.");
}
}

public class Main {
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();
singleton.showMessage();
}
}

Factory Method:

  • Describe how this pattern delegates object creation to subclasses.
  • Provide a code example demonstrating a factory method implementation.
interface Product {
void produce();
}

class ConcreteProductA implements Product {
@Override
public void produce() {
System.out.println("Producing Product A.");
}
}

class ConcreteProductB implements Product {
@Override
public void produce() {
System.out.println("Producing Product B.");
}
}

abstract class Creator {
abstract Product factoryMethod();
}

class ConcreteCreatorA extends Creator {
@Override
Product factoryMethod() {
return new ConcreteProductA();
}
}

class ConcreteCreatorB extends Creator {
@Override
Product factoryMethod() {
return new ConcreteProductB();
}
}

public class Main {
public static void main(String[] args) {
Creator creatorA = new ConcreteCreatorA();
Product productA = creatorA.factoryMethod();
productA.produce();

Creator creatorB = new ConcreteCreatorB();
Product productB = creatorB.factoryMethod();
productB.produce();
}
}

Abstract Factory:

  • Explain how abstract factories create families of related objects.
  • Present a practical example using different types of related objects.
interface Button {
void render();
}

interface Checkbox {
void render();
}

class WindowsButton implements Button {
@Override
public void render() {
System.out.println("Rendering a Windows button.");
}
}

class WindowsCheckbox implements Checkbox {
@Override
public void render() {
System.out.println("Rendering a Windows checkbox.");
}
}

class MacOSButton implements Button {
@Override
public void render() {
System.out.println("Rendering a macOS button.");
}
}

class MacOSCheckbox implements Checkbox {
@Override
public void render() {
System.out.println("Rendering a macOS checkbox.");
}
}

interface GUIFactory {
Button createButton();
Checkbox createCheckbox();
}

class WindowsFactory implements GUIFactory {
@Override
public Button createButton() {
return new WindowsButton();
}

@Override
public Checkbox createCheckbox() {
return new WindowsCheckbox();
}
}

class MacOSFactory implements GUIFactory {
@Override
public Button createButton() {
return new MacOSButton();
}

@Override
public Checkbox createCheckbox() {
return new MacOSCheckbox();
}
}

public class Main {
public static void main(String[] args) {
GUIFactory windowsFactory = new WindowsFactory();
Button windowsButton = windowsFactory.createButton();
Checkbox windowsCheckbox = windowsFactory.createCheckbox();

windowsButton.render();
windowsCheckbox.render();

GUIFactory macOsFactory = new MacOSFactory();
Button macOsButton = macOsFactory.createButton();
Checkbox macOsCheckbox = macOsFactory.createCheckbox();

macOsButton.render();
macOsCheckbox.render();
}
}

— — —

Subsection 2.2: Structural Patterns

Adapter:

  • Introduce the adapter pattern for making incompatible interfaces work together.
  • Provide a code snippet illustrating adapter pattern usage.
interface MediaPlayer {
void play(String audioType, String fileName);
}

interface AdvancedMediaPlayer {
void playVlc(String fileName);
void playMp4(String fileName);
}

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
}
}

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);
}
}

class MediaAdapter implements MediaPlayer {
private AdvancedMediaPlayer advancedMusicPlayer;

public MediaAdapter(String audioType) {
if (audioType.equalsIgnoreCase("vlc")) {
advancedMusicPlayer = new VlcPlayer();
} else if (audioType.equalsIgnoreCase("mp4")) {
advancedMusicPlayer = new Mp4Player();
}
}

@Override
public void play(String audioType, String fileName) {
if (audioType.equalsIgnoreCase("vlc")) {
advancedMusicPlayer.playVlc(fileName);
} else if (audioType.equalsIgnoreCase("mp4")) {
advancedMusicPlayer.playMp4(fileName);
}
}
}

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 format: " + audioType);
}
}
}

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

audioPlayer.play("mp3", "song.mp3");
audioPlayer.play("vlc", "movie.vlc");
audioPlayer.play("mp4", "video.mp4");
audioPlayer.play("avi", "video.avi");
}
}

Decorator:

  • Define the decorator pattern for dynamically adding responsibilities to objects.
  • Offer a coding example with a practical use case.
interface Coffee {
String getDescription();
double getCost();
}

class SimpleCoffee implements Coffee {
@Override
public String getDescription() {
return "Simple Coffee";
}

@Override
public double getCost() {
return 2.0;
}
}

abstract class CoffeeDecorator implements Coffee {
protected Coffee decoratedCoffee;

public CoffeeDecorator(Coffee decoratedCoffee) {
this.decoratedCoffee = decoratedCoffee;
}

@Override
public String getDescription() {
return decoratedCoffee.getDescription();
}

@Override
public double getCost() {
return decoratedCoffee.getCost();
}
}

class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee decoratedCoffee) {
super(decoratedCoffee);
}

@Override
public String getDescription() {
return super.getDescription() + ", Milk";
}

@Override
public double getCost() {
return super.getCost() + 0.5;
}
}

class SugarDecorator extends CoffeeDecorator {
public SugarDecorator(Coffee decoratedCoffee) {
super(decoratedCoffee);
}

@Override
public String getDescription() {
return super.getDescription() + ", Sugar";
}

@Override
public double getCost() {
return super.getCost() + 0.2;
}
}

public class Main {
public static void main(String[] args) {
Coffee simpleCoffee = new SimpleCoffee();
System.out.println("Description: " + simpleCoffee.getDescription() + ", Cost: $" + simpleCoffee.getCost());

Coffee milkCoffee = new MilkDecorator(new SimpleCoffee());
System.out.println("Description: " + milkCoffee.getDescription() + ", Cost: $" + milkCoffee.getCost());

Coffee sugarMilkCoffee = new SugarDecorator(new MilkDecorator(new SimpleCoffee()));
System.out.println("Description: " + sugarMilkCoffee.getDescription() + ", Cost: $" + sugarMilkCoffee.getCost());
}
}

Composite:

  • Explain how the composite pattern structures objects into tree-like hierarchies.
  • Show how to implement the composite pattern with a real-world analogy.
import java.util.ArrayList;
import java.util.List;

interface Component {
void operation();
}

class Leaf implements Component {
private String name;

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

@Override
public void operation() {
System.out.println("Leaf " + name + " operation.");
}
}

class Composite implements Component {
private List<Component> components = new ArrayList<>();

public void add(Component component) {
components.add(component);
}

@Override
public void operation() {
System.out.println("Composite operation:");
for (Component component : components) {
component.operation();
}
}
}

public class Main {
public static void main(String[] args) {
Leaf leaf1 = new Leaf("Leaf 1");
Leaf leaf2 = new Leaf("Leaf 2");
Leaf leaf3 = new Leaf("Leaf 3");

Composite composite = new Composite();
composite.add(leaf1);
composite.add(leaf2);

Composite composite2 = new Composite();
composite2.add(leaf3);

composite.add(composite2);

composite.operation();

— — —

Subsection 2.3: Behavioral Patterns

Observer:

  • Detail the observer pattern for maintaining a one-to-many relationship between objects.
  • Include a code sample demonstrating an observer pattern implementation.
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;
}

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

class Subject {
private List<Observer> observers = new ArrayList<>();
private String state;

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

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

public void setState(String state) {
this.state = state;
notifyObservers();
}

private void notifyObservers() {
for (Observer observer : observers) {
observer.update(state);
}
}
}

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.setState("New state update!");
}
}

Strategy:

  • Discuss the strategy pattern that enables interchangeable algorithms.
  • Provide a coding example showcasing different strategies for a specific problem.
interface PaymentStrategy {
void pay(int amount);
}

class CreditCardPayment implements PaymentStrategy {
private String cardNumber;
private String expirationDate;

public CreditCardPayment(String cardNumber, String expirationDate) {
this.cardNumber = cardNumber;
this.expirationDate = expirationDate;
}

@Override
public void pay(int amount) {
System.out.println("Paid $" + amount + " using credit card " + cardNumber);
}
}

class PayPalPayment implements PaymentStrategy {
private String email;

public PayPalPayment(String email) {
this.email = email;
}

@Override
public void pay(int amount) {
System.out.println("Paid $" + amount + " using PayPal account " + email);
}
}

class ShoppingCart {
private PaymentStrategy paymentStrategy;

public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
this.paymentStrategy = paymentStrategy;
}

public void checkout(int amount) {
paymentStrategy.pay(amount);
}
}

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

PaymentStrategy creditCardPayment = new CreditCardPayment("1234-5678-9012-3456", "12/25");
PaymentStrategy payPalPayment = new PayPalPayment("user@example.com");

cart.setPaymentStrategy(creditCardPayment);
cart.checkout(100);

cart.setPaymentStrategy(payPalPayment);
cart.checkout(50);
}
}

Command:

  • Describe the command pattern for encapsulating requests as objects.
  • Present a scenario where the command pattern can be useful.
interface Command {
void execute();
}

class Light {
void turnOn() {
System.out.println("Light is ON");
}

void turnOff() {
System.out.println("Light is OFF");
}
}

class LightOnCommand implements Command {
private Light light;

public LightOnCommand(Light light) {
this.light = light;
}

@Override
public void execute() {
light.turnOn();
}
}

class LightOffCommand implements Command {
private Light light;

public LightOffCommand(Light light) {
this.light = light;
}

@Override
public void execute() {
light.turnOff();
}
}

class RemoteControl {
private Command command;

public void setCommand(Command command) {
this.command = command;
}

public void pressButton() {
command.execute();
}
}

public class Main {
public static void main(String[] args) {
Light livingRoomLight = new Light();
Command livingRoomLightOn = new LightOnCommand(livingRoomLight);
Command livingRoomLightOff = new LightOffCommand(livingRoomLight);

RemoteControl remote = new RemoteControl();

remote.setCommand(livingRoomLightOn);
remote.pressButton();

remote.setCommand(livingRoomLightOff);
remote.pressButton();
}
}

— — —

Section 3: Implementing Design Patterns

Subsection 3.1: Real-World Use Cases

  • Illustrate scenarios where specific design patterns are commonly applied.
  • Discuss the benefits and outcomes of applying the patterns in each case.

Subsection 3.2: Tips for Effective Usage

  • Offer guidelines for selecting the appropriate design pattern for a given problem.
  • Discuss the importance of understanding the problem domain before choosing a pattern.

— — —

Conclusion: Elevating Software Craftsmanship with Design Patterns

In the intricate realm of software development, design patterns stand as the beacon of excellence. Through this exploration, we’ve uncovered the power of design patterns to transform complex challenges into elegant solutions.

From Creational patterns sculpting instances to Structural patterns weaving architecture, and Behavioral patterns orchestrating interactions, each pattern presents a refined approach to crafting robust, adaptable software.

Mastering design patterns isn’t just about code; it’s about adopting a refined mindset that fosters innovation and collaboration. These patterns become your guide, ensuring every codebase is a testament to engineering artistry.

In your software journey, let design patterns be your trusted allies. They’ll empower you to architect solutions that endure, elevate, and epitomize professionalism. Embrace patterns as tools for delivering code that transcends the mundane and embodies the extraordinary.

As you navigate the future of software, remember: your patterns speak volumes about your craftsmanship. With design patterns in your toolkit, you’re poised to create software that’s not only functional but truly exceptional.

*Design Patterns: Where Code Meets Craftsmanship.*

*Happy coding and pattern crafting.*

— — —

Author’s Note and Engagement:

For queries, insights, or to dive into specific IT and software development topics, feel free to follow my blog articles. Additionally, connect with me on social media through the links below for further discussions and updates.

GitHub: https://github.com/Ahsan-001

LinkedIn: https://www.linkedin.com/in/ahsan-saeed-11a787183/

gmail: ranaahsan5285@gmail.com

Photo by Fotis Fotopoulos on Unsplash

--

--

Ahsi Dev

I am a Developer that writes about Software Development