Created
February 4, 2025 12:30
-
-
Save bsodmike/577a0838d1167cbee6905a6ab8d3cbaf to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
If the `InternalAPI` should not be part of the `ItemsCollection`, we can decouple it by using **dependency injection** or **traits**. This way, the `ItemsCollection` can interact with the `InternalAPI` without directly owning or knowing about its implementation. This approach aligns with the **Dependency Inversion Principle** and makes the code more modular and testable. | |
Here’s how you can refactor the code to achieve this: | |
--- | |
### Refactored Code | |
```rust | |
use reqwest::blocking::Client; | |
use serde::Serialize; | |
use std::collections::HashSet; | |
// Public API | |
pub struct ItemsCollection { | |
items: HashSet<String>, | |
} | |
impl ItemsCollection { | |
pub fn new() -> Self { | |
Self { | |
items: HashSet::new(), | |
} | |
} | |
// Public method to add an item | |
pub fn add_item(&mut self, item: String, internal_api: &impl InternalAPI) { | |
self.items.insert(item.clone()); | |
internal_api.send_item_to_external_service(&item); | |
} | |
// Public method to remove an item | |
pub fn remove_item(&mut self, item: &str, internal_api: &impl InternalAPI) -> bool { | |
if self.items.remove(item) { | |
internal_api.send_item_to_external_service(item); | |
true | |
} else { | |
false | |
} | |
} | |
} | |
// Internal API Trait | |
pub trait InternalAPI { | |
fn send_item_to_external_service(&self, item: &str); | |
} | |
// Concrete implementation of the InternalAPI trait | |
pub struct ExternalServiceAPI { | |
client: Client, | |
external_service_url: String, | |
} | |
impl ExternalServiceAPI { | |
pub fn new() -> Self { | |
Self { | |
client: Client::new(), | |
external_service_url: "https://example.com/api/items".to_string(), | |
} | |
} | |
} | |
impl InternalAPI for ExternalServiceAPI { | |
fn send_item_to_external_service(&self, item: &str) { | |
let item_details = ItemDetails { | |
name: item.to_string(), | |
}; | |
if let Err(e) = self.client.post(&self.external_service_url) | |
.json(&item_details) | |
.send() { | |
eprintln!("Failed to send item to external service: {}", e); | |
} | |
} | |
} | |
// Struct representing the item details to be sent to the external service | |
#[derive(Serialize)] | |
struct ItemDetails { | |
name: String, | |
} | |
fn main() { | |
let mut collection = ItemsCollection::new(); | |
let internal_api = ExternalServiceAPI::new(); | |
// Adding items | |
collection.add_item("item1".to_string(), &internal_api); | |
collection.add_item("item2".to_string(), &internal_api); | |
// Removing an item | |
collection.remove_item("item1", &internal_api); | |
} | |
``` | |
--- | |
### Key Changes and Explanation | |
1. **Decoupling `InternalAPI` from `ItemsCollection`**: | |
- The `ItemsCollection` no longer owns or directly depends on the `InternalAPI`. | |
- Instead, the `InternalAPI` is passed as a parameter to the `add_item` and `remove_item` methods. This is **dependency injection**. | |
2. **Trait for `InternalAPI`**: | |
- A trait `InternalAPI` is defined to abstract the behavior of sending item details to an external service. | |
- This allows you to define multiple implementations of the `InternalAPI` (e.g., for testing, mocking, or different external services). | |
3. **Concrete Implementation**: | |
- The `ExternalServiceAPI` struct implements the `InternalAPI` trait. | |
- It contains the logic for sending HTTP POST requests to the external service. | |
4. **Flexibility and Testability**: | |
- By using a trait, you can easily mock the `InternalAPI` for unit testing or swap out the implementation without modifying the `ItemsCollection`. | |
5. **Usage in `main`**: | |
- The `ExternalServiceAPI` is instantiated and passed to the `ItemsCollection` methods when adding or removing items. | |
--- | |
### Benefits of This Approach | |
- **Separation of Concerns**: The `ItemsCollection` is only responsible for managing the collection of items, while the `InternalAPI` handles communication with the external service. | |
- **Testability**: You can create a mock implementation of the `InternalAPI` trait for testing purposes. | |
- **Extensibility**: You can easily add new implementations of the `InternalAPI` trait without modifying the `ItemsCollection`. | |
--- | |
### Example of a Mock Implementation for Testing | |
```rust | |
#[cfg(test)] | |
mod tests { | |
use super::*; | |
// Mock implementation of InternalAPI for testing | |
struct MockInternalAPI { | |
sent_items: Vec<String>, | |
} | |
impl MockInternalAPI { | |
fn new() -> Self { | |
Self { sent_items: Vec::new() } | |
} | |
} | |
impl InternalAPI for MockInternalAPI { | |
fn send_item_to_external_service(&self, item: &str) { | |
// Simulate sending the item by storing it in a vector | |
self.sent_items.push(item.to_string()); | |
} | |
} | |
#[test] | |
fn test_add_item() { | |
let mut collection = ItemsCollection::new(); | |
let mock_api = MockInternalAPI::new(); | |
collection.add_item("item1".to_string(), &mock_api); | |
assert!(collection.items.contains("item1")); | |
assert_eq!(mock_api.sent_items, vec!["item1"]); | |
} | |
} | |
``` | |
This mock implementation allows you to test the `ItemsCollection` without making actual HTTP requests. | |
--- | |
This design ensures that the public API remains clean and consistent, while the internal API is decoupled and can be easily modified or replaced. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment