3 ms·
Here's how I currently am doing it: I use the repository pattern. I use a trait: pub trait LibraryRepository: Send + Sync + 'static { async fn create_s
by steveklabnik 2y ago
Here's how I currently am doing it: I use the repository pattern. I use a trait:
pub trait LibraryRepository: Send + Sync + 'static {
async fn create_supplier(
&self,
request: supplier::CreateRequest,
) -> Result<Supplier, supplier::CreateError>;
I am splitting things "vertically" (aka by feature) rather than "horizontally" (aka by layer). So "library" is a feature of my app, and "suppliers" are a concept within that feature. This call ultimately takes the information in a CreateRequest and inserts it into a database.
My implementation looks something like this:
impl LibraryRepository for Arc<Sqlite> {
async fn create_supplier(
&self,
request: supplier::CreateRequest,
) -> Result<Supplier, supplier::CreateError> {
let mut tx = self
.pool
.begin()
.await
.map_err(|e| anyhow!(e).context("failed to start SQLite transaction"))?;
let name = request.name().clone();
let supplier = self.create_supplier(&mut tx, request).await.map_err(|e| {
anyhow!(e).context(format!("failed to save supplier with name {name:?}"))
})?;
tx.commit()
.await
.map_err(|e| anyhow!(e).context("failed to commit SQLite transaction"))?;
Ok(supplier)
}
where Sqlite is
#[derive(Debug, Clone)]
pub struct Sqlite {
pool: sqlx::SqlitePool,
}
You'll notice this basically:
1. starts a transaction
2. delegates to an inherent method with the same name
3. finishes the transaction
The inherent method has this signature:
impl Sqlite {
async fn create_supplier(
self: &Arc<Self>,
tx: &mut Transaction<'_, sqlx::Sqlite>,
request: supplier::CreateRequest,
) -> Result<Supplier, sqlx::Error> {
So, I can choose how I want to test: with a real database, or without.
If I want to write a test using a real database, I can do so, by testing the inherent method and passing it a transaction my test harness has prepared. sqlx makes this really nice.
If I'm testing some other function, and I want to mock the database, I create a mock implementation of LibraryService, and inject it there. Won't ever interact with the database at all.
In practice, my application is 95% end-to-end tests right now because a lot of it is CRUD with little logic, but the structure means that when I've wanted to do some more fine-grained tests, it's been trivial. The tradeoff is that there's a lot of boilerplate at the moment. I'm considering trying to reduce it, but I'm okay with it right now, as it's the kind that's pretty boring: the worst thing that's happened is me copy/pasting one of these implementations of a method and forgetting to change the message in that format!. I am also not 100% sure if I like using anyhow! here, as I think I'm erasing too much of the error context. But it's working well enough for now.
I got this idea from https://www.howtocodeit.com/articles/master-hexagonal-architecture-rust https://www.howtocodeit.com/articles/master-hexagonal-archit..., which I am very interested to see the final part of. (and also, I find the tone pretty annoying, but the ideas are good, and it's thorough.) I'm not 100% sure that I like every aspect of this specific implementation, but it's served me pretty well so far.
- bbkane 2y agoThanks so much for the detailed example. Bookmarking for when I need to find it again...
- steveklabnik 2y agoNo problem! I also left this response to someone on reddit who said similar: Nice. I want to write about my experiences someday, but some quick random thoughts about this: My repository files are huge. i need to break them up. More submodules can work, and defining the inherent methods in a different module than the trait implementation. I've found the directory structure this advocates, that is, ├── src │ ├── domain │ ├── inbound │ ├── outbound gets a bit weird when you're splitting things up by feature, because you end up re-doing the same directories inside of all three of the submodules. I want to see if moving to something more like ├── src │ ├── feature1 │ │ ├── domain │ │ ├── inbound │ │ ├── outbound │ ├── feature2 │ │ ├── domain │ │ ├── inbound │ │ ├── outbound feels better. Which is of course its own kind of repetition, but I feel like if I'm splitting by feature, having each feature in its own directory with the repetition being the domain/inbound/outbound layer making more sense. I'm also curious about if coherence will allow me to move this to each feature being its own crate. compile times aren't terrible right now, but as things grow... we'll see.
- BlackFly 2y agoYou've dodged the meat of my complaint by not having an example where you need to inject into a struct to test an implementation. Moreover, if you slap `Send + Sync + 'static` then you can certainly avoid the problems I am hinting at: you've committed to never having a lifetime and won't have to deal with the borrow checker. Inject a short lived bee into your example. A database that is only going to live for a finite time.
- steveklabnik 2y ago> You've dodged the meat of my complaint by not having an example where you need to inject into a struct to test an implementation. Sure. "Dr, it hurts... well stop doing that." Sometimes, you can design around the issue. I don't claim that this specific pattern works for everything, just that this is how my real-world application is built. > Moreover, if you slap `Send + Sync + 'static` then you can certainly avoid the problems I am hinting at: you've committed to never having a lifetime and won't have to deal with the borrow checker. Yes. Sometimes, one atomic increment on startup is worth not making your code more complex. > A database that is only going to live for a finite time. This is just not the case for a web application.