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
);
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']})
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
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
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
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
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
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)