DEV Community

Billy Okeyo
Billy Okeyo

Posted on • Originally published at billyokeyo.com on

Creational Design Patterns

In software engineering, especially software design, a number of techniques are used to address the development of the processes of creating complex objects, called creational design patterns. The purposes of these patterns include the storing and hiding of the instantiation of an object, thereby facilitating the reuse and flexibility of the source code. The possibility of design and usage of a software system many times determines how robust and maintainable this system will be. This article provides an overview of a number of creational design patterns including, but not limited to, the Singleton, Factory Method, Abstract Factory, Prototype, and Builder patterns with their approaches, implementations, and use cases.

This is a continuation of Introduction to Design Patterns , if you haven’t checked it out you can check it here

Creational Design Patterns: Where Objects Are Born

Definition of Creational Design Patterns

Think about how nice it would be if there is an unlimited scope to recreate Geographical Area objects without restrictions. Creational design patterns are the templates that help in achieving this vision. They specify the rules which are to be followed for the creation of objects in a way that is appropriate and which can be reversed when need be.

Importance of Creational Design Patterns

If you were a chef with a collection of recipes, developers would use different types of creational design patterns to cook an object in an orderly and sophisticated manner. These patterns assist in isolating the creation of an object away from the working code, therefore improving manageability and maintainability. In brief, these simplifications facilitate a usually complex process of creating an object.

1. Singleton Pattern

Let us welcome the attention seeking Singleton pattern, which most rightly might be considered the James Bond of the design patterns: the most elegant and the most reserved. This pattern guarantees that a class has only one object and provides a way of accessing that object. The very best in situations when you need to have one object which will be responsible, for instance, for the storage of configuration data or the control of the access to certain functionalities

Implementation of Singleton Pattern

In effect, we are adding creative touch in establishing a Singleton by introducing a static method that controls the process of object creation in such a way that only one object is created and no more. It’s like having a restricted access object where there will be no clones or copies of the object only exclusive usage for the person with granted permissions..

Use Cases for Singleton Pattern

Want to have one logger for all your application? Worry not, Singleton is here for you. One of the most useful patterns for managing things like the persistence of a database connection or the scope of a user session is the singleton pattern – when one instance is all you need.

using System;
using System.IO;

namespace SingletonPatternExample
{
    // Singleton Logger class
    public sealed class Logger
    {
        private static Logger _instance = null;
        private static readonly object _lock = new object();
        private string _logFilePath;

        // Private constructor to prevent direct instantiation
        private Logger()
        {
            // Log file path (can be any location you prefer)
            _logFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "log.txt");
        }

        // Public static property to access the single instance
        public static Logger Instance
        {
            get
            {
                // Thread-safe double-check locking to ensure only one instance is created
                if (_instance == null)
                {
                    lock (_lock)
                    {
                        if (_instance == null)
                        {
                            _instance = new Logger();
                        }
                    }
                }
                return _instance;
            }
        }

        // Method to log a message
        public void Log(string message)
        {
            string logMessage = $"{DateTime.Now}: {message}";
            Console.WriteLine(logMessage); // Output to the console
            File.AppendAllText(_logFilePath, logMessage + Environment.NewLine); // Append to log file
        }
    }

    // Example class using the Logger
    class Database
    {
        public void Connect()
        {
            Logger.Instance.Log("Database connected.");
        }
    }

    // Example class using the Logger
    class UserSession
    {
        public void StartSession(string userName)
        {
            Logger.Instance.Log($"User session started for {userName}.");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            // Using the singleton logger in different parts of the application
            Logger.Instance.Log("Application started.");

            Database db = new Database();
            db.Connect();

            UserSession session = new UserSession();
            session.StartSession("JohnDoe");

            Logger.Instance.Log("Application finished.");
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

Key Points

Logger Singleton : The Logger class is a Singleton with a private constructor to prevent multiple instances. It uses a lock to ensure thread safety in multi-threaded environments.

Log File : Logs are written to both the console and a text file log.txt within the application’s directory.

Double-Checked Locking : The Instance property ensures only one instance of the Logger is created, using double-checked locking to manage thread safety.

Multiple Usage Scenarios : Classes like Database and UserSession use the same logger instance to log their actions.

Output

Application started.
Database connected.
User session started for JohnDoe.
Application finished.

Enter fullscreen mode Exit fullscreen mode

And in the log.txt file, the same messages will be appended with timestamps.

2. Factory Method Pattern

Imagine a situation where there is a factory and different objects are produced based on a generic interface.Present pattern is an example of factory method design pattern. This pattern enables a class to handle object creation by passing on the word to the subclasses, who can figure out the exact class to be created.

Differences between Factory Method Pattern and Simple Factory Pattern

The Simple Factory pattern defines a single class in charge of object creation. For its part, the Factory Method Pattern allows dividing this responsibility between the classes by delegating the creation to subclass(es). It is self-service where one can make his or her own burger or sit back at the restaurant and order whatever is on the menu..

Example of Factory Method Pattern in Action

Let us consider an assembly line of vehicles where each and every factory(s subclass) makes a particular category of vehicles. Be it car factory making saloons or a bicycle factory making bicycle’s. The Factory Method pattern helps to create objects with ease without making the creator class do all the work.

using System;

namespace FactoryMethodPattern
{
    // Abstract Product: Vehicle
    public abstract class Vehicle
    {
        public abstract void Drive();
    }

    // Concrete Product: Car
    public class Car : Vehicle
    {
        public override void Drive()
        {
            Console.WriteLine("Driving a car.");
        }
    }

    // Concrete Product: Bike
    public class Bike : Vehicle
    {
        public override void Drive()
        {
            Console.WriteLine("Riding a bike.");
        }
    }

    // Abstract Creator: VehicleFactory
    public abstract class VehicleFactory
    {
        // Factory Method
        public abstract Vehicle CreateVehicle();

        public void AssembleVehicle()
        {
            // The factory method is called to create a vehicle.
            Vehicle vehicle = CreateVehicle();
            Console.WriteLine("Assembling vehicle.");
            vehicle.Drive();
        }
    }

    // Concrete Creator: CarFactory
    public class CarFactory : VehicleFactory
    {
        public override Vehicle CreateVehicle()
        {
            return new Car();
        }
    }

    // Concrete Creator: BikeFactory
    public class BikeFactory : VehicleFactory
    {
        public override Vehicle CreateVehicle()
        {
            return new Bike();
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            // Create a car factory and assemble a car
            VehicleFactory carFactory = new CarFactory();
            carFactory.AssembleVehicle();

            Console.WriteLine();

            // Create a bike factory and assemble a bike
            VehicleFactory bikeFactory = new BikeFactory();
            bikeFactory.AssembleVehicle();
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

Explanation

Abstract Product (Vehicle): Defines the interface for all vehicles. In this case, it has a method Drive().

Concrete Products (Car and Bike): These are the actual vehicle classes that inherit from the Vehicle class. Each concrete product provides its own implementation of the Drive() method.

Abstract Creator (VehicleFactory): Declares the Factory Method (CreateVehicle()), which subclasses must implement to return a specific product (vehicle). It also contains an operation AssembleVehicle() that relies on the factory method.

Concrete Creators (CarFactory and BikeFactory): These are the factories that implement the CreateVehicle() method to return specific types of vehicles (Car and Bike).

Output

Assembling vehicle.
Driving a car.

Assembling vehicle.
Riding a bike.
Enter fullscreen mode Exit fullscreen mode

How it Works

Factory Method : The CreateVehicle() method in VehicleFactory is abstract, allowing each subclass (i.e., CarFactory or BikeFactory) to define which type of vehicle to instantiate.

Flexible Creation : The client code (in this case, AssembleVehicle()) doesn’t know which concrete vehicle is being created; it simply calls the factory method, and the subclass takes care of the specific object creation.

3. Abstract Factory Pattern

Now when you start searching for the Abstract Factory pattern it is almost as if you have walk into a design room in which various sets of design objects are brought together at the same time. This design pattern allows us to create sets of related or dependent objects without the need for their concrete classes to be defined.

Relationship between Abstract Factory Pattern and Factory Method Pattern

So, Factory Method style aims on particular object creation, while working with the Abstract Factory pattern consolidates such objects into a group. See Factory Method as cooking one single meal, while in Abstract Factory you are preparing a number of different dishes from one specific cuisine type

Benefits of Abstract Factory Pattern:

Encapsulation of Object Creation : The factories encapsulate the creation of related objects, making it easy to extend with new product families (e.g., a Linux GUI).

Flexibility : You can change the family of objects used throughout the codebase without changing the client code, making your system more flexible.

Real-world Application of Abstract Factory Pattern

Whether it be producing parts of the application’s User Interface for every operating system or creating parts needed to connect the application with the databases of several different database vendors, the Abstract Factory style excels in areas where the need is to assemble sets of objects that are consistent and well integrated. It’s like getting a design set of custom parts pressed to fit together perfectly without the hassle of ever specifying which class to design for what part

In this example, we’ll create GUI components (buttons and checkboxes) for two different operating systems: Windows and Mac. The Abstract Factory pattern will allow us to create these components without tying our code to specific concrete classes.

using System;

namespace AbstractFactoryPattern
{
    // Abstract Product A: Button
    public interface IButton
    {
        void Paint();
    }

    // Concrete Product A1: Windows Button
    public class WindowsButton : IButton
    {
        public void Paint()
        {
            Console.WriteLine("Rendering a button in Windows style.");
        }
    }

    // Concrete Product A2: Mac Button
    public class MacButton : IButton
    {
        public void Paint()
        {
            Console.WriteLine("Rendering a button in Mac style.");
        }
    }

    // Abstract Product B: Checkbox
    public interface ICheckbox
    {
        void Paint();
    }

    // Concrete Product B1: Windows Checkbox
    public class WindowsCheckbox : ICheckbox
    {
        public void Paint()
        {
            Console.WriteLine("Rendering a checkbox in Windows style.");
        }
    }

    // Concrete Product B2: Mac Checkbox
    public class MacCheckbox : ICheckbox
    {
        public void Paint()
        {
            Console.WriteLine("Rendering a checkbox in Mac style.");
        }
    }

    // Abstract Factory: GUI Factory
    public interface IGUIFactory
    {
        IButton CreateButton();
        ICheckbox CreateCheckbox();
    }

    // Concrete Factory 1: Windows Factory
    public class WindowsFactory : IGUIFactory
    {
        public IButton CreateButton()
        {
            return new WindowsButton();
        }

        public ICheckbox CreateCheckbox()
        {
            return new WindowsCheckbox();
        }
    }

    // Concrete Factory 2: Mac Factory
    public class MacFactory : IGUIFactory
    {
        public IButton CreateButton()
        {
            return new MacButton();
        }

        public ICheckbox CreateCheckbox()
        {
            return new MacCheckbox();
        }
    }

    // Client class that uses the factories
    public class Application
    {
        private readonly IButton _button;
        private readonly ICheckbox _checkbox;

        // The client code works with factories through their abstract interface
        public Application(IGUIFactory factory)
        {
            _button = factory.CreateButton();
            _checkbox = factory.CreateCheckbox();
        }

        public void Render()
        {
            _button.Paint();
            _checkbox.Paint();
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            // Simulating client decision based on operating system
            IGUIFactory factory;

            Console.WriteLine("Select operating system (windows/mac): ");
            string os = Console.ReadLine().ToLower();

            if (os == "windows")
            {
                factory = new WindowsFactory();
            }
            else if (os == "mac")
            {
                factory = new MacFactory();
            }
            else
            {
                throw new ArgumentException("Unsupported operating system.");
            }

            // Create and use application
            Application app = new Application(factory);
            app.Render();
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

Explanation

Abstract Products (IButton and ICheckbox): These interfaces define the methods that all buttons and checkboxes must implement.

Concrete Products (WindowsButton, MacButton, WindowsCheckbox, MacCheckbox): These classes provide specific implementations of buttons and checkboxes for Windows and Mac operating systems.

Abstract Factory (IGUIFactory): This interface declares methods for creating abstract products (buttons and checkboxes).

Concrete Factories (WindowsFactory, MacFactory): These factories create the concrete products (Windows-style or Mac-style buttons and checkboxes).

Client (Application): The client class interacts with the factories through the abstract factory interface. It doesn’t know the concrete classes it’s dealing with, making the client code independent of the product-specific logic.

How It Works

When the client (user) selects an operating system (windows or mac), the corresponding factory is instantiated.

The client calls the Render() method, which internally creates and paints the buttons and checkboxes for the selected operating system.

The Abstract Factory pattern ensures that all products (buttons and checkboxes) are compatible, meaning you won’t mix, for example, a Windows button with a Mac checkbox.

If the user selects windows:

Rendering a button in Windows style.
Rendering a checkbox in Windows style.
Enter fullscreen mode Exit fullscreen mode

If the user selects mac:

Rendering a button in Mac style.
Rendering a checkbox in Mac style.
Enter fullscreen mode Exit fullscreen mode

Benefits of Abstract Factory Pattern:

  • Encapsulation of Object Creation : The factories encapsulate the creation of related objects, making it easy to extend with new product families (e.g., a Linux GUI).
  • Flexibility : You can change the family of objects used throughout the codebase without changing the client code, making your system more flexible.

4. Prototype Pattern

Some would say that the Prototype Pattern is a ‘redesign’ of the revision of design-purpose use. It lets you instantiate clones of existing objects without having your production code tethered to subclassed objects, it’s like an object Xerox machine

How Prototype Pattern Works

The Prototype Pattern does assist in avoiding wasteful and unproductive practice of reusing these resources. It is like having the ability to photocopy the objects

Scenario: Cloning Different Types of Cars

In this example, we’ll create different types of vehicles (car, bike) and clone them using the Prototype pattern.

using System;

namespace PrototypePattern
{
    // Prototype interface
    public abstract class Vehicle
    {
        public string Model { get; set; }
        public string Color { get; set; }

        // Abstract method to clone the object
        public abstract Vehicle Clone();

        public void DisplayInfo()
        {
            Console.WriteLine($"Vehicle: {this.GetType().Name}, Model: {Model}, Color: {Color}");
        }
    }

    // Concrete Prototype 1: Car
    public class Car : Vehicle
    {
        public int NumberOfDoors { get; set; }

        public Car(string model, string color, int numberOfDoors)
        {
            Model = model;
            Color = color;
            NumberOfDoors = numberOfDoors;
        }

        // Clone method to return a copy of the Car object
        public override Vehicle Clone()
        {
            Console.WriteLine("Cloning Car...");
            return (Vehicle)this.MemberwiseClone(); // Shallow copy
        }
    }

    // Concrete Prototype 2: Bike
    public class Bike : Vehicle
    {
        public bool HasCarrier { get; set; }

        public Bike(string model, string color, bool hasCarrier)
        {
            Model = model;
            Color = color;
            HasCarrier = hasCarrier;
        }

        // Clone method to return a copy of the Bike object
        public override Vehicle Clone()
        {
            Console.WriteLine("Cloning Bike...");
            return (Vehicle)this.MemberwiseClone(); // Shallow copy
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            // Original Car object
            Car originalCar = new Car("Sedan", "Red", 4);
            originalCar.DisplayInfo();

            // Clone the Car object
            Car clonedCar = (Car)originalCar.Clone();
            clonedCar.DisplayInfo();

            Console.WriteLine();

            // Original Bike object
            Bike originalBike = new Bike("Mountain Bike", "Blue", true);
            originalBike.DisplayInfo();

            // Clone the Bike object
            Bike clonedBike = (Bike)originalBike.Clone();
            clonedBike.DisplayInfo();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

Prototype Interface (Vehicle): The Vehicle abstract class defines a Clone() method that must be implemented by concrete prototypes. It also has common properties like Model and Color.

Concrete Prototypes (Car, Bike): The Car and Bike classes inherit from Vehicle and implement the Clone() method, which uses the MemberwiseClone() method to create a shallow copy of the object.

Shallow Copy : In this case, MemberwiseClone() creates a shallow copy of the object, meaning it copies the object itself but not any complex objects it references. This is suitable here since all the properties are value types (e.g., string, int, bool).

Output

Vehicle: Car, Model: Sedan, Color: Red
Cloning Car...
Vehicle: Car, Model: Sedan, Color: Red

Vehicle: Bike, Model: Mountain Bike, Color: Blue
Cloning Bike...
Vehicle: Bike, Model: Mountain Bike, Color: Blue
Enter fullscreen mode Exit fullscreen mode

Key Points

Cloning : The Prototype pattern allows creating a new object by copying an existing one. This can be more efficient than creating a new object from scratch, especially when object creation is costly.

Shallow Copy : In the example, we use MemberwiseClone() to perform a shallow copy. If the object has references to other objects, a deep copy might be required.

Flexibility : The client code (Program class) doesn’t depend on concrete classes like Car or Bike to create new objects. It can clone any object that implements the Clone() method.

Benefits of the Prototype Pattern:

  • Efficient Object Creation : Useful when creating objects is resource-intensive.
  • Less Subclassing : Reduces the need to create subclasses for object creation.
  • Dynamic Object Creation : Objects can be created dynamically at runtime without hardcoding types.

5. Builder Pattern

The Builder Pattern helps in designing complex objects the same way. ——-Your personal sandwich artist. Builder is the person who constructs and assembles the parts of complex objects, ändern is the solution that involves creating different flavors without mess managerial hands.

Comparison with Factory Pattern

Although, the Factory Pattern may be likened to placing an order from a pre-defined set of options, using the Builder Pattern one can place an order and add his own toppings or special request

Scenario: Building a Customizable Car

In this example, we’ll create a Car object, which can be built in different configurations (e.g., Sports Car, SUV). The Builder pattern will be used to handle the construction of the car’s features step by step.

using System;

namespace BuilderPattern
{
    // Product: The complex object we are going to build
    public class Car
    {
        public string Engine { get; set; }
        public string Transmission { get; set; }
        public int Wheels { get; set; }
        public bool HasGPS { get; set; }
        public bool HasSunroof { get; set; }

        public void DisplaySpecifications()
        {
            Console.WriteLine($"Engine: {Engine}");
            Console.WriteLine($"Transmission: {Transmission}");
            Console.WriteLine($"Wheels: {Wheels}");
            Console.WriteLine($"GPS: {(HasGPS ? "Yes" : "No")}");
            Console.WriteLine($"Sunroof: {(HasSunroof ? "Yes" : "No")}");
        }
    }

    // Abstract Builder: Defines the steps to create a product
    public abstract class CarBuilder
    {
        protected Car car;

        public void CreateNewCar()
        {
            car = new Car();
        }

        public Car GetCar()
        {
            return car;
        }

        public abstract void SetEngine();
        public abstract void SetTransmission();
        public abstract void SetWheels();
        public abstract void SetGPS();
        public abstract void SetSunroof();
    }

    // Concrete Builder 1: Builds a sports car
    public class SportsCarBuilder : CarBuilder
    {
        public override void SetEngine()
        {
            car.Engine = "V8 Engine";
        }

        public override void SetTransmission()
        {
            car.Transmission = "Manual";
        }

        public override void SetWheels()
        {
            car.Wheels = 4;
        }

        public override void SetGPS()
        {
            car.HasGPS = true;
        }

        public override void SetSunroof()
        {
            car.HasSunroof = true;
        }
    }

    // Concrete Builder 2: Builds an SUV
    public class SUVBuilder : CarBuilder
    {
        public override void SetEngine()
        {
            car.Engine = "V6 Engine";
        }

        public override void SetTransmission()
        {
            car.Transmission = "Automatic";
        }

        public override void SetWheels()
        {
            car.Wheels = 4;
        }

        public override void SetGPS()
        {
            car.HasGPS = true;
        }

        public override void SetSunroof()
        {
            car.HasSunroof = false;
        }
    }

    // Director: Controls the building process
    public class CarDirector
    {
        private CarBuilder _builder;

        public CarDirector(CarBuilder builder)
        {
            _builder = builder;
        }

        // Construct the car by calling the builder's methods step by step
        public void ConstructCar()
        {
            _builder.CreateNewCar();
            _builder.SetEngine();
            _builder.SetTransmission();
            _builder.SetWheels();
            _builder.SetGPS();
            _builder.SetSunroof();
        }

        public Car GetCar()
        {
            return _builder.GetCar();
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            // Building a sports car
            CarBuilder sportsCarBuilder = new SportsCarBuilder();
            CarDirector sportsCarDirector = new CarDirector(sportsCarBuilder);

            sportsCarDirector.ConstructCar();
            Car sportsCar = sportsCarDirector.GetCar();
            Console.WriteLine("Sports Car Specifications:");
            sportsCar.DisplaySpecifications();

            Console.WriteLine();

            // Building an SUV
            CarBuilder suvBuilder = new SUVBuilder();
            CarDirector suvDirector = new CarDirector(suvBuilder);

            suvDirector.ConstructCar();
            Car suv = suvDirector.GetCar();
            Console.WriteLine("SUV Specifications:");
            suv.DisplaySpecifications();
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

Explanation

Product (Car): This is the complex object being built. It has various attributes (e.g., engine, transmission, wheels, GPS, sunroof).

Abstract Builder (CarBuilder): Defines the steps required to build a car (e.g., setting the engine, transmission, wheels, etc.). Each step is abstract, allowing different builders to implement them in their own way.

Concrete Builders (SportsCarBuilder, SUVBuilder): Implement the steps to build a specific type of car. For example, a sports car might have a V8 engine and manual transmission, while an SUV has a V6 engine and automatic transmission.

Director (CarDirector): Controls the building process. It calls the builder’s methods step by step to construct the car. The director ensures the car is built in a specific sequence.

Client (Program): The client uses the director to build the car by providing a specific builder. Once the car is built, it is retrieved and displayed.

Output

Sports Car Specifications:
Engine: V8 Engine
Transmission: Manual
Wheels: 4
GPS: Yes
Sunroof: Yes

SUV Specifications:
Engine: V6 Engine
Transmission: Automatic
Wheels: 4
GPS: Yes
Sunroof: No

Enter fullscreen mode Exit fullscreen mode

How It Works

The Client decides what kind of car to build by selecting a builder (e.g., SportsCarBuilder or SUVBuilder).

The Director orchestrates the building process, ensuring the steps (e.g., engine, transmission, etc.) are followed in a particular order.

The Concrete Builder defines how each step is performed (e.g., a sports car has a V8 engine, while an SUV has a V6 engine).

Benefits of the Builder Pattern:

  • Encapsulation of Construction : The construction process is encapsulated in the builder, keeping the product creation details hidden.
  • Step-by-Step Construction : The director controls the process step by step, which is useful when building complex objects.
  • Reusability : The same construction process can build different types of products by swapping out builders (e.g., you can add a TruckBuilder without modifying the director or the product).

You can get the source code of the examples used on the github here

Top comments (0)