Simplifying Object Creation - Using The Factory Design Pattern In Everyday Development

In software engineering, a creational design pattern is a design pattern that deals with the process of object creation in a way that is both flexible and efficient. Creational design patterns provide general solutions for creating objects suitable for various situations without being tied to specific classes or configurations. These patterns help encapsulate the object creation process, making the code more flexible and easier to maintain. They can also improve the performance of an application by minimizing the number of object instances created. Examples of creational design patterns include Singleton, Factory Method, Abstract Factory, Builder, and Prototype patterns. Each pattern addresses a specific type of object creation problem and provides a standardized solution that can be adapted to various scenarios.

In this article, we will start with the Factory Method pattern. 

The Factory Design Pattern is a popular creational design pattern in software engineering that allows us to create objects without exposing the object creation logic to the client. This pattern is widely used in C# programming and helps create an object of a similar type based on the input parameter passed.

According to the Gang of Four, the Factory Design Pattern is defined as "A factory is an object which is used for creating other objects". In technical terms, a factory is a class with a method that creates and returns different types of objects based on the input parameter it receives. The Factory Design Pattern is particularly useful when we have a superclass and multiple subclasses and need to create and return an object of one of the subclasses based on the data provided. With the help of this pattern, we can encapsulate the object creation process and provide a common interface to the client to refer to the newly created object. The basic principle behind the Factory Design Pattern is to provide a flexible and efficient solution for creating objects that can be adapted to various situations. By utilizing this pattern, we can improve the performance of our application by minimizing the number of object instances created. Factory Design Pattern is a widely used creational design pattern that provides a way to create objects without exposing the object creation logic to the client. 

Factory Design Pattern example scenario 1

Suppose you own a pizza restaurant that offers different types of pizza to your customers, such as Cheese Pizza, Pepperoni Pizza, and Veggie Pizza. To implement this in your software system, you can create a hierarchy of pizza classes where a superclass called "Pizza" represents the common features of all pizzas, and each subclass represents a specific type of pizza. 

For example, the Pizza superclass can have common properties like "name", "description", and "price", and methods like "prepare", "bake", "cut", and "box". Each subclass can then override these methods and add its own implementation for the specific pizza type. Now, when a customer orders a specific pizza, you need to create an object of that particular pizza type and return it to the customer. Here's where the Factory Design Pattern comes into play. You can create a Factory class called "PizzaFactory" with a static method called "CreatePizza" that takes a string parameter representing the type of pizza the customer wants to order. In the "CreatePizza" method, you can use a switch statement or some other mechanism to create an object of the appropriate pizza subclass based on the input parameter. For example, if the input parameter is "cheese", you can create an object of the "Cheese Pizza" subclass. If the input parameter is "pepperoni", you can create an object of the "Pepperoni Pizza" subclass, and so on. Once the object of the specific pizza type is created, you can return it to the customer. The customer can then call the methods on the returned pizza object to prepare, bake, cut, and box the pizza.

Using the Factory Design Pattern in this example, you can encapsulate the object creation process and provide a common interface for the client (in this case, the customer) to refer to the newly created object. This approach also allows you to add more pizza types in the future without modifying the client code, making your system more modular and extensible.

Here's the complete code for the Pizza Restaurant example of the Factory Design Pattern in C#:

using System;
using System.Collections.Generic;
namespace FactoryMethodPattern_1 {
    public abstract class Pizza {
        public string Name {
            get;
            set;
        }
        public string Description {
            get;
            set;
        }
        public double Price {
            get;
            set;
        }
        public List < string > Toppings {
            get;
            set;
        }
        public abstract void Prepare();
        public abstract void Bake();
        public abstract void Cut();
        public abstract void Box();
    }
    public class CheesePizza: Pizza {
        public CheesePizza() {
            Name = "Cheese Pizza";
            Description = "Pizza with cheese and tomato sauce";
            Price = 8.99;
            Toppings = new List < string > {
                "Cheese",
                "Tomato Sauce"
            };
        }
        public override void Prepare() {
            Console.WriteLine("Preparing " + Name);
        }
        public override void Bake() {
            Console.WriteLine("Baking " + Name);
        }
        public override void Cut() {
            Console.WriteLine("Cutting " + Name);
        }
        public override void Box() {
            Console.WriteLine("Boxing " + Name);
        }
    }
    public class PepperoniPizza: Pizza {
        public PepperoniPizza() {
            Name = "Pepperoni Pizza";
            Description = "Pizza with pepperoni, cheese, and tomato sauce";
            Price = 9.99;
            Toppings = new List < string > {
                "Pepperoni",
                "Cheese",
                "Tomato Sauce"
            };
        }
        public override void Prepare() {
            Console.WriteLine("Preparing " + Name);
        }
        public override void Bake() {
            Console.WriteLine("Baking " + Name);
        }
        public override void Cut() {
            Console.WriteLine("Cutting " + Name);
        }
        public override void Box() {
            Console.WriteLine("Boxing " + Name);
        }
    }
    public class VeggiePizza: Pizza {
        public VeggiePizza() {
            Name = "Veggie Pizza";
            Description = "Pizza with veggies, cheese, and tomato sauce";
            Price = 10.99;
            Toppings = new List < string > {
                "Mushrooms",
                "Bell Peppers",
                "Onions",
                "Cheese",
                "Tomato Sauce"
            };
        }
        public override void Prepare() {
            Console.WriteLine("Preparing " + Name);
        }
        public override void Bake() {
            Console.WriteLine("Baking " + Name);
        }
        public override void Cut() {
            Console.WriteLine("Cutting " + Name);
        }
        public override void Box() {
            Console.WriteLine("Boxing " + Name);
        }
    }

Next, let's define the PizzaFactory class that will create objects of the appropriate pizza subclass based on the input parameter,

public class PizzaFactory {
    public Pizza CreatePizza(string pizzaType) {
        Pizza pizza = null;
        switch (pizzaType.ToLower()) {
            case "cheese":
                pizza = new CheesePizza();
                break;
            case "pepperoni":
                pizza = new PepperoniPizza();
                break;
            case "veggie":
                pizza = new VeggiePizza();
                break;
            default:
                // Console.WriteLine("Invalid pizza type. Please choose from cheese, pepperoni, or veggie.");
                // break;
                throw new ArgumentException("Invalid pizza type.");
        }
        return pizza;
    }
}

Finally, here's an example usage of the PizzaFactory to create and prepare a Cheese Pizza,

internal class Program {
    static void Main(string[] args) {
        try {
            Console.WriteLine("Welcome to Pizza Restaurant!");
            PizzaFactory pizzaFactory = new PizzaFactory();
            // create and prepare a Cheese Pizza
            Console.WriteLine("Please enter your choice from out of these three options: 1. cheese; 2. pepperoni; 3. veggie ");
            string pizzaType = Console.ReadLine();
            Pizza pizzaTypeObj = pizzaFactory.CreatePizza(pizzaType);
            pizzaTypeObj.Prepare();
            pizzaTypeObj.Bake();
            pizzaTypeObj.Cut();
            pizzaTypeObj.Box();
            Console.WriteLine("Enjoy your pizza!");
        } catch (ArgumentException ex) {
            Console.WriteLine(ex.Message);
        }
        Console.ReadLine();
    }
  }
}

This code demonstrates the usage of the Factory Design Pattern to create and prepare different types of pizzas. The PizzaFactory class creates and returns objects of the appropriate Pizza subclass based on the input parameter passed to the CreatePizza method.

To unit test the code, we can create a test project and write unit tests for the PizzaFactory.CreatePizza() method. Here's an example of a unit test using the NUnit testing framework:

using FactoryMethodPattern_1;
using NUnit.Framework;
using System;
namespace NUnitTest_FactoryMethodPattern_1 {
    public class PizzaFactoryTests {
        PizzaFactory pizzaFactory = new PizzaFactory();
        [Test]
        public void CreatePizza_CheesePizza_ReturnsCheesePizzaObject() {
                // Arrange
                string pizzaType = "cheese";
                // Act
                Pizza pizza = pizzaFactory.CreatePizza(pizzaType);
                // Assert
                Assert.IsInstanceOf(typeof(CheesePizza), pizza);
            }
            [Test]
        public void CreatePizza_InvalidPizzaType_ThrowsArgumentException() {
            // Arrange
            string pizzaType = "mushroom";
            // Act & Assert
            Assert.Throws < ArgumentException > (() => pizzaFactory.CreatePizza(pizzaType));
        }
    }
}

An instance of the PizzaFactory class is created.

" CreatePizza_CheesePizza_ReturnsCheesePizzaObjec" is the first test method that checks whether the CreatePizza method of the PizzaFactory class returns a CheesePizza object when the pizza type is set to "cheese".

  • Arrange sets up the input data for the test, in this case, the pizza type is set to "cheese".
  • Act calls the CreatePizza method of the PizzaFactory class to create a pizza object.
  • Assert checks whether the returned object is an instance of the CheesePizza class.

"CreatePizza_InvalidPizzaType_ThrowsArgumentException" is the second test method that checks whether an ArgumentException is thrown when the CreatePizza method of the PizzaFactory class is called with an invalid pizza type.

  • Arrange sets up the input data for the test, in this case, the pizza type is set to "mushroom".
  • Act & Assert checks whether an ArgumentException is thrown when the CreatePizza method of the PizzaFactory class is called with an invalid pizza type.

This test class checks whether the PizzaFactory class can create the desired pizza object based on the input data and whether it throws an exception when invalid input is provided.

Future improvements

The following points can make the above code more scalable and maintainable by customizing the Factory method pattern.

  1. Use an interface instead of an abstract class for the Pizza class. Since all the methods in the Pizza class are abstract and do not have any default implementation, using an interface instead of an abstract class would make more sense.
  2. Rename the PizzaFactory class to PizzaStore. In the Factory Design Pattern, the class that creates and returns objects is usually named as the factory. However, in our example, we are not just creating objects but also preparing and serving pizzas, which makes it more like a pizza store. So, it would be more appropriate to rename the class as PizzaStore.
  3. Use a Dictionary to store the Pizza objects instead of using switch-case statements. Using switch-case statements to create objects is not scalable and becomes harder to maintain as the number of classes increases. Instead, we can use a Dictionary to store the Pizza objects and retrieve them based on the input parameter.  
using System;
using System.Collections.Generic;
namespace FactoryMethodPattern_1 {
    public interface IPizza {
        void Prepare();
        void Bake();
        void Cut();
        void Box();
    }
    public class CheesePizza: IPizza {
        public void Prepare() {
            Console.WriteLine("Preparing Cheese Pizza...");
        }
        public void Bake() {
            Console.WriteLine("Baking Cheese Pizza...");
        }
        public void Cut() {
            Console.WriteLine("Cutting Cheese Pizza...");
        }
        public void Box() {
            Console.WriteLine("Boxing Cheese Pizza...");
        }
    }
    public class PepperoniPizza: IPizza {
        public void Prepare() {
            Console.WriteLine("Preparing Pepperoni Pizza...");
        }
        public void Bake() {
            Console.WriteLine("Baking Pepperoni Pizza...");
        }
        public void Cut() {
            Console.WriteLine("Cutting Pepperoni Pizza...");
        }
        public void Box() {
            Console.WriteLine("Boxing Pepperoni Pizza...");
        }
    }
    public class VeggiePizza: IPizza {
        public void Prepare() {
            Console.WriteLine("Preparing Veggie Pizza...");
        }
        public void Bake() {
            Console.WriteLine("Baking Veggie Pizza...");
        }
        public void Cut() {
            Console.WriteLine("Cutting Veggie Pizza...");
        }
        public void Box() {
            Console.WriteLine("Boxing Veggie Pizza...");
        }
    }
    public class PizzaStore {
        private Dictionary < string, IPizza > pizzaMenu = new Dictionary < string, IPizza > () {
            {
                "cheese",
                new CheesePizza()
            }, {
                "pepperoni",
                new PepperoniPizza()
            }, {
                "veggie",
                new VeggiePizza()
            }
        };
        public IPizza OrderPizza(string pizzaType) {
            if (pizzaMenu.ContainsKey(pizzaType.ToLower())) {
                IPizza pizza = pizzaMenu[pizzaType.ToLower()];
                pizza.Prepare();
                pizza.Bake();
                pizza.Cut();
                pizza.Box();
                return pizza;
            } else {
                Console.WriteLine($ "Sorry, we don't serve {pizzaType} pizza.");
                return null;
            }
        }
    }
    internal class Program {
        static void Main(string[] args) {
            try {
                Console.WriteLine("Welcome to Pizza Restaurant!");
                PizzaStore pizzaStore = new PizzaStore();
                // create and prepare a Cheese Pizza
                IPizza cheesePizza = pizzaStore.OrderPizza("cheese");
                Console.WriteLine("Enjoy your pizza!");
            } catch (ArgumentException ex) {
                Console.WriteLine(ex.Message);
            }
            Console.ReadLine();
        }
    }

Through this Factory Design Pattern in this real-time example, we can encapsulate the object creation process and provide a common interface for the client to refer to the newly created object. This approach also allows us to add more pizza types in the future without modifying the client code. Some common scenarios where the Factory Design Pattern is useful to include,

  • When we have a class hierarchy and want to create objects of different subclasses based on runtime parameters.
  • When we want to isolate the object creation logic from the client code.
  • When we want to abstract the creation of objects to a common interface so that clients can work with the objects without knowing their specific types.
  • When we want to centralize object creation logic in a single location, making it easier to modify and maintain in the future.

In the next article, we will see another working example where we can implement the Factory method pattern and focus more on exploring the common scenarios mentioned above.


Similar Articles