0%
Reading Settings
Font Size
18px
Line Height
1.5
Letter Spacing
0.01em
Font Family
Table of contents
One Design Pattern a Week: Week 3
Software Engineer
Software Engineer
Design Pattern
Design Pattern
Welcome back to my "One Design Pattern a Week" series!
Try to solve this real problem: Incompatible Interfaces
Imagine you're developing a large web application that integrates with multiple third-party services. Each service has its own unique interface, but you need to use them in a uniform way within your application.
Here are some potential issues:
Here are some potential issues:
- Inconsistent Interfaces: Different third-party services have different methods and properties, making it difficult to use them interchangeably.
- Code Duplication: Without a proper structure, you might end up duplicating code to adapt each service's interface to your application's requirements.
Take a moment to think about how you would solve this problem. How can you create a uniform interface to interact with different third-party services?
An example of bad design
// language: ruby
class ServiceA
def fetch_data
# Fetch data from Service A
"Data from Service A"
end
end
class ServiceB
def get_info
# Get info from Service B
"Info from Service B"
end
end
class ServiceC
def retrieve_details
# Retrieve details from Service C
"Details from Service C"
end
end
# Client code
def display_data(service)
case service
when ServiceA
puts service.fetch_data
when ServiceB
puts service.get_info
when ServiceC
puts service.retrieve_details
else
raise "Unknown service type"
end
end
# Usage
service_a = ServiceA.new
service_b = ServiceB.new
service_c = ServiceC.new
display_data(service_a) # Output: Data from Service A
display_data(service_b) # Output: Info from Service B
display_data(service_c) # Output: Details from Service CIssues with this implementation:
- Poor Maintainability: The display_data method becomes hard to maintain as the logic for handling different services grows. Any change in the service interfaces requires updating the client code.
- Code Duplication: Common logic for displaying data is duplicated for each service type.
- Scalability: Adding a new service requires modifying the display_data method, which increases the risk of introducing bugs.
Adapter Pattern
The Adapter pattern is a structural design pattern that allows objects with incompatible interfaces to work together. It acts as a bridge between two incompatible interfaces by wrapping an existing class with a new interface.
Key Characteristics:
Key Characteristics:
- Interface Compatibility: Converts the interface of a class into another interface that a client expects.
- Reusability: Promotes code reusability by allowing existing classes to be used in new ways without modification.
- Single Responsibility Principle: Adheres to the Single Responsibility Principle by separating the interface adaptation logic from the business logic.
Implementing Adapter Pattern in Ruby
By using the Adapter pattern, we can encapsulate the interface adaptation logic, making the code more maintainable, scalable, and adhering to the Single Responsibility Principle.
// language: ruby
# Target interface
class DataService
def get_data
raise NotImplementedError, 'Subclasses must override this method'
end
end
# Adaptee 1
class ServiceA
def fetch_data
"Data from Service A"
end
end
# Adapter for ServiceA
class ServiceAAdapter < DataService
def initialize(service_a)
@service_a = service_a
end
def get_data
@service_a.fetch_data
end
end
# Adaptee 2
class ServiceB
def get_info
"Info from Service B"
end
end
# Adapter for ServiceB
class ServiceBAdapter < DataService
def initialize(service_b)
@service_b = service_b
end
def get_data
@service_b.get_info
end
end
# Adaptee 3
class ServiceC
def retrieve_details
"Details from Service C"
end
end
# Adapter for ServiceC
class ServiceCAdapter < DataService
def initialize(service_c)
@service_c = service_c
end
def get_data
@service_c.retrieve_details
end
end
# Client code
def display_data(service)
puts service.get_data
end
# Usage
service_a_adapter = ServiceAAdapter.new(ServiceA.new)
service_b_adapter = ServiceBAdapter.new(ServiceB.new)
service_c_adapter = ServiceCAdapter.new(ServiceC.new)
display_data(service_a_adapter) # Output: Data from Service A
display_data(service_b_adapter) # Output: Info from Service B
display_data(service_c_adapter) # Output: Details from Service CBenefits of this implementation:
- Interface Compatibility: Each adapter class adapts a specific service to the target interface (DataService), making it easy to use different services interchangeably.
- Maintainability: The client code (display_data) is simplified and does not need to be modified when new services are added.
- Scalability: Adding a new service only requires creating a new adapter class without modifying existing code.
- Single Responsibility Principle: The adaptation logic is separated from the business logic, making the code more modular and easier to maintain.
More Practical Examples
1. Payment Gateway Integration
A payment system can use the Adapter pattern to integrate with different payment gateways such as PayPal, Stripe, and Square.
A payment system can use the Adapter pattern to integrate with different payment gateways such as PayPal, Stripe, and Square.
// language: ruby
# Target interface
class PaymentProcessor
def process_payment(amount)
raise NotImplementedError, 'Subclasses must override this method'
end
end
# Adaptee 1
class PayPal
def send_payment(amount)
"Processing #{amount} payment through PayPal"
end
end
# Adapter for PayPal
class PayPalAdapter < PaymentProcessor
def initialize(paypal)
@paypal = paypal
end
def process_payment(amount)
@paypal.send_payment(amount)
end
end
# Adaptee 2
class Stripe
def make_payment(amount)
"Processing #{amount} payment through Stripe"
end
end
# Adapter for Stripe
class StripeAdapter < PaymentProcessor
def initialize(stripe)
@stripe = stripe
end
def process_payment(amount)
@stripe.make_payment(amount)
end
end
# Adaptee 3
class Square
def execute_payment(amount)
"Processing #{amount} payment through Square"
end
end
# Adapter for Square
class SquareAdapter < PaymentProcessor
def initialize(square)
@square = square
end
def process_payment(amount)
@square.execute_payment(amount)
end
end
# Client code
def process_payment(payment_processor, amount)
puts payment_processor.process_payment(amount)
end
# Usage
paypal_adapter = PayPalAdapter.new(PayPal.new)
stripe_adapter = StripeAdapter.new(Stripe.new)
square_adapter = SquareAdapter.new(Square.new)
process_payment(paypal_adapter, 100) # Output: Processing 100 payment through PayPal
process_payment(stripe_adapter, 200) # Output: Processing 200 payment through Stripe
process_payment(square_adapter, 300) # Output: Processing 300 payment through Square2. File Reading
A file reader system can use the Adapter pattern to read different types of files such as CSV, JSON, and XML.
// language: ruby
# Target interface
class FileReader
def read_file
raise NotImplementedError, 'Subclasses must override this method'
end
end
# Adaptee 1
class CSVFile
def read_csv
"Reading CSV file"
end
end
# Adapter for CSVFile
class CSVFileReader < FileReader
def initialize(csv_file)
@csv_file = csv_file
end
def read_file
@csv_file.read_csv
end
end
# Adaptee 2
class JSONFile
def read_json
"Reading JSON file"
end
end
# Adapter for JSONFile
class JSONFileReader < FileReader
def initialize(json_file)
@json_file = json_file
end
def read_file
@json_file.read_json
end
end
# Adaptee 3
class XMLFile
def read_xml
"Reading XML file"
end
end
# Adapter for XMLFile
class XMLFileReader < FileReader
def initialize(xml_file)
@xml_file = xml_file
end
def read_file
@xml_file.read_xml
end
end
# Client code
def read_file(file_reader)
puts file_reader.read_file
end
# Usage
csv_reader = CSVFileReader.new(CSVFile.new)
json_reader = JSONFileReader.new(JSONFile.new)
xml_reader = XMLFileReader.new(XMLFile.new)
read_file(csv_reader) # Output: Reading CSV file
read_file(json_reader) # Output: Reading JSON file
read_file(xml_reader) # Output: Reading XML fileConclusion
The Adapter pattern is a powerful tool for integrating incompatible interfaces, ensuring that different classes can work together seamlessly. It promotes maintainability, scalability, and adherence to the Single Responsibility Principle.
Happy coding!
Happy coding!
Related blogs
Speed Up Independent Queries Using Rails load_async
When you're building a dashboard, it's common to fetch multiple, independent datasets. The page loading might be slow because it has to fetch all data to render a page. A common solution is using AJAX to load pieces of the dashboard, which is great, ...
Software Engineer
Software Engineer
Hello Golang: My First Steps to the Language
I’ve worked with Ruby in several projects, which is defined as "a programmer’s best friend" for its clean and English-like syntax. While my back-end experience is rooted in the Ruby on Rails framework, I prefer TypeScript for building CLI tools and s...
Software Engineer
Software Engineer