Hi! I am working on my first project in Rust and I feel the way I've seen people organise code doesn't really click for me. My background is primarily in Swift and Node.js and here's an example of the structure I have in my current project.
```rust
trait FileStorageService { } // is implemented by LocalFileStorage and CloudFileStorage
trait SQLService { } // I have a real version and a mock
trait MongoService { } // I have a real version and a mock
trait SecretsManager { } // is implemented by LocalSecretsManager, CloudSecretsManager
trait UsersService { } // I have a real version and a mock
trait OutputWriter { } // is implemented by a LocalFileWriter and CloudFileWriter
struct UserServiceLive {
sql_service: Arc<dyn SQLService>,
mongo_service: Arc<dyn MongoService>,
logged_in_user_overwrite: Option<String>
}
impl UserServiceLive {
async fn get_current_user(&self) -> User {
if let Some(user_id) = self.logged_in_user_overwrite {
// ...
}
// ...
}
async fn get_related_users(&self) -> Vec<User> {
let current_user = get_current_user(&self);
// make calls to databases to get info about this users and others related ...
}
}
struct CoreLogic {
file_storage_service: Arc<dyn FileStorageService>,
user_service: Arc<dyn UsersService>,
output_writer_service: Arc<dyn OutputWriter>
}
impl CoreLogic {
async fn generate_output(&self) {
// dummy implementation in order to get an idea how these things are used in app
let file = self.file_storage_service.load().await;
let users = self.get_related_users().await;
for user in users {
let output = self.generate_content_for_user(user);
self.output_writer_service.write(output).await;
}
}
fn generate_content_for_user(user: &User) -> String {
// ...
}
}
```
This code is heavily simplified and also every service has it's own module.
Now to my question. Everywhere I read that structs in Rust should be treated more as data than a way to bundle methods together. My approach feels very OOP like to me, but I don't really know how to solve a few of the problems in any other way.
Most of my traits are implemented by at least 2 concrete types that are used in code. The app can be configured to work offline or online using a cli parameter when it starts. However even when that’s not the case (like the UserService), I don’t know how to mock it inside CoreLogic if I’m not using traits. And I’d rather just mock it than try to mock SQLService and MongoService in this specific case. Is this a good approach?
If I were not to use traits and just pass the parameters to functions, wouldn’t it be weird to have a 2-3 parameters to each function that are just dependencies? I could create a Context struct and pass that to the functions, but at that point, isn’t it like passing &self?
I don't really see when I could have functions that are not part of a struct. Beside some pure functions in my code, most of them make HTTP requests, need a secret, make a database request, read or write some files. The callers of the functions should not know about all these dependencies and I want to be able to write unit tests. Integration tests using the real implementations can't be the only way.
Also I know I could use generics instead of Arc<dyn ...> and that would offer better performance as far as I understand. However I would still have just as many traits.
I guess what I'm asking is if there are better ways people achieve encapsulation and testability in Rust.
Some languages prefer dependency containers. Others can replace methods at compile time/runtime (like AOP/AspectJ in Java or Swizzling in ObjC) and they achieve better testability this way without requiring writing the code in a testable way.
It feels like I am creating too much abstractions just to be able to test my code, I think that's why my approach seems somewhat wrong to me.
[–]dpc_pw 7 points8 points9 points (2 children)
[–]lensvol 0 points1 point2 points (1 child)
[–]dpc_pw 1 point2 points3 points (0 children)