Unlock Design Mastery: The Essential Top 10 GoF Patterns You Can’t Afford to Miss!

Rajneesh Singh
9 min readDec 20, 2023

--

23 GoF Design Patters

A design pattern is like a proven recipe for solving common problems in a specific way when building things, just like how you might have a recipe for baking cookies. It’s a set of steps that people have found to work well together in certain situations. Instead of reinventing the wheel each time you face a similar problem, you can use a design pattern to guide your solution. It’s a way for experienced builders (developers) to share their best practices with others, so everyone doesn’t have to start from scratch every time they build something.

Singleton Pattern:

  • Ensures a class has only one instance and provides a global point of access to it.
  • In simple terms, imagine you have a magical box. This box can do something special, and you want to make sure there’s only one box in the entire world. So, you create a rule: whenever someone asks for the box, they always get the same one.
  • That’s what the Singleton Pattern is like in programming. It ensures that there’s only one instance (or copy) of a special thing, and whenever someone needs that special thing, they always get the same one. It’s like having a unique, one-of-a-kind object that everyone shares and uses.
  • For example, In many software applications, there’s a need to manage configuration settings that are used throughout the program. The Singleton Pattern can be applied to ensure that there’s only one instance of the configuration manager, and it can be accessed consistently from different parts of the application.
public class ConfigurationManager {
// This is where we'll keep our one and only instance
private static ConfigurationManager instance;
// Configuration settings go here
private String databaseUrl;
private int maxConnections;
// Private constructor to prevent creating multiple instances
private ConfigurationManager() {
// Load configuration settings from a file or another source
// For simplicity, we'll initialize with default values here
this.databaseUrl = "jdbc:mysql://localhost:3306/mydatabase";
this.maxConnections = 10;
}
// Method to get the one and only instance of ConfigurationManager
public static ConfigurationManager getInstance() {
if (instance == null) {
instance = new ConfigurationManager(); // Create the instance if it doesn't exist yet
}
return instance;
}
// Getter methods for configuration settings
public String getDatabaseUrl() {
return databaseUrl;
}
public int getMaxConnections() {
return maxConnections;
}
// Setter methods for configuration settings (if needed)
public void setDatabaseUrl(String databaseUrl) {
this.databaseUrl = databaseUrl;
}
public void setMaxConnections(int maxConnections) {
this.maxConnections = maxConnections;
}
}

Factory Method Pattern:

  • Defines an interface for creating an object but leaves the choice of its type to the subclasses, creating instances of a class in a factory method
  • In simple terms, Imagine you have a pizza shop, and people want different types of pizzas — some want Margherita, others want Pepperoni. Instead of making pizzas directly, you have a special pizza chef (the factory) who makes pizzas based on what people order.
  • The Factory Method Pattern is like having a customizable pizza chef. There’s a method (the factory method) that you call whenever you want a pizza, and this method creates the specific type of pizza you order. You don’t have to worry about how the pizza is made; you just ask the chef for the kind you want.
  • For example, a document creation system where you can create different types of documents, such as PDFs and Word documents. In this example, we’ll have a Document interface and two concrete implementations: PDFDocument and WordDocument. We'll use a DocumentFactory as the factory to create instances of these documents based on user requests.
// Document interface
interface Document {
void create();
}
// Concrete implementation: PDFDocument
class PDFDocument implements Document {
@Override
public void create() {
System.out.println("Creating a PDF document.");
// Additional PDF-specific implementation details can go here
}
}
// Concrete implementation: WordDocument
class WordDocument implements Document {
@Override
public void create() {
System.out.println("Creating a Word document.");
// Additional Word-specific implementation details can go here
}
}
// DocumentFactory interface
interface DocumentFactory {
Document createDocument();
}
// Concrete implementation of DocumentFactory for creating PDF documents
class PDFDocumentFactory implements DocumentFactory {
@Override
public Document createDocument() {
return new PDFDocument();
}
}
// Concrete implementation of DocumentFactory for creating Word documents
class WordDocumentFactory implements DocumentFactory {
@Override
public Document createDocument() {
return new WordDocument();
}
}
// Client code
public class DocumentCreationClient {
public static void main(String[] args) {
// User wants to create a PDF document
DocumentFactory pdfFactory = new PDFDocumentFactory();
Document pdfDocument = pdfFactory.createDocument();
pdfDocument.create();
// User wants to create a Word document
DocumentFactory wordFactory = new WordDocumentFactory();
Document wordDocument = wordFactory.createDocument();
wordDocument.create();
}
}

Abstract Factory Pattern:

  • Provides an interface for creating families of related or dependent objects without specifying their concrete classes.
  • In simple terms, Imagine you’re in charge of creating different themes for a mobile phone. You have a set of buttons, icons, and backgrounds that need to look consistent for each theme (e.g., Dark Theme, Light Theme). Instead of choosing each element one by one, you have a magic tool (the abstract factory) that gives you a complete set of buttons, icons, and backgrounds that match the theme you want.
  • The Abstract Factory Pattern is like having a theme tool that provides a whole set of related objects. You don’t have to worry about each button or icon; you just ask the tool for a complete set, and it gives you everything that goes together.
  • For example, the GUI (Graphical User Interface) library creates different UI elements for different operating systems. We’ll create an abstract factory that can produce buttons and checkboxes, and then we’ll have concrete factories for two different operating systems: Windows and macOS.
// Abstract Product: Button
interface Button {
void display();
}
// Concrete Product: WindowsButton
class WindowsButton implements Button {
@Override
public void display() {
System.out.println("Displaying a Windows button.");
}
}
// Concrete Product: MacOSButton
class MacOSButton implements Button {
@Override
public void display() {
System.out.println("Displaying a MacOS button.");
}
}
// Abstract Product: Checkbox
interface Checkbox {
void display();
}
// Concrete Product: WindowsCheckbox
class WindowsCheckbox implements Checkbox {
@Override
public void display() {
System.out.println("Displaying a Windows checkbox.");
}
}
// Concrete Product: MacOSCheckbox
class MacOSCheckbox implements Checkbox {
@Override
public void display() {
System.out.println("Displaying a MacOS checkbox.");
}
}
// Abstract Factory
interface GUIFactory {
Button createButton();
Checkbox createCheckbox();
}
// Concrete Factory for Windows
class WindowsGUIFactory implements GUIFactory {
@Override
public Button createButton() {
return new WindowsButton();
}
@Override
public Checkbox createCheckbox() {
return new WindowsCheckbox();
}
}
// Concrete Factory for MacOS
class MacOSGUIFactory implements GUIFactory {
@Override
public Button createButton() {
return new MacOSButton();
}
@Override
public Checkbox createCheckbox() {
return new MacOSCheckbox();
}
}
// Client Code
public class GUIApplication {
private GUIFactory guiFactory;
public GUIApplication(GUIFactory guiFactory) {
this.guiFactory = guiFactory;
}
public void createUI() {
Button button = guiFactory.createButton();
Checkbox checkbox = guiFactory.createCheckbox();
button.display();
checkbox.display();
}
public static void main(String[] args) {
// Creating GUI for Windows
GUIFactory windowsFactory = new WindowsGUIFactory();
GUIApplication windowsApp = new GUIApplication(windowsFactory);
windowsApp.createUI();
// Creating GUI for MacOS
GUIFactory macosFactory = new MacOSGUIFactory();
GUIApplication macosApp = new GUIApplication(macosFactory);
macosApp.createUI();
}
}

Difference between Factory Method Pattern and Abstract Factory Pattern

  • The key difference lies in the scope and purpose. The Factory Method Pattern deals with creating a single object, allowing subclasses to alter the type of object created. In contrast, the Abstract Factory Pattern deals with creating families of related or dependent objects, providing an interface for creating multiple types of objects.
  • The Factory Method involves a single method, while the Abstract Factory involves multiple factory methods organized in a family.
  • The choice between these patterns depends on the specific requirements of the system and the level of flexibility needed in creating objects.

Builder Pattern:

  • Separates the construction of a complex object from its representation, allowing the same construction process to create different representations.
  • In simple terms, Imagine you’re building a house, and you have various components like walls, doors, windows, and a roof. Instead of having a predefined blueprint for every type of house, you hire a special builder (the builder pattern) who understands how to put these components together based on your preferences.
  • The Builder Pattern is like having a customizable house builder. You tell the builder what type of walls you want, what style of doors and windows, and what kind of roof. The builder then takes care of assembling these components in the right order to construct your personalized house.
  • For example, the Builder Pattern might be used: to build a complex object, such as an HTML document.
  • In this example, we’ll create a builder for an HTML document that can have a title, a heading, paragraphs, and a footer. The builder allows you to specify these components flexibly.
// Builder interface
interface HTMLBuilder {
void setTitle(String title);
void setHeading(String heading);
void addParagraph(String paragraph);
void setFooter(String footer);
String getResult();
}
// Concrete HTML builder implementation
class SimpleHTMLBuilder implements HTMLBuilder {
private StringBuilder html = new StringBuilder();
@Override
public void setTitle(String title) {
html.append("<title>").append(title).append("</title>");
}
@Override
public void setHeading(String heading) {
html.append("<h1>").append(heading).append("</h1>");
}
@Override
public void addParagraph(String paragraph) {
html.append("<p>").append(paragraph).append("</p>");
}
@Override
public void setFooter(String footer) {
html.append("<footer>").append(footer).append("</footer>");
}
@Override
public String getResult() {
return html.toString();
}
}
// Director class that constructs an HTML document using the builder
class HTMLDocumentDirector {
private HTMLBuilder builder;
public HTMLDocumentDirector(HTMLBuilder builder) {
this.builder = builder;
}
public void constructDocument() {
builder.setTitle("Builder Pattern Example");
builder.setHeading("Welcome to the Builder Pattern");
builder.addParagraph("This is a simple example of using the Builder Pattern in software development.");
builder.addParagraph("It allows you to construct complex objects step by step.");
builder.setFooter("© 2023 Builder Pattern Corporation");
}
}
// Client code
public class HTMLBuilderExample {
public static void main(String[] args) {
// Create a concrete builder
HTMLBuilder builder = new SimpleHTMLBuilder();
// Create a director that constructs an HTML document using the builder
HTMLDocumentDirector director = new HTMLDocumentDirector(builder);
// Construct the HTML document
director.constructDocument();
// Get the result from the builder
String htmlDocument = builder.getResult();
// Display the result
System.out.println(htmlDocument);
}
}

Prototype Pattern:

  • Creates new objects by copying an existing object, known as the prototype.
  • In simple terms, Imagine you have a specific type of notebook that you like. It has a particular cover design, paper type, and layout that suits your preferences. Instead of buying the same notebook over and over again, you have a special machine (the prototype) that can create exact copies of your favorite notebook whenever you need a new one.
  • In this case: The prototype is the specific type of notebook you like. The Prototype Pattern is like using a special machine to duplicate the notebook whenever you want another one.
  • For example, you have a document editor where users can create documents with different sections, headings, and paragraphs. Each document may have a specific structure and formatting. Instead of defining the structure from scratch every time, you can use the Prototype Pattern to clone existing document templates.
// Prototype interface
interface Document {
Document clone();
void display();
}
// Concrete implementation: Resume
class Resume implements Document {
private String heading;
private String content;
public Resume(String heading, String content) {
this.heading = heading;
this.content = content;
}
@Override
public Document clone() {
return new Resume(this.heading, this.content); // Cloning by creating a new instance with the same properties
}
@Override
public void display() {
System.out.println("Resume - Heading: " + heading + ", Content: " + content);
}
}
// Concrete implementation: Report
class Report implements Document {
private String title;
private String body;
public Report(String title, String body) {
this.title = title;
this.body = body;
}
@Override
public Document clone() {
return new Report(this.title, this.body); // Cloning by creating a new instance with the same properties
}
@Override
public void display() {
System.out.println("Report - Title: " + title + ", Body: " + body);
}
}
// Client code
public class DocumentEditor {
public static void main(String[] args) {
// Create prototype documents
Document originalResume = new Resume("John Doe's Resume", "Skills: Java, Python, HTML");
Document originalReport = new Report("Monthly Report", "Summary: Project progress and goals");
// Clone documents when needed
Document clonedResume = originalResume.clone();
Document clonedReport = originalReport.clone();
// Use the cloned documents
clonedResume.display();
clonedReport.display();
}
}

In summary, design patterns provide proven solutions to recurring problems in software design. They are tools in a developer’s toolbox, aiding in the creation of flexible, maintainable, and scalable software systems. Choosing and applying design patterns should be a thoughtful process based on the specific needs and context of a given software project.

--

--

Rajneesh Singh
Rajneesh Singh

Written by Rajneesh Singh

Problem Solver | Software Engineer

No responses yet