Implementation and Application of the Composite Pattern in Unity
Kevin Nox Yuan

Kevin Nox Yuan @sky_noc__16618949385255bd

About: Full-time "dad" to a cute baby, and a Unity developer on the side.​

Location:
China
Joined:
Oct 14, 2025

Implementation and Application of the Composite Pattern in Unity

Publish Date: Oct 14
3 0

Table of Contents

What is the Composite Pattern

The Composite Pattern is a structural design pattern that allows you to compose objects into tree structures to represent “part-whole” hierarchies. It lets clients treat individual objects and compositions of objects uniformly.

Core Concepts

  • Component: Defines the interface for objects in the tree structure
  • Leaf: A leaf node in the tree; it has no children
  • Composite: A branch node in the tree; it can contain child nodes

UML Class Diagram

    Component
    ├── Operation()
    ├── Add(Component)
    ├── Remove(Component)
    └── GetChild(int)
         ↑
    ┌────┴────┐
   Leaf    Composite
           ├── children: List<Component>
           ├── Operation()
           ├── Add(Component)
           ├── Remove(Component)
           └── GetChild(int)
Enter fullscreen mode Exit fullscreen mode

Use Cases in Unity

In Unity development, the Composite Pattern has several typical use cases:

  1. UI System: Panels containing multiple UI elements forming a tree
  2. GameObject Hierarchy: GameObject parent-child relationships naturally fit the pattern
  3. Skill Systems: Composite skills built from multiple sub-skills
  4. Equipment Systems: Sets composed of multiple individual items
  5. AI Behavior Trees: Composition and nesting of behavior nodes
  6. Scene Management: Areas containing multiple sub-areas or game objects

Implementation Approaches

Approach 1: Classic Composite Implementation

This is the most standard implementation and suits scenarios that require strict adherence to the pattern.

using System.Collections.Generic;
using UnityEngine;

// Abstract component
public abstract class GameComponent
{
    protected string name;
    protected Transform transform;

    public GameComponent(string name)
    {
        this.name = name;
    }

    public abstract void Execute();
    public abstract void Add(GameComponent component);
    public abstract void Remove(GameComponent component);
    public abstract GameComponent GetChild(int index);
    public abstract int GetChildCount();
}

// Leaf node - single game object
public class GameLeaf : GameComponent
{
    private GameObject gameObject;

    public GameLeaf(string name, GameObject gameObject) : base(name)
    {
        this.gameObject = gameObject;
        this.transform = gameObject.transform;
    }

    public override void Execute()
    {
        Debug.Log($"Executing leaf: {name}");
        // Run specific game logic
        if (gameObject != null)
        {
            // e.g., play animation, move, attack, etc.
            var animator = gameObject.GetComponent<Animator>();
            if (animator != null)
            {
                animator.SetTrigger("Execute");
            }
        }
    }

    // Leaf nodes do not support children
    public override void Add(GameComponent component)
    {
        throw new System.NotSupportedException("Cannot add component to a leaf");
    }

    public override void Remove(GameComponent component)
    {
        throw new System.NotSupportedException("Cannot remove component from a leaf");
    }

    public override GameComponent GetChild(int index)
    {
        throw new System.NotSupportedException("Leaf has no children");
    }

    public override int GetChildCount()
    {
        return 0;
    }
}

// Composite node - group of game objects
public class GameComposite : GameComponent
{
    private List<GameComponent> children = new List<GameComponent>();
    private GameObject rootObject;

    public GameComposite(string name, GameObject rootObject = null) : base(name)
    {
        this.rootObject = rootObject;
        if (rootObject != null)
        {
            this.transform = rootObject.transform;
        }
    }

    public override void Execute()
    {
        Debug.Log($"Executing composite: {name}");

        // Execute its own logic first
        if (rootObject != null)
        {
            var animator = rootObject.GetComponent<Animator>();
            if (animator != null)
            {
                animator.SetTrigger("Execute");
            }
        }

        // Then execute all children
        foreach (var child in children)
        {
            child.Execute();
        }
    }

    public override void Add(GameComponent component)
    {
        children.Add(component);

        // If there is a GameObject, establish parent-child relation
        if (transform != null && component.transform != null)
        {
            component.transform.SetParent(transform);
        }
    }

    public override void Remove(GameComponent component)
    {
        children.Remove(component);

        // Detach parent-child relation
        if (component.transform != null)
        {
            component.transform.SetParent(null);
        }
    }

    public override GameComponent GetChild(int index)
    {
        if (index >= 0 && index < children.Count)
        {
            return children[index];
        }
        return null;
    }

    public override int GetChildCount()
    {
        return children.Count;
    }
}

// Usage example
public class CompositeExample : MonoBehaviour
{
    void Start()
    {
        // Create root composite
        var army = new GameComposite("Army");

        // Create infantry squad
        var infantrySquad = new GameComposite("Infantry Squad");
        infantrySquad.Add(new GameLeaf("Soldier1", CreateSoldier("Soldier1")));
        infantrySquad.Add(new GameLeaf("Soldier2", CreateSoldier("Soldier2")));
        infantrySquad.Add(new GameLeaf("Soldier3", CreateSoldier("Soldier3")));

        // Create tank squad
        var tankSquad = new GameComposite("Tank Squad");
        tankSquad.Add(new GameLeaf("Tank1", CreateTank("Tank1")));
        tankSquad.Add(new GameLeaf("Tank2", CreateTank("Tank2")));

        // Assemble army
        army.Add(infantrySquad);
        army.Add(tankSquad);

        // Execute the entire army's action
        army.Execute();
    }

    private GameObject CreateSoldier(string name)
    {
        var soldier = GameObject.CreatePrimitive(PrimitiveType.Capsule);
        soldier.name = name;
        return soldier;
    }

    private GameObject CreateTank(string name)
    {
        var tank = GameObject.CreatePrimitive(PrimitiveType.Cube);
        tank.name = name;
        tank.transform.localScale = Vector3.one * 2f;
        return tank;
    }
}
Enter fullscreen mode Exit fullscreen mode

Pros:

  • Strictly adheres to Composite design principles
  • Type-safe with clear structure
  • Supports arbitrary depth of nesting
  • Easy to extend with new component types

Cons:

  • More code and complexity
  • Might be over-engineered for simple scenarios
  • Extra memory overhead to maintain the tree structure

Approach 2: Unity GameObject-based Implementation

Leverages Unity’s native GameObject parent-child relationships—this is the most “Unity-ish” approach.

using System.Collections.Generic;
using UnityEngine;

// Game component interface
public interface IGameComponent
{
    void Execute();
    string GetName();
    Transform GetTransform();
}

// Base component class
public abstract class BaseGameComponent : MonoBehaviour, IGameComponent
{
    [SerializeField] protected string componentName;

    protected virtual void Awake()
    {
        if (string.IsNullOrEmpty(componentName))
        {
            componentName = gameObject.name;
        }
    }

    public abstract void Execute();

    public string GetName()
    {
        return componentName;
    }

    public Transform GetTransform()
    {
        return transform;
    }
}

// Leaf component - single unit
public class UnitComponent : BaseGameComponent
{
    [SerializeField] private float moveSpeed = 5f;
    [SerializeField] private Vector3 targetPosition;
    [SerializeField] private bool hasTarget = false;

    public override void Execute()
    {
        Debug.Log($"Unit {componentName} executing action");

        if (hasTarget)
        {
            MoveToTarget();
        }
        else
        {
            PerformDefaultAction();
        }
    }

    private void MoveToTarget()
    {
        transform.position = Vector3.MoveTowards(
            transform.position, 
            targetPosition, 
            moveSpeed * Time.deltaTime
        );

        if (Vector3.Distance(transform.position, targetPosition) < 0.1f)
        {
            hasTarget = false;
            Debug.Log($"{componentName} reached target!");
        }
    }

    private void PerformDefaultAction()
    {
        // Default behavior, e.g., patrol
        transform.Rotate(0, 45 * Time.deltaTime, 0);
    }

    public void SetTarget(Vector3 target)
    {
        targetPosition = target;
        hasTarget = true;
    }
}

// Composite component - group of units
public class GroupComponent : BaseGameComponent
{
    [SerializeField] private bool executeInSequence = false;
    [SerializeField] private float sequenceDelay = 0.5f;

    private List<IGameComponent> childComponents = new List<IGameComponent>();
    private bool isInitialized = false;

    protected override void Awake()
    {
        base.Awake();
        InitializeChildren();
    }

    private void InitializeChildren()
    {
        if (isInitialized) return;

        childComponents.Clear();

        // Get components from all direct children
        for (int i = 0; i < transform.childCount; i++)
        {
            var child = transform.GetChild(i);
            var component = child.GetComponent<IGameComponent>();
            if (component != null)
            {
                childComponents.Add(component);
            }
        }

        isInitialized = true;
    }

    public override void Execute()
    {
        Debug.Log($"Group {componentName} executing with {childComponents.Count} children");

        if (executeInSequence)
        {
            StartCoroutine(ExecuteSequentially());
        }
        else
        {
            ExecuteSimultaneously();
        }
    }

    private void ExecuteSimultaneously()
    {
        foreach (var child in childComponents)
        {
            child.Execute();
        }
    }

    private System.Collections.IEnumerator ExecuteSequentially()
    {
        foreach (var child in childComponents)
        {
            child.Execute();
            yield return new WaitForSeconds(sequenceDelay);
        }
    }

    // Dynamically add a child component
    public void AddChild(GameObject childObject)
    {
        childObject.transform.SetParent(transform);
        var component = childObject.GetComponent<IGameComponent>();
        if (component != null && !childComponents.Contains(component))
        {
            childComponents.Add(component);
        }
    }

    // Remove a child component
    public void RemoveChild(GameObject childObject)
    {
        var component = childObject.GetComponent<IGameComponent>();
        if (component != null)
        {
            childComponents.Remove(component);
        }
        childObject.transform.SetParent(null);
    }

    // Reinitialize the child component list
    public void RefreshChildren()
    {
        isInitialized = false;
        InitializeChildren();
    }
}

// Advanced group component - supports more complex operations
public class AdvancedGroupComponent : GroupComponent
{
    [SerializeField] private GroupFormation formation = GroupFormation.Line;
    [SerializeField] private float spacing = 2f;
    [SerializeField] private bool autoArrange = true;

    public enum GroupFormation
    {
        Line,
        Circle,
        Grid,
        Wedge
    }

    void Start()
    {
        if (autoArrange)
        {
            ArrangeFormation();
        }
    }

    public void ArrangeFormation()
    {
        var children = new List<Transform>();
        for (int i = 0; i < transform.childCount; i++)
        {
            children.Add(transform.GetChild(i));
        }

        switch (formation)
        {
            case GroupFormation.Line:
                ArrangeInLine(children);
                break;
            case GroupFormation.Circle:
                ArrangeInCircle(children);
                break;
            case GroupFormation.Grid:
                ArrangeInGrid(children);
                break;
            case GroupFormation.Wedge:
                ArrangeInWedge(children);
                break;
        }
    }

    private void ArrangeInLine(List<Transform> children)
    {
        for (int i = 0; i < children.Count; i++)
        {
            children[i].localPosition = new Vector3(i * spacing, 0, 0);
        }
    }

    private void ArrangeInCircle(List<Transform> children)
    {
        float angleStep = 360f / children.Count;
        for (int i = 0; i < children.Count; i++)
        {
            float angle = i * angleStep * Mathf.Deg2Rad;
            Vector3 position = new Vector3(
                Mathf.Cos(angle) * spacing,
                0,
                Mathf.Sin(angle) * spacing
            );
            children[i].localPosition = position;
        }
    }

    private void ArrangeInGrid(List<Transform> children)
    {
        int gridSize = Mathf.CeilToInt(Mathf.Sqrt(children.Count));
        for (int i = 0; i < children.Count; i++)
        {
            int x = i % gridSize;
            int z = i / gridSize;
            children[i].localPosition = new Vector3(x * spacing, 0, z * spacing);
        }
    }

    private void ArrangeInWedge(List<Transform> children)
    {
        for (int i = 0; i < children.Count; i++)
        {
            float offset = (i - children.Count / 2f) * spacing;
            children[i].localPosition = new Vector3(offset, 0, -i * spacing * 0.5f);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Pros:

  • Fully leverages Unity’s GameObject system
  • Visual editing and easy debugging
  • Parent-child relationships handled automatically
  • Supports Inspector configuration
  • Memory efficient

Cons:

  • Dependent on Unity’s MonoBehaviour system
  • Not ideal for purely logical composites
  • Constrained by GameObject lifecycle

Approach 3: ScriptableObject-based Implementation

Ideal for data-driven composites, especially for skills, equipment, and configuration systems.

using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu(fileName = "New Skill", menuName = "Game/Skill")]
public abstract class SkillData : ScriptableObject
{
    [SerializeField] protected string skillName;
    [SerializeField] protected Sprite icon;
    [SerializeField] protected float cooldown;
    [SerializeField] protected int manaCost;

    public abstract void Execute(GameObject caster, GameObject target = null);
    public abstract float GetDuration();
    public virtual string GetDescription() => skillName;
}

[CreateAssetMenu(fileName = "New Basic Skill", menuName = "Game/Basic Skill")]
public class BasicSkill : SkillData
{
    [SerializeField] private int damage;
    [SerializeField] private float range;
    [SerializeField] private GameObject effectPrefab;

    public override void Execute(GameObject caster, GameObject target = null)
    {
        Debug.Log($"Executing basic skill: {skillName}");

        if (target != null && Vector3.Distance(caster.transform.position, target.transform.position) <= range)
        {
            // Apply damage
            var health = target.GetComponent<Health>();
            if (health != null)
            {
                health.TakeDamage(damage);
            }

            // Play VFX
            if (effectPrefab != null)
            {
                Instantiate(effectPrefab, target.transform.position, Quaternion.identity);
            }
        }
    }

    public override float GetDuration()
    {
        return 0.5f; // Instant cast
    }
}

[CreateAssetMenu(fileName = "New Combo Skill", menuName = "Game/Combo Skill")]
public class ComboSkill : SkillData
{
    [SerializeField] private List<SkillData> subSkills = new List<SkillData>();
    [SerializeField] private float delayBetweenSkills = 0.3f;
    [SerializeField] private bool executeSimultaneously = false;

    public override void Execute(GameObject caster, GameObject target = null)
    {
        Debug.Log($"Executing combo skill: {skillName}");

        if (executeSimultaneously)
        {
            ExecuteSimultaneously(caster, target);
        }
        else
        {
            var skillExecutor = caster.GetComponent<SkillExecutor>();
            if (skillExecutor != null)
            {
                skillExecutor.StartCoroutine(ExecuteSequentially(caster, target));
            }
        }
    }

    private void ExecuteSimultaneously(GameObject caster, GameObject target)
    {
        foreach (var skill in subSkills)
        {
            if (skill != null)
            {
                skill.Execute(caster, target);
            }
        }
    }

    private System.Collections.IEnumerator ExecuteSequentially(GameObject caster, GameObject target)
    {
        foreach (var skill in subSkills)
        {
            if (skill != null)
            {
                skill.Execute(caster, target);
                yield return new WaitForSeconds(delayBetweenSkills);
            }
        }
    }

    public override float GetDuration()
    {
        if (executeSimultaneously)
        {
            float maxDuration = 0f;
            foreach (var skill in subSkills)
            {
                if (skill != null)
                {
                    maxDuration = Mathf.Max(maxDuration, skill.GetDuration());
                }
            }
            return maxDuration;
        }
        else
        {
            float totalDuration = 0f;
            foreach (var skill in subSkills)
            {
                if (skill != null)
                {
                    totalDuration += skill.GetDuration() + delayBetweenSkills;
                }
            }
            return totalDuration;
        }
    }

    public void AddSkill(SkillData skill)
    {
        if (skill != null && !subSkills.Contains(skill))
        {
            subSkills.Add(skill);
        }
    }

    public void RemoveSkill(SkillData skill)
    {
        subSkills.Remove(skill);
    }
}

// Skill executor
public class SkillExecutor : MonoBehaviour
{
    [SerializeField] private List<SkillData> availableSkills = new List<SkillData>();
    private Dictionary<SkillData, float> skillCooldowns = new Dictionary<SkillData, float>();

    void Update()
    {
        // Update cooldowns
        var keys = new List<SkillData>(skillCooldowns.Keys);
        foreach (var skill in keys)
        {
            skillCooldowns[skill] -= Time.deltaTime;
            if (skillCooldowns[skill] <= 0)
            {
                skillCooldowns.Remove(skill);
            }
        }
    }

    public bool CanUseSkill(SkillData skill)
    {
        return !skillCooldowns.ContainsKey(skill);
    }

    public void UseSkill(SkillData skill, GameObject target = null)
    {
        if (CanUseSkill(skill))
        {
            skill.Execute(gameObject, target);
            skillCooldowns[skill] = skill.cooldown;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Pros:

  • Separation of data and logic
  • Supports dynamic composition at runtime
  • Easy serialization and persistence
  • Asset references and reuse
  • Convenient for designer-driven configuration

Cons:

  • Requires an extra executor component
  • Less suitable for complex runtime logic
  • Harder to debug

Approach 4: Lightweight Interface-based Implementation

Suited for performance-sensitive scenarios with minimal memory footprint.

using System.Collections.Generic;
using UnityEngine;

public interface IExecutable
{
    void Execute();
}

public interface IComposite : IExecutable
{
    void Add(IExecutable executable);
    void Remove(IExecutable executable);
    IExecutable GetChild(int index);
    int GetChildCount();
}

// Lightweight leaf implementation
public struct ActionLeaf : IExecutable
{
    private System.Action action;

    public ActionLeaf(System.Action action)
    {
        this.action = action;
    }

    public void Execute()
    {
        action?.Invoke();
    }
}

// Lightweight composite implementation
public class ActionComposite : IComposite
{
    private List<IExecutable> children;
    private System.Action preAction;
    private System.Action postAction;

    public ActionComposite(System.Action preAction = null, System.Action postAction = null)
    {
        this.children = new List<IExecutable>();
        this.preAction = preAction;
        this.postAction = postAction;
    }

    public void Execute()
    {
        preAction?.Invoke();

        for (int i = 0; i < children.Count; i++)
        {
            children[i].Execute();
        }

        postAction?.Invoke();
    }

    public void Add(IExecutable executable)
    {
        children.Add(executable);
    }

    public void Remove(IExecutable executable)
    {
        children.Remove(executable);
    }

    public IExecutable GetChild(int index)
    {
        return index >= 0 && index < children.Count ? children[index] : null;
    }

    public int GetChildCount()
    {
        return children.Count;
    }
}

// Usage example
public class LightweightCompositeExample : MonoBehaviour
{
    void Start()
    {
        // Create a composite action
        var moveAndAttack = new ActionComposite(
            preAction: () => Debug.Log("Starting move and attack sequence"),
            postAction: () => Debug.Log("Completed move and attack sequence")
        );

        // Add move action
        moveAndAttack.Add(new ActionLeaf(() => {
            Debug.Log("Moving forward");
            transform.Translate(Vector3.forward);
        }));

        // Add attack action
        moveAndAttack.Add(new ActionLeaf(() => {
            Debug.Log("Attacking!");
            // Attack logic
        }));

        // Add a sub-composite
        var multiAttack = new ActionComposite();
        multiAttack.Add(new ActionLeaf(() => Debug.Log("Attack 1")));
        multiAttack.Add(new ActionLeaf(() => Debug.Log("Attack 2")));
        multiAttack.Add(new ActionLeaf(() => Debug.Log("Attack 3")));

        moveAndAttack.Add(multiAttack);

        // Execute the entire sequence
        moveAndAttack.Execute();
    }
}
Enter fullscreen mode Exit fullscreen mode

Pros:

  • Very low memory footprint
  • High execution efficiency
  • Concise code
  • Supports lambda expressions

Cons:

  • Limited functionality
  • No complex state management
  • Harder to debug
  • Lower type safety

Performance Optimization Considerations

1. Object Pooling

public class CompositeObjectPool : MonoBehaviour
{
    private Queue<GameComposite> compositePool = new Queue<GameComposite>();
    private Queue<GameLeaf> leafPool = new Queue<GameLeaf>();

    public GameComposite GetComposite(string name)
    {
        if (compositePool.Count > 0)
        {
            var composite = compositePool.Dequeue();
            composite.Reset(name);
            return composite;
        }
        return new GameComposite(name);
    }

    public void ReturnComposite(GameComposite composite)
    {
        composite.Clear();
        compositePool.Enqueue(composite);
    }
}
Enter fullscreen mode Exit fullscreen mode

2. Lazy Loading

public class LazyComposite : IComposite
{
    private List<IExecutable> children;
    private System.Func<List<IExecutable>> childrenProvider;
    private bool isLoaded = false;

    public LazyComposite(System.Func<List<IExecutable>> childrenProvider)
    {
        this.childrenProvider = childrenProvider;
    }

    private void EnsureLoaded()
    {
        if (!isLoaded)
        {
            children = childrenProvider?.Invoke() ?? new List<IExecutable>();
            isLoaded = true;
        }
    }

    public void Execute()
    {
        EnsureLoaded();
        foreach (var child in children)
        {
            child.Execute();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

3. Caching Optimization

public class CachedComposite : IComposite
{
    private List<IExecutable> children = new List<IExecutable>();
    private bool isDirty = true;
    private List<IExecutable> cachedExecutionList;

    public void Execute()
    {
        if (isDirty)
        {
            RefreshExecutionCache();
            isDirty = false;
        }

        for (int i = 0; i < cachedExecutionList.Count; i++)
        {
            cachedExecutionList[i].Execute();
        }
    }

    private void RefreshExecutionCache()
    {
        cachedExecutionList = new List<IExecutable>(children);
    }

    public void Add(IExecutable executable)
    {
        children.Add(executable);
        isDirty = true;
    }
}
Enter fullscreen mode Exit fullscreen mode

Best Practices

1. Choose the Right Approach

  • UI System: GameObject-based approach
  • Skills/Equipment Systems: ScriptableObject-based approach
  • AI Behavior: Interface-based approach
  • Complex Game Logic: Classic approach

2. Avoid Excessive Nesting

// Not recommended: excessive nesting
var root = new GameComposite("Root");
var level1 = new GameComposite("Level1");
var level2 = new GameComposite("Level2");
var level3 = new GameComposite("Level3");
// ... continue nesting

// Recommended: flatter structure
var root = new GameComposite("Root");
root.Add(new GameLeaf("Action1", gameObject1));
root.Add(new GameLeaf("Action2", gameObject2));
root.Add(new GameLeaf("Action3", gameObject3));
Enter fullscreen mode Exit fullscreen mode

3. Combine Design Patterns Appropriately

// Composite + Command pattern
public class CommandComposite : IComposite
{
    private List<ICommand> commands = new List<ICommand>();

    public void Execute()
    {
        foreach (var command in commands)
        {
            command.Execute();
        }
    }

    public void Add(IExecutable executable)
    {
        if (executable is ICommand command)
        {
            commands.Add(command);
        }
    }
}

// Composite + Observer pattern
public class ObservableComposite : IComposite
{
    public System.Action<IExecutable> OnChildExecuted;
    private List<IExecutable> children = new List<IExecutable>();

    public void Execute()
    {
        foreach (var child in children)
        {
            child.Execute();
            OnChildExecuted?.Invoke(child);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

4. Error Handling and Safety Checks

public class SafeComposite : IComposite
{
    private List<IExecutable> children = new List<IExecutable>();

    public void Execute()
    {
        for (int i = children.Count - 1; i >= 0; i--)
        {
            try
            {
                if (children[i] != null)
                {
                    children[i].Execute();
                }
                else
                {
                    children.RemoveAt(i); // Clean up null references
                }
            }
            catch (System.Exception e)
            {
                Debug.LogError($"Error executing child {i}: {e.Message}");
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

The Composite Pattern has multiple implementation strategies in Unity, each suited to different scenarios:

  1. Classic implementation: Best for complex business logic, offers maximum flexibility
  2. GameObject-based implementation: The most Unity-native approach for scene object management
  3. ScriptableObject-based implementation: Data-driven, ideal for configuration systems
  4. Lightweight implementation: Performance-first, suitable for simple composite operations

When choosing an approach, consider:

  • Performance needs: Lightweight > GameObject > ScriptableObject > Classic
  • Maintainability: Classic > ScriptableObject > GameObject > Lightweight
  • Unity integration: GameObject > ScriptableObject > Classic > Lightweight

Proper use of the Composite Pattern can make a Unity project’s architecture clearer and easier to maintain and extend. In practice, choose the approach that fits your specific needs, and pay attention to performance optimization and error handling.

Comments 0 total

    Add comment