Skip to content

Instantly share code, notes, and snippets.

@bsodmike
Created February 4, 2025 12:30
Show Gist options
  • Save bsodmike/577a0838d1167cbee6905a6ab8d3cbaf to your computer and use it in GitHub Desktop.
Save bsodmike/577a0838d1167cbee6905a6ab8d3cbaf to your computer and use it in GitHub Desktop.
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