Adapter Design Pattern in Rust

1. Definition

The Adapter pattern is a structural design pattern that allows objects with incompatible interfaces to collaborate. It acts as a bridge between two incompatible interfaces by providing a wrapper that allows one object to be used as if it were another.

2. Problem Statement

Consider a scenario where you've developed a logging system that expects logs in JSON format. However, you're using a third-party library that provides logs in XML format. These two interfaces are incompatible, and direct communication isn't possible without modifying the logging system or the third-party library.

3. Solution

The Adapter pattern can be employed to solve this problem. By creating an adapter class that converts XML logs to JSON logs, we can make the two interfaces compatible without changing their existing code.

4. Real-World Use Cases

1. Translating requests from one API to another in microservices.

2. Wrappers used for legacy systems to fit into newer systems.

3. Adapting third-party libraries to fit into existing systems.

5. Implementation Steps

1. Define the interface your system expects (Target Interface).

2. Create an adapter class that implements the target interface.

3. In the adapter class, include a reference to the object you want to adapt.

4. Implement the methods of the target interface in the adapter using the adapted object's methods.

6. Implementation in Rust Programming

// Step 1: Define the target interface
pub trait JsonLogger {
    fn log(&self, data: &str);
}
// The existing class with an incompatible interface
pub struct XmlLogger;
impl XmlLogger {
    pub fn log_xml(&self, xml_data: &str) {
        println!("Logging XML: {}", xml_data);
    }
}
// Step 2: Create the adapter class
pub struct LoggerAdapter {
    xml_logger: XmlLogger,
}
impl LoggerAdapter {
    pub fn new(xml_logger: XmlLogger) -> Self {
        LoggerAdapter { xml_logger }
    }
}
// Step 3 & 4: Implement the target interface using the adapted object's methods
impl JsonLogger for LoggerAdapter {
    fn log(&self, data: &str) {
        // Here, we're simply passing the JSON data to the XML logger
        // In a real-world scenario, you would convert the JSON data to XML format
        self.xml_logger.log_xml(data);
    }
}
// Client code
fn main() {
    let xml_logger = XmlLogger;
    let logger = LoggerAdapter::new(xml_logger);
    logger.log("{\"message\": \"This is a log.\"}");
}

Output:

"Logging XML: {"message": "This is a log."}"

Explanation:

1. JsonLogger is the target interface that our system expects.

2. XmlLogger is the existing class with an incompatible interface.

3. LoggerAdapter is the adapter that makes XmlLogger compatible with JsonLogger.

4. In the client code (main), we use LoggerAdapter to adapt the XmlLogger and log a message. Although our system expects to log in JSON format, the adapted XML logger handles it seamlessly.

7. When to use?

The Adapter pattern is useful when:

1. You want to use an existing class, but its interface doesn't match what you need.

2. You want to create a reusable class that cooperates with unrelated or unforeseen classes, i.e., classes that don't necessarily have compatible interfaces.

3. You need to adapt several different classes to a uniform interface.


Comments