Table of contents
    blog cover

    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:
    • 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:
    1. 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.
    2. Code Duplication: Common logic for displaying data is duplicated for each service type.
    3. 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:
    • 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:
    1. Interface Compatibility: Each adapter class adapts a specific service to the target interface (DataService), making it easy to use different services interchangeably.
    2. Maintainability: The client code (display_data) is simplified and does not need to be modified when new services are added.
    3. Scalability: Adding a new service only requires creating a new adapter class without modifying existing code.
    4. 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.
    // 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!
    Created at 2024-09-21 15:53:29 +0700

    Related blogs