I agree that youâre probably right about
especially if they seem workable to you.
Just one more detail I would be interested in: You mention existing code using various aspects of the âcurrent interfaceâ already
I would be interested what parts of the interface youâre referring to. I.e.
- is there much code already using the
Codable
trait?
- are you also referring to much existing code using the
EvenStream
trait?
- does this âmany components that use the current interfaceâ also refer to the
EventStore
trait?
Iâm asking only because a refactor to allow type-erased users would likely work well without changing anything about Codable
or its implementors or users, similar to how erased-serde
does for the serde-traits. But, as you notice yourself already, then the fact that the method
fn create_stream<T: Codable>(&self, name: String) -> Result<Box<dyn EventStream<T>>, EventStoreError>
only takes the information of what type T
is in the form of a type argument means that at least the EventStore
trait (and then all of its implementors) would require some form of refactoring.
The final direction to look would be âupâ. I.e. instead of looking down through the details of the EventStream
and Codable
and eliminate the need for generic parameters, instead your use case of things like your State
struct can be looked at. Even though the API of EventStream
isnât, the API of State
itself might end up being completely compatible with object safety again.
This would in particular be possible if your intended use-case
pub struct State {
Store: Box<dyn EventStore>, // <- of course this doesnât work
}
doesnât make any use of the EventStore
at full generality. This is in particular the case if State
itself has no API with a generic T: Codable
parameter because itâs only the person that defines and implements the details of the State
struct that decides which concrete T: Codable
types end up being used.
mod state {
use crate::EventStore;
pub struct State {
store: Box<dyn EventStoreInState>, // instead of `dyn EventStore`
extra: ExtraFields,
}
struct ExtraFields(/* ⌠*/);
// intended API:
/*
impl State {
pub fn new(e: impl EventStore) -> Self {
todo!();
}
pub fn foo(&self) {
// want to do stuff, while using `store` as an actual `EventStore`
todo!();
}
pub fn bar(&mut self) {
// want to do stuff, while using `store` as an actual `EventStore`
todo!();
}
}
*/
// How to actually do it
// private trait, as implementation detail of `State`:
trait EventStoreInState {
// mirror (relevant subset of) `State` API signatures,
// pass view of extra fields of `State` if necessary
// these need to be dyn-compatible (e.g. no generics)
// but if they are, itâs really straightforward what follows next
fn foo(&self, extra: &ExtraFields);
fn bar(&mut self, extra: &mut ExtraFields);
}
// next, simply delegate to the trait, which gives the majority of the API already
impl State {
pub fn foo(&self) {
self.store.foo(&self.extra);
}
pub fn bar(&mut self) {
self.store.foo(&mut self.extra);
}
}
// construction is easy, too:
impl State {
pub fn new(e: impl EventStore + 'static) -> Self {
Self {
store: Box::new(e), // this works because of below implementation
extra: ExtraFields(/* ⌠*/),
}
}
}
// finally, add the actual code/logic for implementing `foo`, `bar`!
// this also establishes how to create the `Box<dyn EventStoreInState>`
// in the constructor
impl<E: EventStore> EventStoreInState for E {
fn foo(&self, extra: &ExtraFields) {
// at this place, we *do* have access to `self`
// as a *true* `EventStore`, and also to all extra
// fields of `State`
}
fn bar(&mut self, extra: &mut ExtraFields) {
// at this place, we *do* have access to `self`
// as a *true* `EventStore`, and also to all extra
// fields of `State`
}
}
}
if your intended API of State
does include any other generics, thatâs additional complication, I suppose, and whether or not it can be avoided depends on the exact details. (Of course, if those methods donât directly interact with the EventStore
, but only through other non-generic helper methods, thatâs completely fine; they just donât become part of that internal EventStoreInState
helper interface at all.)