#pattern to create struct where several steps may fail

1 messages · Page 1 of 1 (latest)

amber lynx
#

hi folks! I was wondering whether somebody here might know of a good pattern to use here --

I have a struct like this ```rust
pub struct MyStruct {
a: ComponentA,
b: ComponentB,
c: ComponentC,
}


ComponentA has no dependency.
to create ComponentB, we need to have successfully created ComponentA.
to create ComponentC, we need to have successfully created ComponentB.

The creation of any of these components (A, B, or C)  may fail for a variety of reasons. 

Therefore, I have an enum to describe these cases ```rust
pub enum MyStructCreationError {
  CreatingAFailed,
  CreatingBFailedForReason1,
  CreatingBFailedForReason2,
  CreatingCFailed,
  etc.
}

I would like to provide a guarantee that if such a struct can be constructed, all of its subcomponents are adequately initialized.

So, I have a new function which returns a result:

impl MyStruct {
  pub fn new() -> Result<Self, MyStructCreationError> {
    todo!();
  }
}

My question is: what is the appropriate way to initialize a struct such as this? I thought at first to make ComponentB and ComponentC optional, and use (within the new function)

let mut x = Self {
  a,
  b: None,
  c: None,
};

x.maybe_initialize_b()?;
x.maybe_initialize_c()?;

Ok(x)

However, this seems flawed for a number of reasons.

I don't want to leak the Option interface everywhere. I would like to guarantee that if new returns Ok, we have a fully initialized struct. Otherwise, we return Err.

Essentially, I don't want the notion of Optional field values to need handling in all other methods.

Is there a good way to do this?

#

One other important detail: I would like to be able to manually trigger a reload of both B and C.

The full public interface of MyStruct is a new function, a maybe_reload function (which can fail and must otherwise successfully reload B and then C), and another function (something like get_component_c to access component C.

The ultimate goal is to be able to access a handle to a fully initialized component C such that we can reload the whole thing at any time and keep access to both A and B.

If reload fails, we should maintain the original valid state of A,B,and C.

As far as client code is concerned, both A and B are implementation details and need not be exposed

oblique root
#
impl MyStruct {
    pub fn new() -> Result<Self> {
        let a = create_a()?;
        let b = create_b(&a)?;
        let c = create_c(&b)?;
        Self { a, b, c }
    }
    pub fn maybe_reload(&mut self) -> Result<()> {
        let b = create_b(&self.a)?;
        let c = create_c(&self.b)?;
        self.b = b;
        self.c = c;
        Ok(())
    }
}
#

I don't know of a good way to de-duplicate the code thoroughly, but if you extract the construction it works like so

amber lynx
#

@oblique root thank you!! wow so simple after all

oblique root
#

and has no invalid states

amber lynx
#

brilliant

#

i think the slight duplication is actually ok here because the initialization logic need not depend on shared functionality with the creation logic

#

you save the day again 😃🌿 thank you!

oblique root
#

I do encourage that insofar as there is shared logic, you put it in separate functions (create_b in my example)

amber lynx
#

will do

oblique root
#

that way it's harder to accidentally introduce differences between creation and reloading

amber lynx
#

here, I think the shared logic is actually the create_a, create_b, and create_c functions you mentioned

#

it makes the dependency structure quite clear