Clean code: Where to put trait implementations?

What is the best place to put implementations of traits on types?

I have always put the implementations of a trait below the type implementing it, which resulted in files like this:

// type1.rs
struct Type1;

impl Trait1 for Type1 {
    // ...
}
impl Trait2 for Type1 {
    // ...
}

but eventually I noticed that this causes changes to the trait to trigger changes in many different places in the code. If Trait1 changes, I need to go to every type's file and update the implementation.

So now I went to put all implementations on types in the file where the trait is defined:

// trait1.rs
trait Trait1 {
    // ...
}
impl Trait1 for Type1 {
    // ...
}
impl Trait1 for Type2 {
    // ...
}

So now updating implementations of a trait only requires changes in one file and the implementations are easy to compare. But of course now if a type changes, I have the same problem as before, that I may need to update every place where that type is used.

Is there a definitive answer to which is better? How do you guys handle this decision?

If you're changing the trait more than you're adding new types implementing the trait, then consider whether you want an enum instead -- ADTs are the way to look at the types-vs-functions table from the other direction, where it's more common to add new operations to the existing possible data.

1 Like

I don't think enums would be a good fit for me, because many of the types I use are generic and this would need the enum to be generic aswell, which would result in a lot of type parameters everywhere that seem to be unrelated to the parameterized code. Traits also abstract away from the data in a way, and I would like to keep this property.

Another reason may be that enums will always require the memory of their largest variant, which can lead to performance degradation, I believe.

To answer the question: my convention is to group by whatever item is "mine" (i.e., in code controlled by me), and by the type if both the type and the trait are mine.

The reasoning goes like this: to me, the type is the "principal" entity, and the various behaviors it is bestowed are secondary, so I like to think in terms of the type first, and not in terms of whatever traits it might implement. (I am out of control as to what traits it implements anyway, because 3rd-parties are always free to add specific or blanket impls.)

However, if I'm in control of the trait only but not the type, then it'd be painful to have to chop up the impls into many different files/modules just for the sake of grouping-by-(3rd-party-)type, so for aesthetic reasons, I then fall back to grouping by trait.

So, to sum it up: I typically write

//! mytype.rs
use other_lib::{ExternTrait, ExternType1, ExternType2};
use mymodule::{MyTrait1, MyTrait2};

struct MyType {
   ...
}

impl ExternTrait for MyType {}
impl MyTrait1 for MyType {}
impl MyTrait2 for MyType {}

//! mytrait3.rs
trait MyTrait3 {}

impl MyTrait3 for ExternType1 {}
impl MyTrait3 for ExternType2 {}
2 Likes

Ah, that's totally fair. If you're intentionally wanting the type erasure, then stick with traits :+1:

1 Like