It’s harder to read code than to write it. - Joel Spolsky
Development can resemble déjà vu, solving recurring or similar problems. Design patterns help tap into the collective knowledge of software engineers who’ve already solved them.
Recommended Reads
- Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley.
- Head First Design Patterns. O’Reilly Media.
- Singleton
- Flyweight
- Serialization
- Scriptable Objects
- Finite State Machine
- Broker Chain
- Decorator
- Command
- Entity-Component System
- MVC
Singleton
- Ensure a class has one instance, and
- provide a global point of access to it.
Benefits
- It doesn’t create the instance if no one uses it
- It’s initialized at runtime.
- You can subclass the singleton
Avoid Singleton unless unavoidable
Poorly designed singletons are often “helpers” that add functionality to another class. If you can, just move all of that behavior into the Manager class it helps.
- Can make code less readable
- They encourage coupling.
- Difficult to be concurrency-friendly
- Lazy initialization takes control away from you
Alternatives
- Pass instance
- Store insatnce in a base class
- Use preexisting global class
- Service Locator
Single thread implementation
public class SimpleSingleton
{
private static SimpleSingleton instance;
//private constructor to not allow external initialization
private SimpleSingleton() { }
//static method to intialize class without creating class instance
public static SimpleSingleton Instance
{
get
{
if (instance == null)
{
instance = new SimpleSingleton();
}
return instance;
}
}
}
Flyweight Pattern
- Enables efficient sharing and reuse of objects.
- Particularly useful when dealing with large numbers of similar objects.
When you have tons of objects, memory can get eaten up fast, and performance can take a hit. The Flyweight Pattern helps by sharing objects to save memory. It splits the state into intrinsic (shared) and extrinsic (unique) parts, so shared objects can be reused in different situations at the same time.
Use Case
- Game Development
- Networking Applications
- Text Editors
Participants
- Flyweight Interface: Declares methods that flyweight objects must implement.
- Concrete Flyweight: Implements the Flyweight interface and stores intrinsic state.
- Unshared Concrete Flyweight: Not all Flyweight subclasses need to be shared.
- Flyweight Factory: Creates and manages Flyweight objects, ensuring shared instances are used.
- Client: Maintains references to flyweights and computes or stores extrinsic state.
Code
// Flyweight interface
interface Shape {
void draw(int x, int y);
}
// Concrete Flyweight
class Circle implements Shape {
private String color; // Intrinsic state
public Circle(String color) {
this.color = color;
}
@Override
public void draw(int x, int y) {
System.out.println("Drawing " + color + " circle at (" + x + "," + y + ")");
}
}
Circle class implemets Shape interface with intrinstic state color
that is shared. The draw
method uses this intrinsic state to darw the circle.
// Flyweight Factory
class ShapeFactory {
private static final Map<String, Shape> circleMap = new HashMap<>();
public static Shape getCircle(String color) {
Circle circle = (Circle) circleMap.get(color);
if (circle == null) {
circle = new Circle(color);
circleMap.put(color, circle);
System.out.println("Created " + color + " circle.");
}
return circle;
}
}
// Client
public class FlyweightPatternDemo {
public static void main(String[] args) {
Shape redCircle1 = ShapeFactory.getCircle("Red");
redCircle1.draw(10, 10);
Shape redCircle2 = ShapeFactory.getCircle("Red");
redCircle2.draw(20, 20);
Shape blueCircle = ShapeFactory.getCircle("Blue");
blueCircle.draw(30, 30);
}
}
Cons
One downside of the Flyweight Pattern is that the client (like our FlyweightPatternDemo
class) has to handle the extrinsic state. This means the client needs to keep track of things like the coordinates where the circles are drawn. If you mess up managing this state, it can lead to bugs.
Serialization
Serialization is the process of converting an object into a stream of bytes to store the object or transmit it to memory, a database, or a file.
Serialization (Object -> Bytes) -> File, Database or Memory -> (Bytes -> Object) Deserialisation
Scriptable Objects
- reduce memory usage, and simplify your code
- Can change values at runtime. Maintains new values after execution termination.
Uses flyweight pattern
First-class objects
:
- Store them in variables
- Dynamically create or destroy them at runtime
- Pass them as arguments
- Return them from a method
- Include them in data structures
- Serialize/deserialize them
[CreateAssetMenu(fileName="NPCConfig")]
public class NPCConfigSO : ScriptableObject
{
[Range(10, 100)]
public int maxHealth;
public NPCAIStateEnum goodHealthAi;
public NPCAIStateEnum lowHealthAi;
}
public class NPCHealth: MonoBehaviour
{
[SerializeField] NPCConfigSO config;
[SerializeField] int currentHealth;
}
Support a limited number of event functions, including Awake, OnEnable, OnDestroy, and OnDisable at runtime. The Editor also calls OnValidate and Reset from the Inspector.
The most common use for ScriptableObjects is as data containers
for shared data, particularly for static game configuration data that doesn’t change at runtime.
Think of ScriptableObject data as “read-only” relative to persistent data
Typical use cases of ScriptableObjects may include:
- Inventories
- Enemy, player, or item statistics
- Audio collections
data containers
: data that does not need to change at runtime
using System.IO;
public class LevelManager : MonoBehaviour
{
public ScriptableObject levelLayout;
public void LoadLevelFromJson(string jsonFile)
{
if (levelLayout == null)
{
levelLayout = ScriptableObject.
CreateInstance<LevelLayout>();
}
var importedFile = File.ReadAllText(jsonFile);
JsonUtility.FromJsonOverwrite(importedFile,
levelLayout);
}
}
But now bad actors can potentially tamper application. To avoid it, one has to use combination of all below three methods:
- Encryption
- Digital signatures (
fingerprint algorithm
to verify) - Server-side validation (Authority rejects anything manipulated)
Empty SO can also be used as Enum like comparisions. However, unlike enums, ScriptableObjects are easy to extend.
Using UnityEngine;
[CreateAssetMenu(fileName=”GameItem”)]
public class GameItem : ScriptableObject
{
// Every GameItem can now be used as Enum GameItemType
//Lets extend it with some business logic
public GameItem weakness;
public bool IsWinner(GameItem other)
{
return other.weakness == this;
}
}
Finite State Machine
A finite state machine can only be in one state at any moment in time
.
- has a finite number of states it can be in at any given time and,
- can operate on input to either make transitions from one state to another or, //ChangeState
- to cause an output or action to take place. //Execute
Idea is to decompose an object’s behavior into easily manageable “chunks” or states.
A naive approach is to use a series of if-else statements . A better mechanism for organizing states and affecting state transitions is a state transition table (A table of conditions and the states those conditions lead to.)
- Embed the rules for the state transitions within the states themselves.
- Provides an elegant way of implementing state-driven behavior.
- Add enter and exit actions to each state; and call them via the agent’s ChangeState method.
A Bowler in a cricket simulation game can be implemented as state machines. Considering some simple states for a bowler :-
- Idle
- Bowling Runup
- Deliver Bowl
- Field Bowl
Interestingly, whole team can also be implemented as FSMs, and can be in state of Batting or Fielding.
class Bowler
{
enum StateType(Idle, Runup, Deliver, Field);
Update()
{
state->execute();
}
UpdateState(StateType newState)
{
//delete currentState
//state = newState
}
}
class DeliverBowl: State
{
Execute()
{
//apply physics to ball with some force and spin
//throw event that ball has been bowled
//or Call batsman object with details of ball bowled
}
}
Broker Chain Pattern
// data class
class BaseStats {
int damage;
int heal;
}
enum StatType {Damage, Heal}
class Stats {
readonly BaseStats baseStats;
int Damage { get {}} //modifier
int Heal { get {} } //modifier
}
class StatsMediator {
// Keep in memory because it is meant to access frequently
readonly LinkedList<StatModifier> modifiers = new ();
event EventHandler<Query> Queries;
void PerformQuery(object sender, Query query) => Queries?.Invoke(sender, query);
public void AddModifier(StatModifier modifier) {
modifiers.AddLast(modifier);
}
}
class Query {
readonly StatType StatType;
int Value;
Query(StatType statType, int value) {
StatType = statType;
Valye = value;
}
}
abstract class StatModifier {}
Decorator Pattern
Decorator or Wrapper pattern
allows behaviour to be added
to individual objects without
affecting the behaviour of other objects of same class.
Use Case
- A game has multiple AI Bot players.
- Each AI Bot Player can cast spells using one of predefined abilities.
- It can also choose to merge its ability with any of other abilities, to make a single cast stronger.
Structure
+-----------+ +--------------+
|Interface |-->|Magic Ability|
+-----+-----+ +------+-------+
|
|
| +-----+------+ +-------------+
+--------->+ Ability +------>|Fire Damage |
| Decorator | |Deocartor |
+------------+ +-------+----+
|
| +---------------+
+---------->|Heal Decorator |
+-------+-------+
+-----------+ +----------------+
|Controller |--->|Ability Definition|
+-----+-----+ +--------+-------+
|
| +--------------+
+--------->|Ability Factory|
+------+-------+
-
Controller
- Create Ability Object from Factory
- Call Manager.Decorate if one ability already exists
- Cast Spell
-
Ability Manager Singleton
- Wraps two abilities
Implementation
publc interface IAbility {
public int CastSpell();
}
public enum AbilityType {
Magic,
FireDamage,
Heal
}
public class AbilityDefinition { // Immutable Data Class
public int value;// = 10;
public AbilityType abilityType;// = AbilityType.Magic;
}
public static class AbilityFactory {
public static IAbility Create(AbilityDefinition definition) {
return definition.type switch {
_ => new MagicAbility(definition.value),
// Magic, Heal and Fire
};
}
}
public class MagicAbility: IAbility {
private readonly int value;
public MagicAbility(int value){
this.value = value;
}
public int CastSpell() {
return vaule;
}
}
public abstract class AbilityDecorator : IAbility {
//ability decorator can Wrap another ability inside it
protected IAbility ability;
public void Decorate(IAbility ability) {
if(ReferenceEquals(this, ability) {
//Dont decoarte with itself
}
if(this.ability is AbilityDecorator decorator){
//Wrap this ability with another
decorator.Decorate(ability);
} else {
this.ability = ability;
}
}
protected readonly int value;
protected AbilityDecorator(int value) {
this.value = value;
}
public virtual int CastSpell() {
//Deocator adds value spell effect over
// original ability spell effect. If it exists.
return ability?.CastSpell() + value ?? value;
}
}
public class FireDamageDecorator: AbilityDecorator {
public FireDamageDecorator(int value) : base(value) {}
}
public class HealDecorator: AbilityDecorator {
public HealDecorator(int value): base(value) {}
public override int CastSpell() {
HealPlayer();
return ability?.CastSpell() ?? 0;
}
void HealPlayer(){
//Special ability to heal self when damaging enemy
}
}
public class AbilityController {
//psuedocode
AbilityDefinition definition;
public IAbility Ability {get; private set;}
//Step 1
Ability = AbilityFactory.Create(definition);
//Step 2
if(AbilityManager.Instance.selectedAbility == null){
AbilityManager.Instance.selectedAbility = this;
} else {
AbilityManager.Instance.Decorate(this);
AbilityManager.Instance.selectedAbility = null;
}
//Step 3
Ability.CastSpell();
}
public class AbilityManager : Singleton {
//Instance for Singleton
public AbilityController selectedAbility;
public void Decorate(AbilityController abilityChosen) {
if (selectedAbility == abilityChosen) return;
if(selectedAbility.Ability is AbilityController decorator) {
//Wrapping two abilities
decorator.Decorate(abilityChosen.Ability);
abilityChosen.Ability = decorator;
}
}
}
Command Pattern
Command/Action : Converts an method call into a stand-alone object.
A method call usually responds to a specific action ( or command ). Everything needed to execute that command is available to the class.
- Instead of directly calling a method to perform an action, you create a command object that holds all the information needed to execute that action.
It needs 4 components:-
- Command Interface
- Concrete Command
- Invoker
- Receiver
-
Allows decoupling. Receiver is not aware about the implementation
-
Logging - object can be serialized (written to a file)
-
Queuing of commands is possible
-
Undo is easy to implement, as serialized object is available
-
eg. Encapsulate all ICommand
- propertyCommon
- execute(RequiredObject obj)
Invoker is needed to set command
- Key
- SetCommand(ICommand)
- press()
- self.command.execute()
Implement Command in concrete classes
- InsertTextCommand
- Editor editor
- execute(Editor editor, text, position) -> editor.insert_text_at_position(position)
- BoldTextCommand
- Editor editor
- execute(Editor editor, text, x, y) -> editor.text_bold_at_xy(x,y)
Reciever is not aware which key is pressed and how to handle it
- TextEditor class
- insert_text_at_position
- text_bold_at_xy
insert_text_key = Key() // .SetCommand(InsertTextCommand) bold_key = Key() // .SetCommand(BoldTextCommand)
bold_key.press()
ECS
Entity-Component System : Components are pure classes, almost entirely for data.
MVC
- decouples views and models by estabilishing a subscribe protocol between them. Views has the ability to refelect the current state of model, while allowing multiple views to attach to a model, providing multiple representations.
- Can be nested
- Utilizes Observer and Composite patterns
- relies on a controller class to implement response statergy
- Factory Method to specify controller class
- Decorator to add scrolling to view