DEV Community

Surya
Surya

Posted on

🎭 Save Your Code with Decorators, Proxies, and Delegation

Introduction

Managing integrations in your app often means dealing with similar objects that have slightly different behaviors. Instead of cluttering your code with conditionals, you can wrap these objects using patterns like decorators, proxies, and delegation.

Let’s walk through a practical example using Ruby and Rails, but the concepts work in any language!


The Problem

Imagine you have a table called installations where you store integrations like Slack, Teams, and SharePoint. Your table might look like this:

CREATE TABLE installations (
  id BIGINT,
  name VARCHAR,
  configs JSON
);
Enter fullscreen mode Exit fullscreen mode

You create records with ActiveRecord like this:

Installation.create(name: 'sharepoint', configs: {tenant_id: '123e4567-e89b-12d3-a456-426614174000', sites: ["https://sample.sharepoint.com/sites/welcome"]})
Installation.create(name: 'msteams', configs: {tenant_id: '123e4567-e89b-12d3-a456-426614174111', enabled_channels: ['IT', 'Admin']})
Enter fullscreen mode Exit fullscreen mode

But now, you need to add specific behavior for each integration. For example, SharePoint needs to handle sites, and Teams needs to handle enabled_channels.


The Bad Solution

You could add these methods directly to Installation, but that creates tight coupling and code duplication:

class Installation < ApplicationRecord
  def sites
    raise NoMethodError if !name.eql? 'sharepoint'
    configs[:sites]
  end

  def sites=(new_sites)
    raise NoMethodError if !name.eql? 'sharepoint'
    configs[:sites] = sites + new_sites
  end

  def enabled_channels
    raise NoMethodError if !name.eql? 'teams'
    configs[:enabled_channels]
  end
end
Enter fullscreen mode Exit fullscreen mode

This approach makes your code hard to maintain and scale.


The Elegant Solution: Decorators, Proxies, and Delegation

Let’s use a cleaner approach: wrapping each integration in a specific class, and delegating shared functionality to the Installation class.

For SharePoint:

class SharepointInstallation
  def initialize(id:)
    @installation = Installation.find_by(id: id)
  end

  def sites
    configs[:sites]
  end

  def sites=(new_sites)
    configs[:sites] = sites + new_sites
  end

  def method_missing(method, *args)
    return @installation.send(method, *args) if @installation.respond_to?(method)
    super
  end
end
Enter fullscreen mode Exit fullscreen mode

Now, you can access and update SharePoint-specific methods:

sharepoint_inst = SharepointInstallation.new(id: 1)
puts sharepoint_inst.sites
sharepoint_inst.sites = ["https://newsite.sharepoint.com"]
sharepoint_inst.save
Enter fullscreen mode Exit fullscreen mode

Adding the Teams Integration

Similarly, create a TeamsInstallation class:

class TeamsInstallation
  def initialize(id:)
    @installation = Installation.find_by(id: id)
  end

  def enabled_channels
    configs[:enabled_channels]
  end

  def enabled_channels=(new_channels)
    configs[:enabled_channels] = enabled_channels + new_channels
    save
  end

  def method_missing(method, *args)
    return @installation.send(method, *args) if @installation.respond_to?(method)
    super
  end
end
Enter fullscreen mode Exit fullscreen mode

Now you can do the same for Teams:

teams_inst = TeamsInstallation.new(id: 1)
puts teams_inst.enabled_channels
teams_inst.enabled_channels = ['Support', 'Marketing']
teams_inst.save
Enter fullscreen mode Exit fullscreen mode

Why This Works

  • Separation of Concerns: Each integration has its own class.
  • Easy to Extend: Add new integrations by creating new wrapper classes.
  • Cleaner Code: No more clutter in the Installation class.
  • Less Maintenance: New logic for integrations doesn’t affect existing code.

Conclusion

This solution keeps your code clean and scalable. By using decorators, proxies, and delegation, you can add behaviors for different integrations while keeping the core logic simple and maintainable.

Even though this example uses Ruby and Rails, this pattern applies to any object-oriented language, from Python to JavaScript. It’s a universal strategy to wrap complexity and keep your codebase neat!


Top comments (0)