Creational Design Patterns

09 October, 2023
creational patterns

Creational design patterns enable system flexibility by abstracting object instantiation and composition details.

Creational design patterns simplify object creation, offering flexibility through inheritance (class creational) or delegation to another object (object creational). As systems evolve, these patterns encapsulate class knowledge, hide instantiation details, and provide versatility in creation, methods, and timing. They're useful for configuring systems with dynamic "product" objects, supporting both static and dynamic configurations. The examples of design patterns falling under creational patterns, listed above, are illustrated below with their usage for effective object instantiation and system configuration.

singleton pattern

Singleton Design Pattern

The Singleton pattern ensures that a class has only one instance and provides a global point of access to it.

  • Intent: To restrict the instantiation of a class to a single instance and provide a global point of access to that instance.

  • Why: Sometimes, it's necessary to have exactly one instance of a class, for example, to control actions that should be performed once across the entire application.

  • How: By defining a class that has a method for creating its instance if it doesn't exist or returning the existing instance if it does.

class Singleton 
{
  constructor() 
  {
    if (Singleton.instance) 
    { return Singleton.instance; }
    Singleton.instance = this;
    // initialization code here
    return this;
  }
  static getInstance() 
  { return Singleton.instance; }
}
  • Explanation:
  1. The constructor checks whether an instance of the class already exists (Singleton.instance). If yes, it returns that instance.
  2. If no instance exists, it creates one (this) and saves it as a property of the class (Singleton.instance).
  3. Subsequent calls to the constructor return the same instance, ensuring a single point of access.
  • Application Scenario: Suppose you are implementing a logger for a web application. You want to ensure that there is only one instance of the logger to centralize logging and prevent multiple conflicting logs. The Singleton pattern ensures that this logger is instantiated only once.
    class Logger extends Singleton 
    {
      constructor() 
      {
        super();

        this.logLevels = {
          INFO: 1,
          WARN: 2,
          ERROR: 3,
        };
        this.logLevel = this.logLevels.INFO;
      }

      log(message, level = this.logLevel) 

      {
        if (level >= this.logLevel) 
        { console.log(`[${level}] ${message}`); }
      }
      setLogLevel(level) 
      { this.logLevel = level; }
    }
  • To use the Logger class, simply call the getInstance() method:
const logger = Logger.getInstance();

//Usage
logger.log('This is an informational message.');
logger.warn('This is a warning message.');
logger.error('This is an error message.');
  • You can also change the log level at any time:

//This will ensure that only error messages are logged to the console.
logger.setLogLevel(Logger.logLevels.ERROR);

//example usage:
const logger = Logger.getInstance();

function myFunction() 
{
  try {
    // Do something
  } catch (error) {
    logger.error('An error occurred: ', error);
  }
}
myFunction();
  • Benefits:
  1. Centralized logging: The Singleton pattern ensures that all logs are written to the same place, which makes it easier to troubleshoot problems.
  2. Improved performance: The Singleton pattern ensures that the logger is instantiated only once, which can improve the performance of your application.
  3. Reduced errors: The Singleton pattern helps to prevent multiple conflicting logs, which can reduce errors in your application. 4 Easier maintenance: The Singleton pattern makes it easier to maintain your application's logging configuration, as you can make changes to the logger in one place and the changes will be reflected throughout your application.
prototype pattern

Prototype Design Pattern

The Prototype pattern is a creational design pattern that allows you to create new objects by copying an existing object, known as the prototype.

  • Intent: To create new objects by copying an existing object, known as the prototype, thus avoiding the need to create new instances from scratch.

  • Why: When the cost of creating an object is more expensive or complex than copying an existing one, or when instances of a class can have one of only a few different combinations of state.

  • How: By implementing a clone method in the prototype object, which other objects can call to create a copy of themselves.

class Prototype 
{
  constructor() 
  { this.property = 'default'; }

  clone() 
  {
    const clone = Object.create(this);
    clone.property = this.property; // Copying state
    return clone;
  }
}
  • Explanation:
  1. The Prototype class defines a clone method that creates a new object using Object.create(this), which sets the new object's prototype to the current object.
  2. State is copied from the existing object to the new object.
  • Application Scenario:

Take for example a system where users can customize the appearance of their avatars. Instead of creating a new avatar from scratch every time, you can use the Prototype pattern.

class Avatar extends Prototype 
{
  constructor() 
  {
    super();
    this.color = 'blue';
    this.accessories = [];
  }

  addAccessory(accessory) 
  { this.accessories.push(accessory); }
}

To create a new avatar, clone an existing one and modify its state:

const defaultAvatar = new Avatar();

// Create a customized avatar by cloning the default one
const userAvatar = defaultAvatar.clone();
userAvatar.color = 'red';
userAvatar.addAccessory('hat');

// Original avatar remains unchanged
console.log(defaultAvatar); // { property: 'default', color: 'blue', accessories: [] }

// Customized avatar
console.log(userAvatar); // { property: 'default', color: 'red', accessories: ['hat'] }
  • Benefits:
  1. Efficient Object Creation: Avoids the overhead of creating new objects from scratch, especially when the construction process is complex.
  2. Dynamic Object Creation: Allows for dynamic creation of objects with different configurations at runtime.
  3. Reduced Code Duplication: Promotes code reuse by allowing objects to be cloned and modified rather than explicitly created each time.
builder pattern

Builder Design Pattern

The Builder pattern is a creational design pattern that separates the construction of a complex object from its representation, allowing the same construction process to create different representations.

  • Intent: To separate the construction of a complex object from its representation so that the same construction process can create different representations.

  • Why: When an object needs to be constructed with numerous possible configurations, or when the construction process is complex and involves multiple steps.

  • How: By defining a builder interface with methods for constructing parts of the object and a director class that orchestrates the construction process using a concrete builder.

For eaxample developing a learning platform where instructors can create customized online courses with various content types. The Builder pattern can be applied to construct different types of courses step by step.

// Product: Course
class Course 
{
  constructor() 
  { this.modules = []; }

  addModule(module) 
  { this.modules.push(module); }

  showModules() 
  {
    console.log("Course Modules:");
    for (const module of this.modules) 
    { console.log(`- ${module.title}`); }
  }
}

// Builder Interface: CourseBuilder
class CourseBuilder 
{
  constructor() 
  { this.course = new Course(); }

  buildIntroduction() {}
  buildVideoModule() {}
  buildQuizModule() {}

  getCourse() 
  { return this.course; }
}

// Concrete Builder 1: BeginnerCourseBuilder
class BeginnerCourseBuilder extends CourseBuilder 
{
  buildIntroduction() 
  { this.course.addModule({ title: 'Introduction to the Course', type: 'Introduction' }); }

  buildVideoModule() 
  { this.course.addModule({ title: 'Getting Started with Basics', type: 'Video' }); }

  buildQuizModule() 
  { this.course.addModule({ title: 'Quiz on Basics', type: 'Quiz' }); }
}

// Concrete Builder 2: AdvancedCourseBuilder
class AdvancedCourseBuilder extends CourseBuilder 
{
  buildIntroduction() 
  { this.course.addModule({ title: 'Advanced Concepts Overview', type: 'Introduction' }); }

  buildVideoModule() 
  { this.course.addModule({ title: 'In-Depth Video Lectures', type: 'Video' }); }

  buildQuizModule() 
  { this.course.addModule({ title: 'Challenging Quiz Section', type: 'Quiz' }); }
}

// Director: CourseDirector
class CourseDirector 
{
  construct(builder) 
  {
    builder.buildIntroduction();
    builder.buildVideoModule();
    builder.buildQuizModule();
  }
}
  • Usage:
// Client Code
const beginnerCourseBuilder = new BeginnerCourseBuilder();
const advancedCourseBuilder = new AdvancedCourseBuilder();

const courseDirector = new CourseDirector();

courseDirector.construct(beginnerCourseBuilder);
const beginnerCourse = beginnerCourseBuilder.getCourse();
beginnerCourse.showModules();
// Output:
// Course Modules:
// - Introduction to the Course
// - Getting Started with Basics
// - Quiz on Basics

courseDirector.construct(advancedCourseBuilder);
const advancedCourse = advancedCourseBuilder.getCourse();
advancedCourse.showModules();
// Output:
// Course Modules:
// - Advanced Concepts Overview
// - In-Depth Video Lectures
// - Challenging Quiz Section
  • Benefits:
  1. Customization: Instructors can create courses with different structures and content types without the need to know the internal details of the course construction process.
  2. Easy Modification: The construction process can be easily modified or extended to support new content types or course structures.
  3. Reusability: Course builders can be reused to create multiple courses with similar structures, reducing code duplication.
factory method pattern

Factory Method Design Pattern

The Factory Method pattern is a creational design pattern that provides an interface for creating instances of a class, but it allows subclasses to alter the type of objects that will be created. It falls under the category of "creational patterns" as it deals with the process of object creation.

  • Intent: The primary goal is to define an interface for creating objects in a superclass, but defer the instantiation to subclasses, allowing a class to delegate the responsibility of instantiating its objects.

  • Why: This pattern is useful when the exact class of an object is not known until runtime or when the creation process involves multiple steps that subclasses should implement.

  • How: The pattern involves defining a creator interface or an abstract class with a method (the factory method) for creating objects. Concrete subclasses then implement this method to produce objects of specific types, adhering to the common interface.

Key Components:

  1. Product Interface (or Abstract Class): Defines the interface of the objects that the factory method creates.

  2. Concrete Products: Classes that implement the product interface, representing different variations of objects.

  3. Creator Interface (or Abstract Class): Declares the factory method, which returns an object of the product interface type.

  4. Concrete Creators: Subclasses that implement the factory method to create instances of specific concrete products.

Problem Scenario:

A reader for the RTF (Rich Text Format) document exchange format needs to convert RTF documents into multiple text formats such as plain ASCII text, interactive text widgets, etc. The challenge is that the number of possible conversions is open-ended, and new conversions should be easily incorporable without modifying the RTFReader class.

  • Solution:

The Factory Method pattern can be employed by configuring the RTFReader class with a TextConverter object responsible for the conversion. The RTFReader will utilize the TextConverter to convert RTF tokens as it parses the document.

// Product Interface: TextConverter
class TextConverter 
{ convertTextToken(token) {} }

// Concrete Products: ASCIIConverter, TeXConverter, TextWidgetConverter
class ASCIIConverter extends TextConverter 
{
  convertTextToken(token) 
  {
    // Conversion logic for plain ASCII text
    console.log(`Converting to ASCII: ${token}`);
  }
}

class TeXConverter extends TextConverter 
{
  convertTextToken(token) 
  {
    // Conversion logic for TeX representation
    console.log(`Converting to TeX: ${token}`);
  }
}

class TextWidgetConverter extends TextConverter 
{
  convertTextToken(token) 
  {
    // Conversion logic for interactive text widget
    console.log(`Converting to Text Widget: ${token}`);
  }
}

// Creator Interface: RTFReader
class RTFReader 
{
  constructor(converter) 
  { this.converter = converter; }

  parseRTFDocument(rtfDocument) 
  {
    // Parsing logic...
    const tokens = ['token1', 'token2', 'token3']; // Example tokens

    for (const token of tokens) 
    { this.converter.convertTextToken(token); }
  }
}

// Concrete Creators: RTFReader for different conversions
const asciiConverter = new ASCIIConverter();
const rtfReaderWithAsciiConverter = new RTFReader(asciiConverter);

const texConverter = new TeXConverter();
const rtfReaderWithTeXConverter = new RTFReader(texConverter);

const textWidgetConverter = new TextWidgetConverter();
const rtfReaderWithTextWidgetConverter = new RTFReader(textWidgetConverter);

// Client Code
const sampleRTFDocument = "Sample RTF document content...";

rtfReaderWithAsciiConverter.parseRTFDocument(sampleRTFDocument);
rtfReaderWithTeXConverter.parseRTFDocument(sampleRTFDocument);
rtfReaderWithTextWidgetConverter.parseRTFDocument(sampleRTFDocument);
  • Explanation:
  1. Product Interface (TextConverter): Defines the common interface for concrete converters.

  2. Concrete Products (ASCIIConverter, TeXConverter, TextWidgetConverter): Classes implementing the TextConverter interface, representing different text formats.

  3. Creator Interface (RTFReader): Declares the factory method (convertTextToken) to create product instances. Configures the RTFReader with a specific converter.

  4. Concrete Creators (RTFReader for different conversions): Instances of RTFReader configured with specific converters for different conversions.

  • Benefits:
  1. Extensibility: New conversion types can be easily added by creating additional concrete product classes and corresponding creators.

  2. Encapsulation: The responsibility of converting RTF tokens is encapsulated within the converter classes, promoting a clear separation of concerns.

  3. Flexibility: Clients (RTFReader instances) can work with different types of text converters without knowing the specific class of the converter they are using.

abstract factory pattern

Abstract Factory Design Pattern

  • Intent: The Abstract Factory pattern provides an interface for creating families of related or dependent objects without specifying their concrete classes.

  • Why: It is useful when a system needs to be independent of how its objects are created, composed, and represented, and the system is configured with multiple families of objects.

  • How: By defining interfaces for each type of product and their corresponding factories. Concrete implementations of these interfaces are then created to produce specific sets of related objects.

This pattern allows for the creation of interchangeable families of objects, promoting system flexibility and independence from the specifics of object creation. The abstract factory provides a unified interface for creating products, ensuring that the created objects are compatible and consistent within their families.

  • Example usage: Let's consider a multimedia application that supports different platforms and needs to create various UI elements, such as buttons and sliders, in a way that is independent of the underlying operating system. The Abstract Factory pattern can help achieve this by providing an abstraction for creating families of related UI components.

The application, might have different UI components tailored for various platforms, such as Windows and macOS. The goal is to create a flexible system where the UI components can be switched based on the underlying platform.

// Abstract Product Interface: Button
class Button 
{ click() {} }

// Concrete Products: WindowsButton, MacOSButton
class WindowsButton extends Button 
{
  click() 
  { console.log('Windows button clicked'); }
}

class MacOSButton extends Button 
{
  click() 
  { console.log('MacOS button clicked'); }
}

// Abstract Product Interface: Slider
class Slider 
{ move() {} }

// Concrete Products: WindowsSlider, MacOSSlider
class WindowsSlider extends Slider 
{
  move() 
  { console.log('Windows slider moved'); }
}

class MacOSSlider extends Slider 
{
  move() 
  { console.log('MacOS slider moved'); }
}

// Abstract Factory Interface: UIFactory
class UIFactory 
{
  createButton() {}
  createSlider() {}
}

// Concrete Factories: WindowsUIFactory, MacOSUIFactory
class WindowsUIFactory extends UIFactory 
{
  createButton() 
  { return new WindowsButton(); }

  createSlider() 
  { return new WindowsSlider(); }
}

class MacOSUIFactory extends UIFactory 
{
  createButton() 
  { return new MacOSButton(); }

  createSlider() 
  { return new MacOSSlider(); }
}
  • Explanation:
  1. Abstract Product Interfaces (Button, Slider): Define the common interface for product families.

  2. Concrete Products (WindowsButton, MacOSButton, WindowsSlider, MacOSSlider): Classes implementing the interfaces, representing different products for specific platforms.

  3. Abstract Factory Interface (UIFactory): Declares the abstract factory methods for creating product families.

  4. Concrete Factories (WindowsUIFactory, MacOSUIFactory): Implement the abstract factory methods to create specific products for each platform.

  • Benefits:
  1. Platform Independence: The multimedia application remains independent of the underlying platform, allowing it to switch UI components dynamically.

  2. Flexibility: By changing the concrete factory used, the application can adapt to different platforms without modifying the core logic.

  3. Encapsulation: The responsibility of creating related UI components is encapsulated within the factories, promoting a clear separation of concerns.

Subscribe to the Newsletter

Get my latest posts and project updates by email