Writing Clean Code Part 3 || What are Creational Design Patterns? How do I use them? And why do I care?

I have received numerous inquiries and comments regarding the anticipated release date for the forthcoming installment of this series. While my previous response to such queries was that the release would occur “when I have time”, I am pleased to announce that part 3 of this clean code series is now available for readers to peruse.

Before delving into the content of this article, I would like to establish a set of assumptions for the reader. I will assume that all readers possess a fundamental understanding of Object-Oriented Development and its application in the development of applications. Furthermore, I will presume that readers have read parts 1 and 2 of this series. For those who have not had the opportunity to read my previous articles on OOP development or part 1 and 2 of this series, the relevant links can be found here(OOP Article, Part 1, Part 2).

In this article, we will be discussing the concept of creational design patterns and their relevance in ROBLOX development. One of the fundamental principles of Object-Oriented Programming (OOP) is encapsulation, which aims to hide the inner workings of objects from end-users. This is essential to ensure that the object’s functionality is not compromised, and it is used for various reasons, including security and protecting intellectual property. Creational design patterns are a set of patterns that address the process of object creation and provide developers with ways to create objects while hiding the creation logic from the client.

Creational design patterns are useful in scenarios where the creation of objects involves complex logic or sensitive information that must be kept secure. For example, if an object contains sensitive information such as passwords or credit card details, it should be created in a way that hides this information from the end-user. Creational patterns provide a secure, flexible, and efficient way to create objects, while also adhering to the principles of encapsulation.

Here is a list of the creational design patterns their definitions:
• Factory Method Pattern: This pattern defines an interface for creating objects but delegates the instantiation of those objects to its subclasses. This provides a flexible way to create objects without the client code having to know the details of the object creation process.

• Abstract Factory Pattern: This pattern provides an interface for creating families of related objects. It allows clients to create objects without having to know the specific classes being instantiated, making it a good choice for creating objects that have dependencies.

• Singleton Pattern: This pattern ensures that only one instance of a class is created and provides global access to that instance. This is useful in situations where there should only be one instance of a particular object, such as in a database connection.

• Builder Pattern: This pattern provides a way to create complex objects step-by-step, allowing for greater flexibility and control over the creation process. This is useful when there are many optional parameters that can be set during object creation.

• Prototype Pattern: This pattern provides a way to create new objects by cloning existing objects. This can be useful when creating new objects is expensive or when there are many instances of an object that are similar.

Now, lets talk about real world examples of each of these patterns.
The Factory Method Pattern is a useful design pattern for creating different types of objects without needing to know the specific classes that must be instantiated. It is particularly beneficial when we have an object with an unknown implementation of an interface that needs to be instantiated during runtime based on specific criteria. This week, I utilized this pattern to implement a logging and notification rule system to handle error logging on one of our core applications.

The system’s implementation of the factory pattern was somewhat complex, but I will attempt to simplify it as much as possible. I began by defining two objects: one to be saved to our database and the other to be serialized in JSON format to the saved object. The object saved to the database is called IssueType and contains all necessary data for a particular issue type. For example, we may have various issues based on what we are logging, and this class would contain that information. The second class, NotificationRule, was a base class with several derived classes that had different attributes depending on the usage of the rule. For instance, I could have an interval attribute defined on one of the derived classes that the rule handler would use to determine when we notify.

We had two defined issue types, one with a notification rule attached that notifies constantly and another that notifies after an issue has occurred longer than a set interval of time from the last resolved issue. Each issue type had an Enum attached in the database, allowing us to attach the Enum to the specific implementation of the notification rule. The factory class that handled the notification rule was called NotificationHandlerFactory and contained a custom-made directory that returned a delegated method that instantiated and returned the required object that implemented the interface based on the specified IssueType. The object that was returned held the implementation of the notification logic, specifying how exactly we handle the notification.

Now, lets look at some code:
Notification Rules Module

return{
   ConstantNotifyOnFailRule = function() : {}
   return {}
   end,
   NotifyOnInterval = function() : {NotificationInterval : number}
   return {
      NotificationInterval = 3 * 60; -- 3 min in secods
   }
   end,
}

NotificationRuleHandler Module

local NotificationHandlerFactory = {}

export type LoggedIssue = {
   Reason : string,
   Created : DateTime,
}
export type IssueType = {
   Name : string,
   Created : DateTime,
   NotificationRule : string,
}
export type NotificationHandlerArgs = {
   LoggedIssueDef : LoggedIssue,
   IssueTypeDef : IssueType,
}

export type INotificationHandler = (NotificationHandlerArgs) -> (boolean)
local NotificationHandlers : {INotificationHandler} = {
   function(args : NotificationHandlerArgs) : boolean
      return true
      end,

      function(args : NotificationHandlerArgs) : boolean
         local HTTP = game:GetService("HttpService")
         local rule = HTTP:JSONDecode(args.IssueTypeDef.NotificationRule)
         if rule.NotificationInterval > os.clock() then
            return true
         end

         return false
         end,
      }


      function NotificationHandlerFactory.GetNotificationHandler(IssueType : number) : INotificationHandler?
         return NotificationHandlers[IssueType] :: INotificationHandler?
      end


      return NotificationHandlerFactory

This provided implementation is all I can provide of the system, any more an I would be breaching the Non-Disclosure I signed with my company. Just know this uses the factory design pattern to return an object that determines if we notify or not.

A game development example of this pattern is a factory that returns a gun object based on the type. We can make a function that returns a gun data without having to expose the logic externally. This prevents us from changing the gun data externally.

export type GunData = {
   Damage : number,
   FireRate : number,
   FireMode : number
}

local GunData : {[string] : GunData} = {

   MP5 = {
      Damage = 5;
      FireRate = 650;
      FireMode = 1;
   }

}

local function CopyTable(t)
   if not t then return {} end
   local c = {}
   for Index, Value in next, t do
      if typeof(Value) == 'table' then
         c[Index] = CopyTable(Value)
      else
         c[Index] = Value
      end
   end

   return c
end

return function(GunName : string) : GunData
return CopyTable(GunData[GunName])
end

For the same of simplicity I will not be using LuaU past this point.
Here is an example of the abstract factory pattern:

-- Define an abstract class for creating different types of vehicles
local VehicleFactory = {}
VehicleFactory.__index = VehicleFactory

function VehicleFactory:createCar()
   error("This method must be overridden in a subclass")
end

function VehicleFactory:createBike()
   error("This method must be overridden in a subclass")
end

-- Define a concrete class that creates a set of related vehicles
local FordFactory = {}
FordFactory.__index = FordFactory
setmetatable(FordFactory, { __index = VehicleFactory })

function FordFactory:createCar()
   return FordCar.new()
end

function FordFactory:createBike()
   return FordBike.new()
end

-- Define another concrete class that creates a different set of related vehicles
local HondaFactory = {}
HondaFactory.__index = HondaFactory
setmetatable(HondaFactory, { __index = VehicleFactory })

function HondaFactory:createCar()
   return HondaCar.new()
end

function HondaFactory:createBike()
   return HondaBike.new()
end

-- Define the base classes for different types of vehicles
local Car = {}
Car.__index = Car

function Car:new()
   error("This method must be overridden in a subclass")
end

function Car:drive()
   print("Driving a car")
end

local Bike = {}
Bike.__index = Bike

function Bike:new()
   error("This method must be overridden in a subclass")
end

function Bike:ride()
   print("Riding a bike")
end

-- Define the concrete classes for each type of vehicle
local FordCar = {}
FordCar.__index = FordCar
setmetatable(FordCar, { __index = Car })

function FordCar:new()
   local self = setmetatable({}, FordCar)
   return self
end

function FordCar:drive()
   print("Driving a Ford car")
end

local FordBike = {}
FordBike.__index = FordBike
setmetatable(FordBike, { __index = Bike })

function FordBike:new()
   local self = setmetatable({}, FordBike)
   return self
end

function FordBike:ride()
   print("Riding a Ford bike")
end

local HondaCar = {}
HondaCar.__index = HondaCar
setmetatable(HondaCar, { __index = Car })

function HondaCar:new()
   local self = setmetatable({}, HondaCar)
   return self
end

function HondaCar:drive()
   print("Driving a Honda car")
end

local HondaBike = {}
HondaBike.__index = HondaBike
setmetatable(HondaBike, { __index = Bike })

function HondaBike:new()
   local self = setmetatable({}, HondaBike)
   return self
end

function HondaBike:ride()
   print("Riding a Honda bike")
end

-- Create a factory instance based on user input
function createFactory(type)
   if type == "Ford" then
      return FordFactory
   elseif type == "Honda" then
      return HondaFactory
   else
      error("Invalid factory type")
   end
end

-- Use the factory to create related vehicles
local factory = createFactory("Ford")
local car = factory:createCar()
local bike = factory:createBike()
car:drive()   -- Output: "Driving a Ford car"
bike:ride()   -- Output: "Riding a Ford bike"

The singleton is a pattern that I use quite frequently. It’s a pattern where we will always have one object of that specific type. When I did ROBLOX development I used this pattern when I needed to create multiple services and make one globalized access point for each of those services. For example, in a combat game I was working on, I had a CombatSystem, InventorySystem, NPCSystem, and a few other systems all of which were created and put in a module called, GameSystems. This object contained all of the game Systems and allowed only one of each system to be created at a time.

Here is some code of this

-- Define the GameSystems module
local GameSystems = {}
GameSystems.__index = GameSystems

-- Define the CombatSystem class
local CombatSystem = {}
CombatSystem.__index = CombatSystem

function CombatSystem.New()
    local self = setmetatable({}, CombatSystem)
    -- Initialize the CombatSystem here
    return self
end

-- Define the InventorySystem class
local InventorySystem = {}
InventorySystem.__index = InventorySystem

function InventorySystem.New()
    local self = setmetatable({}, InventorySystem)
    -- Initialize the InventorySystem here
    return self
end

-- Define the NPCSystem class
local NPCSystem = {}
NPCSystem.__index = NPCSystem

function NPCSystem.New()
    local self = setmetatable({}, NPCSystem)
    -- Initialize the NPCSystem here
    return self
end

-- Create the singleton instance of the GameSystems module
local Instance = nil

function GameSystems.GetInstance()
    if not Instance then
        Instance = setmetatable({
            CombatSystem = CombatSystem.New(),
            InventorySystem = InventorySystem.New(),
            NPCSystem = NPCSystem.New()
        }, GameSystems)
    end
    return Instance
end

Notice how we also use somewhat of a factory pattern implementation for this SystemsModule as well. This allows us to keep the logic contained and secure.

The Builder pattern is another useful pattern that allows us to create objects in separate components rather than at one time. If there are memory constraints this can be useful because you can create an object with all null attributes and init those attributes on runtime as needed.

A real-world example of the Builder Pattern is building a custom PC on a website. Let’s say you are a computer parts retailer, and you want to offer your customers the ability to customize and build their own PC from a selection of components. The Builder Pattern can be used to create a flexible and extensible system for building and configuring custom PCs.

-- Define a PC class
local Pc = {}
Pc.__index = Pc

function Pc.New(builder)
    local self = setmetatable({}, Pc)
    self.Case = builder.Case
    self.Motherboard = builder.Motherboard
    self.Cpu = builder.Cpu
    self.Gpu = builder.Gpu
    self.Ram = builder.Ram
    self.Storage = builder.Storage
    return self
end

-- Define a Builder class for building PCs
local PcBuilder = {}
PcBuilder.__index = PcBuilder

function PcBuilder.New()
    local self = setmetatable({}, PcBuilder)
    self.Case = nil
    self.Motherboard = nil
    self.Cpu = nil
    self.Gpu = nil
    self.Ram = nil
    self.Storage = nil
    return self
end

function PcBuilder:SetCase(case)
    self.Case = case
    return self
end

function PcBuilder:SetMotherboard(motherboard)
    self.Motherboard = motherboard
    return self
end

function PcBuilder:SetCpu(cpu)
    self.Cpu = cpu
    return self
end

function PcBuilder:SetGpu(gpu)
    self.Gpu = gpu
    return self
end

function PcBuilder:SetRam(ram)
    self.Ram = ram
    return self
end

function PcBuilder:SetStorage(storage)
    self.Storage = storage
    return self
end

-- Define some classes for PC components
local PcCase = {}
function PcCase.New()
    local self = {}
    -- Initialize the Case properties here
    return self
end

local PcMotherboard = {}
function PcMotherboard.New()
    local self = {}
    -- Initialize the Motherboard properties here
    return self
end

local PcCpu = {}
function PcCpu.New()
    local self = {}
    -- Initialize the CPU properties here
    return self
end

local PcGpu = {}
function PcGpu.New()
    local self = {}
    -- Initialize the GPU properties here
    return self
end

local PcRam = {}
function PcRam.New()
    local self = {}
    -- Initialize the RAM properties here
    return self
end

local PcStorage = {}
function PcStorage.New()
    local self = {}
    -- Initialize the Storage properties here
    return self
end

-- Example usage of the Builder Pattern to build a custom PC
local builder = PcBuilder.New()
local pc = Pc.New(builder:SetCase(PcCase.New())
                           :SetMotherboard(PcMotherboard.New())
                           :SetCpu(PcCpu.New())
                           :SetGpu(PcGpu.New())
                           :SetRam(PcRam.New())
                           :SetStorage(PcStorage.New()))

In this example, we define a PC class that represents a custom-built PC. The PC class has properties for the various PC components: case, motherboard, CPU, GPU, RAM, and storage. We also define a Builder class that is responsible for building and configuring the custom PC. The Builder class has methods for setting each of the PC component properties. Finally, we define classes for each of the PC components: Case, Motherboard, CPU, GPU, RAM, and Storage. These classes have new() methods that initialize the properties of the corresponding PC component.

A game development example of this pattern could be a character creation system.

-- Define a PlayerCharacter class
local PlayerCharacter = {}
PlayerCharacter.__index = PlayerCharacter

function PlayerCharacter.New(builder)
    local self = setmetatable({}, PlayerCharacter)
    self.Head = builder.Head
    self.Torso = builder.Torso
    self.LeftArm = builder.LeftArm
    self.RightArm = builder.RightArm
    self.LeftLeg = builder.LeftLeg
    self.RightLeg = builder.RightLeg
    self.Humanoid = builder.Humanoid
    return self
end

-- Define a Builder class for building player characters
local PlayerCharacterBuilder = {}
PlayerCharacterBuilder.__index = PlayerCharacterBuilder

function PlayerCharacterBuilder.New()
    local self = setmetatable({}, PlayerCharacterBuilder)
    self.Head = nil
    self.Torso = nil
    self.LeftArm = nil
    self.RightArm = nil
    self.LeftLeg = nil
    self.RightLeg = nil
    self.Humanoid = nil
    return self
end

function PlayerCharacterBuilder:SetHead(head)
    self.Head = head
    return self
end

function PlayerCharacterBuilder:SetTorso(torso)
    self.Torso = torso
    return self
end

function PlayerCharacterBuilder:SetLeftArm(leftArm)
    self.LeftArm = leftArm
    return self
end

function PlayerCharacterBuilder:SetRightArm(rightArm)
    self.RightArm = rightArm
    return self
end

function PlayerCharacterBuilder:SetLeftLeg(leftLeg)
    self.LeftLeg = leftLeg
    return self
end

function PlayerCharacterBuilder:SetRightLeg(rightLeg)
    self.RightLeg = rightLeg
    return self
end

function PlayerCharacterBuilder:SetHumanoid(humanoid)
    self.Humanoid = humanoid
    return self
end

-- Example usage of the Builder Pattern to create a player character
local builder = PlayerCharacterBuilder.New()
local playerCharacter = PlayerCharacter.New(builder:SetHead(head)
                                                  :SetTorso(torso)
                                                  :SetLeftArm(leftArm)
                                                  :SetRightArm(rightArm)
                                                  :SetLeftLeg(leftLeg)
                                                  :SetRightLeg(rightLeg)
                                                  :SetHumanoid(humanoid))

There are many other ways to implement this pattern, but the idea remains the same. Whenever we want to create an object and simplify how much of it we want to create up front, the builder pattern is the way to go.

Now, the last pattern we will cover is the Prototype Pattern. In Lua, objects are prototyped rather than instantiated from classes, which means that new objects are created by copying an existing object, rather than defining a class and instantiating it.
This approach is implemented using tables and metatables. A table is an object that contains slots, and a metatable is a table that defines the behavior of other tables. When a table is created, it can be given a metatable that defines how to handle operations on the table, such as indexing and assignment.
To prototype an object in Lua, a table is created with the desired slots and behavior, and then other objects are created by cloning the table and modifying its slots as needed. The new objects inherit the behavior of the original object through its metatable.
The prototyping approach in Lua is useful because it allows for flexible and dynamic object creation, without the need to define classes and inheritance hierarchies. Objects can be modified and extended at runtime, and multiple inheritance can be achieved by combining multiple prototypes.

-- create a class (metatable)
local Class = {}
Class.__index = Class -- set the metatable's __index to itself

-- create a constructor
function Class:new(name)
  local obj = { name = name }
  setmetatable(obj, Class) -- set the object's metatable to the class
  return obj
end

-- create a method
function Class:sayHello()
  print("Hello, my name is " .. self.name)
end

-- create an object (table) from the class
local obj = Class:new("John")

-- call the object's method
obj:sayHello() --  Hello, my name is John

Out side of the theoretics of the language, we can use this pattern to create reusable game objects with similar properties and behaviors. But remember that we need to make sure that we copy the table that we are referencing, otherwise each object that we create will reference the same address in the heap. This means whatever object uses that table, they would all have the same values.

-- define a prototype for a platform object
local platformPrototype = {
   width = 10,
   height = 2,
   color = Color3.fromRGB(255, 255, 255)
}

-- create a function to clone the platform prototype
function CloneTable(t)
   local c = {}
   for i, v in next, t do
      if typeof(v) == 'table' then
         c[i] = CloneTable(v)
      else
         c[i] = v
      end
   end
   return c
end

-- create a new platform object by cloning the prototype
local platform1 = CloneTable(platformPrototype)
platform1.width = 20
platform1.color = Color3.fromRGB(255, 0, 0)

-- create another platform object by cloning the prototype
local platform2 = CloneTable(platformPrototype)
platform2.height = 4
platform2.color = Color3.fromRGB(0, 0, 255)

-- create a function to spawn a platform object
function SpawnPlatform(position, platform)
   local part = Instance.new("Part")
   part.Anchored = true
   part.Size = Vector3.new(platform.width, platform.height, 1)
   part.Color = platform.color
   part.Position = position
   part.Parent = workspace
   return part
end

-- spawn the first platform object
local position = Vector3.new(0, 0, 0)
local part1 = SpawnPlatform(position, platform1)

-- spawn the second platform object
position = position + Vector3.new(0, platform1.height + 1, 0)
local part2 = SpawnPlatform(position, platform2)

In the code above, a prototype is defined for a platform object, which includes properties such as width, height, and color. A function is created to clone the platform prototype, which creates a deep copy of the prototype table.

Two platform objects are created by cloning the prototype and modifying their properties as needed. A function is also defined to spawn a platform object in the game world, which creates a new Part object and sets its properties based on the platform object’s properties.

The first platform object is spawned at position (0, 0, 0) with modified width and color properties, and the second platform object is spawned at a position above the first platform with modified height and color properties.

A project example of this pattern was when I was working on a simulator game. We used prototyping for the pets because each pet would have the same attributes, and when the server started up we populated a table full of the pet template with the data depending on the rarity of the pet. This made it easier to add new pets, because we didn’t have to hard code each pet, rather, we create one or two templates for the pets, and iterated through each of the rarity types.

Okay, that pretty much sums up creational design patterns. Now, what should you gain from this? Well, creational design patterns help us as the developer make creating objects much easier. This can be done by concealing the complex logic behind how we create certain objects or hiding sensitive information.

So why do you care? You care because it keeps your code clean, neat, and extendable.

That’s all folks!

26 Likes

Funnily enough, this is one of the topics that my final is over tomorrow. Excellently written, and is arguably a important facet of coding in general.

1 Like

Thank you! Next topic will be structural patterns.

1 Like

Following making classes extendable and not modifying the original class, how would you do this in lua?

Because lua is much like python and java script, OOP development is sort of a grey area. However, it is important to note, you can easily inherit from other meta-tables.

Here is a code example that came up with:

-- Define the Vehicle class
local Vehicle = {}
Vehicle.__index = Vehicle

function Vehicle.new(speed)
    local self = setmetatable({}, Vehicle)
    self.speed = speed or 0
    return self
end

function Vehicle:accelerate(amount)
    self.speed = self.speed + amount
end

function Vehicle:brake(amount)
    self.speed = self.speed - amount
end

-- Define the Car class inheriting from Vehicle
local Car = {}
Car.__index = Car
setmetatable(Car, {__index = Vehicle}) -- Inherit from Vehicle

function Car.new(speed, brand)
    local self = setmetatable(Vehicle.new(speed), Car)
    self.brand = brand or "Unknown"
    return self
end

function Car:honk()
    print("Honk honk!")
end

-- Example usage
local myCar = Car.new(0, "Toyota")
print("My car's speed:", myCar.speed) -- Output: My car's speed: 0
myCar:accelerate(50)
print("My car's speed after accelerating:", myCar.speed) -- Output: My car's speed after accelerating: 50
myCar:honk() -- Output: Honk honk!

Ooh I see. Do you have any sort of old open sourced projects on roblox that show any of these in real use?

Sorry I keep making replies to this old topic lol.

I have some Systems using the same style system as you showed, but some of the systems need to require each other. So would it be okay to do like roblox’s approach and doing “return class.new()”?

Example:

-- Define the GameSystems module
local GameSystems = {}
GameSystems.__index = GameSystems

-- Grab Game Systems
local GameUI = require(script.GameUI)
local CameraSystem = require(script.CameraSystem)
local CharacterSystem = require(script.CharacterSystem)

-- Singleton instance of the GameSystems module
local Instance = nil

function GameSystems.GetInstance()
	if not Instance then
		Instance = setmetatable({
			CameraSystem = CameraSystem,
			CharacterSystem = CharacterSystem,
			UI = GameUI.GetInstance(),
		}, GameSystems)
	end
	return Instance
end

return GameSystems
-- Imports
local CharacterSystem = require(script.Parent.CharacterSystem)

-- CameraSystem
local CameraSystem = {}
CameraSystem.__index = CameraSystem

function CameraSystem.new()
	local self = setmetatable({}, CameraSystem)
	RunService.Stepped:Connect(function(time, deltaTime)
		self:Update(deltaTime, true)
	end)
	
	CharacterSystem.Signals.Spawned:Connect(function()
		self:Update(0, false)
	end)
	
	if CharacterSystem:GetCharacter() then
		self:Update(0, false)
	end
	return self
end

-- Return
return CameraSystem.new()

At first it was just CharacterSystem that did return CharacterSystem.new() but I figured if one does it they all should. What do you think and what would your approach be to this?