Composite Design Pattern

Composite Design Pattern

What it is ?

Composite is a structural design pattern that lets you compose objects into tree structures and then work with these structures as if they were individual objects.

As described by Gof, “Compose objects into tree structure to represent part-whole hierarchies. Composite lets client treat individual objects and compositions of objects uniformly”.

When to Use ?

  • When we you have to implement a tree-like object structure.

  • When we want the client code to treat both simple and complex elements uniformly.

When not to use ?

  1. Different Stuff: If the things in your system (like unable to group objects together) are very different from each other, the Composite Pattern might not be the best fit. It works better when everything follows a similar pattern.

  2. Speed Issues: If your system needs to be super fast, the Composite Pattern could slow it down because of how it organizes things. In really speedy situations, simpler ways might be better.

  3. Always Changing: If your system keeps changing a lot, especially with things being added or taken away frequently, using the Composite Pattern might not be the easiest or most efficient way.

  4. Not Too Complicated: If your system isn't very complicated and doesn't have a lot of layers or levels, using the Composite Pattern might make things more complex than they need to be.

  5. Worried About Memory: If your system needs to use as little memory as possible, the Composite Pattern might use more than you'd like. In memory-sensitive situations, it might be better to use simpler methods.

Structure

  1. Component: The Component interface describes operations that are common to both simple and complex elements of the tree.

  2. Leaf: The Leaf is a basic element of a tree that doesn’t have sub-elements. Usually, leaf components end up doing most of the real work, since they don’t have anyone to delegate the work to.

  3. Composite: The Container (aka composite) is an element that has sub-elements: leaves or other containers. A container doesn’t know the concrete classes of its children. It works with all sub-elements only via the component interface.

  4. Client: The Client works with all elements through the component interface. As a result, the client can work in the same way with both simple or complex elements of the tree.

Examples

1.Let’s suppose we are building a financial application. We have customers with multiple bank accounts. We are asked to prepare a design which can be useful to generate the customer’s consolidated account view which is able to show customer’s total account balance as well as consolidated account statement after merging all the account statements. So, application should be able to generate:

1) Customer’s total account balance from all accounts
2) Consolidated account statement

from abc import ABC, abstractmethod

# Component
class AccountComponent(ABC):
    @abstractmethod
    def get_balance(self):
        pass

    @abstractmethod
    def get_statement(self):
        pass

# Leaf
class BankAccount(AccountComponent):
    def __init__(self, account_number, balance, statement):
        self.account_number = account_number
        self.balance = balance
        self.statement = statement

    def get_balance(self):
        return self.balance

    def get_statement(self):
        return f"Account {self.account_number} Statement:\n{self.statement}"

# Composite
class CustomerAccount(AccountComponent):
    def __init__(self, customer_name):
        self.customer_name = customer_name
        self.accounts = []

    def add_account(self, account):
        self.accounts.append(account)

    def get_balance(self):
        total_balance = sum(account.get_balance() for account in self.accounts)
        return total_balance

    def get_statement(self):
        consolidated_statement = f"Consolidated Statement for {self.customer_name}:\n"
        for account in self.accounts:
            consolidated_statement += account.get_statement() + "\n"
        return consolidated_statement

# Usage
if __name__ == "__main__":
    account1 = BankAccount("123456", 5000, "Transaction 1: +$100\nTransaction 2: -$50")
    account2 = BankAccount("789012", 7000, "Transaction 1: +$200\nTransaction 2: -$100")

    customer = CustomerAccount("John Doe")
    customer.add_account(account1)
    customer.add_account(account2)

    # Generate Customer’s total account balance
    total_balance = customer.get_balance()
    print(f"Customer's Total Account Balance: ${total_balance}")

    # Generate Consolidated account statement
    consolidated_statement = customer.get_statement()
    print(consolidated_statement)

Explanation:

In this example:

  • AccountComponent is the common interface for both leaf (BankAccount) and composite (CustomerAccount) objects.

  • BankAccount represents a leaf object, which is an individual bank account with a specific account number, balance, and statement.

  • CustomerAccount is the composite object that can contain multiple bank accounts and provides methods to calculate the total balance and generate a consolidated account statement.

The get_balance method is applied uniformly to both leaf and composite objects, allowing the calculation of the total account balance. The get_statement method is similarly applied to generate a consolidated account statement.

  1. Let’s consider a car. Car has an engine and a tire. The engine is made up of some electrical components and valves. Likewise how do you calculate the total price

     from abc import ABC, abstractmethod
    
     # Component (Leaf)
     class Component(ABC):
         @abstractmethod
         def get_price(self):
             pass
    
     # Leaf
     class Transistor(Component):
         def get_price(self):
             return 10  # Just an arbitrary value for demonstration
    
     # Leaf
     class Chip(Component):
         def get_price(self):
             return 20  # Just an arbitrary value for demonstration
    
     # Leaf
     class Valve(Component):
         def get_price(self):
             return 15  # Just an arbitrary value for demonstration
    
     # Leaf
     class Tire(Component):
         def get_price(self):
             return 50  # Just an arbitrary value for demonstration
    
     # Composite
     class Composite(Component):
         def __init__(self, name):
             self.name = name
             self.components = []
    
         def add_component(self, component):
             self.components.append(component)
    
         def get_price(self):
             total_price = sum(component.get_price() for component in self.components)
             return total_price
    
     # Client code
     if __name__ == "__main__":
         # Creating leaf objects
         transistor = Transistor()
         chip = Chip()
         valve = Valve()
         tire = Tire()
    
         # Creating composite objects
         electrical_components = Composite("Electrical Components")
         electrical_components.add_component(transistor)
         electrical_components.add_component(chip)
         electrical_components.add_component(valve)
    
         engine = Composite("Engine")
         engine.add_component(electrical_components)
    
         car = Composite("Car")
         car.add_component(engine)
         car.add_component(tire)
    
         # Applying operation on leaf objects
         print(f"Transistor Price: {transistor.get_price()}")
         print(f"Chip Price: {chip.get_price()}")
         print(f"Valve Price: {valve.get_price()}")
         print(f"Tire Price: {tire.get_price()}")
    
         # Applying operation on composite objects
         print(f"Engine Price: {engine.get_price()}")
         print(f"Car Price: {car.get_price()}")
    

    In this example:

    • Component is the common interface for both leaf and composite objects.

    • Transistor, Chip, Valve, and Tire are leaf objects implementing the Component interface.

    • Composite is the composite object that can contain both leaf and other composite objects.

The get_price method is applied uniformly to both leaf and composite objects, demonstrating how the operation is recursively applied to the entire object hierarchy. The pricing example is kept simple for demonstration purposes. In a real-world scenario, you would likely have more complex pricing logic.

  1. Graphic Shapes in a Drawing Application

     from abc import ABC, abstractmethod
    
     # Component
     class Graphic(ABC):
         @abstractmethod
         def draw(self):
             pass
    
     # Leaf
     class Circle(Graphic):
         def draw(self):
             print("Drawing Circle")
    
     # Leaf
     class Square(Graphic):
         def draw(self):
             print("Drawing Square")
    
     # Composite
     class CompositeGraphic(Graphic):
         def __init__(self):
             self.graphics = []
    
         def add(self, graphic):
             self.graphics.append(graphic)
    
         def draw(self):
             print("Drawing Composite:")
             for graphic in self.graphics:
                 graphic.draw()
    
     # Usage
     circle = Circle()
     square = Square()
     composite = CompositeGraphic()
     composite.add(circle)
     composite.add(square)
    
     composite.draw()
    
  2. File system representation

     from abc import ABC, abstractmethod
    
     # Component
     class FileSystemComponent(ABC):
         @abstractmethod
         def size(self):
             pass
    
     # Leaf
     class File(FileSystemComponent):
         def __init__(self, size):
             self.size_value = size
    
         def size(self):
             return self.size_value
    
     # Composite
     class Directory(FileSystemComponent):
         def __init__(self):
             self.children = []
    
         def add(self, component):
             self.children.append(component)
    
         def size(self):
             total_size = sum(child.size() for child in self.children)
             return total_size
    
     # Usage
     file1 = File(10)
     file2 = File(5)
     directory = Directory()
     directory.add(file1)
     directory.add(file2)
    
     print("Total Size:", directory.size())
    
  3. Menu Hierarchy in Restaurant

     from abc import ABC, abstractmethod
    
     # Component
     class MenuItem(ABC):
         @abstractmethod
         def display(self):
             pass
    
     # Leaf
     class Dish(MenuItem):
         def __init__(self, name, price):
             self.name = name
             self.price = price
    
         def display(self):
             print(f"{self.name} - ${self.price}")
    
     # Composite
     class Menu(MenuItem):
         def __init__(self):
             self.items = []
    
         def add_item(self, item):
             self.items.append(item)
    
         def display(self):
             print("Menu:")
             for item in self.items:
                 item.display()
    
     # Usage
     dish1 = Dish("Spaghetti", 12.99)
     dish2 = Dish("Salad", 7.99)
     menu = Menu()
     menu.add_item(dish1)
     menu.add_item(dish2)
    
     menu.display()
    

Do we need to stick hard to it ?

In scenarios where performance is critical, the overhead introduced by the Composite Pattern's recursive structure can potentially impact speed. Here's a simplified example illustrating this concept with a focus on speed:

For example, consider a performance-sensitive system that involves processing a large number of graphic objects.

Non Composite Approach

class Circle:
    def __init__(self, radius):
        self.radius = radius

    def draw(self):
        print(f"Drawing Circle with radius {self.radius}")

class Square:
    def __init__(self, side_length):
        self.side_length = side_length

    def draw(self):
        print(f"Drawing Square with side length {self.side_length}")

# Usage
circles = [Circle(5) for _ in range(1000000)]
squares = [Square(4) for _ in range(1000000)]

for circle in circles:
    circle.draw()

for square in squares:
    square.draw()

Composite Approach

class Graphic:
    def draw(self):
        pass

class Circle(Graphic):
    def __init__(self, radius):
        self.radius = radius

    def draw(self):
        print(f"Drawing Circle with radius {self.radius}")

class Square(Graphic):
    def __init__(self, side_length):
        self.side_length = side_length

    def draw(self):
        print(f"Drawing Square with side length {self.side_length}")

class CompositeGraphic(Graphic):
    def __init__(self):
        self.graphics = []

    def add(self, graphic):
        self.graphics.append(graphic)

    def draw(self):
        for graphic in self.graphics:
            graphic.draw()

# Usage
composite = CompositeGraphic()
for _ in range(500000):
    composite.add(Circle(5))

for _ in range(500000):
    composite.add(Square(4))

composite.draw()

From the above example we can see,

  • The non-composite approach creates separate lists of circles and squares and iterates through each list to draw the shapes.

  • The composite approach creates a composite object that contains both circles and squares and draws them through a single draw method.

In a performance-sensitive scenario, the non-composite approach may be more efficient due to its simplicity and direct iteration through the lists.

The composite approach involves recursive calls through the composite structure, potentially introducing additional function call overhead, impacting speed.

Advantages:

  • You can work with complex tree structures more conveniently: use polymorphism and recursion to your advantage.
  • Open/Closed Principle. You can introduce new element types into the app without breaking the existing code, which now works with the object tree.

  • Uniform treatment of leaf and composite objects.

  • Its particularly useful when dealing with hierarchial structures.

  • Scalability - we can easily new types of components to the entire hierarchy.

  • Promotes encapsulation

Disadvantages

  • It might be difficult to provide a common interface for classes whose functionality differs too much. In certain scenarios, you’d need to over generalize the component interface, making it harder to comprehend.

  • If individual leaf objects have unique properties or behaviors, the Composite pattern may not be the best choice, as it enforces a uniform interface across all components. In such cases, you may need to resort to other patterns or adaptations.

  • Storing hierarchy objects can consume memory if its deep.