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 C
Issues 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 C
Benefits 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 Square
2. 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 file
Conclusion
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!
Created at
2024-09-21 15:53:29 +0700
Related blogs
One Design Pattern a Week: Week 2
Welcome back to my "One Design Pattern a Week" series!
Try to solve this real problem: Object CreationImagine you're developing a large web applicatio...
Software Engineer
Software Engineer
Design Pattern
Design Pattern
2024-09-21 15:38:03 +0700
How Google achieves seamless SSO across multiple domains like Gmail and Youtube?
Hey there! Ever wondered how you can log into Gmail and then magically find yourself logged into YouTube, Google Drive, and all other Google services ...
Software Engineer
Software Engineer
2024-09-24 22:52:06 +0700