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!