Author: David Martinez
Created: December 3, 2024 - Updated: December 4, 2024
Read Time: 15 min
In this article, we explore the Composite Design Pattern and how it can simplify your code when dealing with tree-like structures. ๐ฟ
The Composite Pattern is a structural design pattern that lets you compose objects into tree structures to represent part-whole hierarchies. It allows you to treat individual objects and compositions of objects uniformly.
This pattern is particularly useful when your system deals with hierarchical structures, such as organizational charts, file systems, or UI components.
๐น Tree-like Structures: Use this pattern when you need to represent part-whole hierarchies, such as folders containing files or organizations with multiple departments.
๐น Uniform Treatment of Objects and Compositions: When individual objects and composites should be treated the same way.
๐น Dynamic Hierarchical Structures: Itโs ideal when objects can be dynamically added or removed, and the hierarchy needs to evolve over time.
Consider an access control system where different types of roles (e.g., User, Manager, Admin) need to be managed, and permissions are granted to these roles for various resources (e.g., files, documents).
Without the Composite pattern, the system may involve checking each role separately, resulting in complex logic and potential duplication.
In the naive approach, roles and permissions are handled by individual classes, each with its own access logic.
This leads to complex logic and difficulty in managing large numbers of roles or adding new ones and can result in code duplication.

In this example:
๐น User, Manager, Admin are individual classes with their specific permissions.
๐น AccessControl has to check each role separately and cannot scale well if roles change or new roles are introduced.
class Userattr_reader :owned_resourcesdef initialize(owned_resources)@owned_resources = owned_resourcesenddef access_to?(resource, action)case actionwhen :read, :editowned_resources.include?(resource)elsefalseendendendclass Managerdef access_to?(_resource, action)action == :readendendclass Admindef access_to?(_resource, action)%i[read edit].include?(action)endendclass AccessControlattr_reader :user, :manager, :admindef initialize(user: nil, manager: nil, admin: nil)@user = user@manager = manager@admin = adminenddef access_to?(role_type, resource, action)case role_typewhen :useruser ? user.access_to?(resource, action) : falsewhen :managermanager ? manager.access_to?(resource, action) : falsewhen :adminadmin ? admin.access_to?(resource, action) : falseelsefalseendendendfile1 = 'file1.txt'file2 = 'file2.txt'user1 = User.new([file1])manager_role = Manager.newadmin_role = Admin.newaccess_control = AccessControl.new(user: user1, manager: manager_role, admin: admin_role)puts access_control.access_to?(:user, file1, :read) # true (User1 owns file1)puts access_control.access_to?(:user, file2, :read) # false (User1 does not own file2)puts access_control.access_to?(:manager, file2, :read) # true (Manager can read any file)puts access_control.access_to?(:admin, file2, :edit) # true (Admin can edit any file)puts access_control.access_to?(:admin, file1, :edit) # true (Admin can edit any file)
1๏ธโฃ Rigid Role Management: New roles or changes in permissions require modifying existing classes, violating the Open-Closed Principle.
2๏ธโฃ Complexity: The logic for checking access grows exponentially as more roles or resources are added.
3๏ธโฃ Duplication: Permissions logic is scattered across different classes, leading to redundancy and maintenance difficulties.
4๏ธโฃ Lack of Hierarchical Flexibility: There is no way to represent roles that aggregate other roles, like a composite of multiple roles.
The Composite pattern solves these issues by introducing a common interface for roles and allowing roles to be composed of other roles.
With this pattern, individual roles (like User) and composite roles (like a combination of Manager and Admin) can be treated uniformly.

In this improved approach:
๐น Role defines a base interface for shapes.
๐น User implements the Role interface for specific access.
๐น CompositeRole is a container for other Role objects, allowing composite access checks.
๐น Manager and Admin are concrete roles that implement the Role interface.
1๏ธโฃ Component: Defines the interface for objects in the composition.
2๏ธโฃ Leaf: Represents individual objects in the composition.
3๏ธโฃ Composite: Contains leaf elements and implements the component interface.
4๏ธโฃ Client: Manipulates the component objects through the component interface.
๐น Scalability: New roles or permissions can be added without modifying existing code, adhering to the Open-Closed Principle.
๐น Flexibility: Roles can be combined to form composite roles, representing complex access control hierarchies.
๐น Reusability: Individual and composite roles share a common interface, promoting code reuse and reducing redundancy.
๐น Simplified Access Logic: The access check becomes simpler, as it only involves traversing a tree structure, rather than dealing with multiple role types individually.
๐น Easier Maintenance: Changes in role behavior or access permissions are localized in the respective roles, making the system easier to update.
๐น Hierarchical Representation: The pattern provides a natural way to represent hierarchical structures, such as access control systems.
class Roledef access_to?(resource, action)raise NotImplementedError, 'Subclasses must implement `has_access_to?`'endendclass User < Roleattr_reader :owned_resourcesdef initialize(owned_resources)super()@owned_resources = owned_resourcesenddef access_to?(resource, action)case actionwhen :read, :editowned_resources.include?(resource)elsefalseendendendclass Manager < Roledef access_to?(_resource, action)action == :readendendclass Admin < Roledef access_to?(_resource, action)%i[read edit].include?(action)endendclass CompositeRole < Roleattr_reader :sub_rolesdef initializesuper()@sub_roles = []enddef add_role(role)sub_roles << roleenddef access_to?(resource, action)sub_roles.any? { |role| role.access_to?(resource, action) }endendfile1 = 'file1.txt'file2 = 'file2.txt'user1 = User.new([file1])manager_role = Manager.newadmin_role = Admin.newcomposite = CompositeRole.newcomposite.add_role(manager_role)composite.add_role(admin_role)composite.add_role(user1)puts composite.access_to?(file1, :read) # true (User1 or Manager/Admin can read)puts composite.access_to?(file2, :read) # true (Manager/Admin can read any file)puts composite.access_to?(file1, :edit) # true (Admin or User1 can edit file1)puts composite.access_to?(file2, :edit) # true (Admin can edit any file)puts user1.access_to?(file2, :read) # false (User1 does not own file2)
The Composite Design Pattern provides an elegant solution for managing complex hierarchies by treating both individual objects and compositions uniformly. It simplifies access control logic, scales gracefully, and promotes flexibility in your system.
By using the Composite pattern, you can manage roles and permissions in a much more maintainable and scalable way, especially as your system grows.
๐ป You can find this and other design patterns here ๐