3
\$\begingroup\$

From my experience in C# programming I think that DI is important. But it's not possible to do it same way in Rust. There are some DI frameworks but I've come with an idea on how it can be made and decided to implement it my way. My implementation uses only constructor injection, no container needed. I'm only learning Rust now so can you review my code, find possible pitfalls, suggest improvements or better approaches?

My goals in DI:

  1. Make stubs possible for testing.
  2. Systems shouldn't know "sub-dependencies" of their dependencies. Separate construction and providing dependencies from actual usage. All dependencies are "wired" in one place.
  3. No global shared state.
  4. The design principle that higher level systems declare what they need and lower level systems implement it, not other way.
  5. Avoid God objects.
  6. All dependencies can be clearly determined by a constructor signature, no looking inside code needed. Therefore I don't use Resource Locator pattern.
#[cfg(not(feature = "test"))]
mod Bindings {
    // real implementations binding

    // I assume only one implementor per each interface
    // this is not always the case but al least it's good for a simple scenario
    pub type Door = crate::Autocar::DoorMod::DoorImpl;
    pub type Engine = crate::Autocar::EngineMod::EngineImpl;
}

// how can I move these into actual tests files?
#[cfg(all(feature = "test", feature = "test1"))]
mod Bindings {
    // stubs for test1 can be binded here
    pub type Door = crate::Autocar::DoorMod::DoorImpl;
    pub type Engine = crate::Autocar::EngineMod::EngineImpl;
}

#[cfg(all(feature = "test", feature = "test2"))]
mod Bindings {
    // stubs for test2 can be binded here
    pub type Door = crate::Autocar::DoorMod::DoorImpl;
    pub type Engine = crate::Autocar::EngineMod::EngineImpl;
}

// prelude for internal use
mod Usings {
    pub use crate::Bindings::*;
    pub use std::cell::RefCell;
    pub use std::rc::Rc;
    pub type Mrc<T> = Rc<RefCell<T>>; // Mutable Reference Counter
    pub fn Mrc<T>(v: T) -> Mrc<T> {
        Rc::new(RefCell::new(v))
    }
}

fn main() {
    // this code performs constructor injection itself
    // all constructors are called here

    use Autocar::*;
    use Usings::*;

    let engine = Mrc(Engine::new());

    // also we can make factory methods
    let make_door = || -> Door { Door::new(engine.clone()) };

    let doors = vec![make_door(), make_door()];
    let mut car = Car::new(engine, doors);

    // all constructed, now run something
    car.doors[0].open();
}

// now application code
mod Autocar {
    use crate::Usings::*;

    // top-level struct so no interface
    pub struct Car {
        // Since same Engine is used also by a Door too, I have to use Mrc.
        // This may become an issue as once a dependency becomes
        // used by multiple structs I have to change it everywhere to Mrc
        // and insert borrow_mut() everywhere.
        // Which doesn't look like a good design. But no choice. Or?
        pub engine: Mrc<Engine>,

        pub doors: Vec<Door>,
    }

    impl Car {
        pub fn new(engine: Mrc<Engine>, doors: Vec<Door>) -> Car {
            Car { engine, doors }
        }
    }

    // declare Car dependencies:

    // we actually need IDoor so stubs can inherit it and reflect signature changes when refactoring
    pub trait IDoor {
        fn is_opened(&self) -> bool;
        fn open(&mut self);
    }

    pub trait IEngine {
        fn is_running(&self) -> bool;
        fn start(&mut self);
        fn stop(&mut self);
    }

    pub(crate) mod DoorMod {
        use super::*;
        use crate::Usings::*;

        pub struct DoorImpl {
            // I tried to design the code in a way so that DI doesn't prevent optimizations.
            // So I don't use IEngine here or otherwise it becomes dyn implicitly and then
            // no inlining and can't be placed on the stack.
            // But one issue with this approach is that IntelliSense can see
            // all EngineImpl functions even if it implements multiple traits, not just IEngine.
            // But a stub will contain only interface-declared functions
            // so it will be at least checked by the compiler.
            engine: Mrc<Engine>,
        }

        impl IDoor for DoorImpl {
            fn is_opened(&self) -> bool {
                unimplemented!()
            }

            fn open(&mut self) {
                if self.engine.borrow().is_running() {
                    self.engine.borrow_mut().stop();
                }
                println!("opening")
            }
        }

        impl DoorImpl {
            pub fn new(engine: Mrc<Engine>) -> Self {
                DoorImpl { engine }
            }
        }
    }

    pub(crate) mod EngineMod {
        use super::*;
        use crate::Usings::*;

        pub struct EngineImpl;

        impl IEngine for EngineImpl {
            fn is_running(&self) -> bool {
                true
            }

            fn start(&mut self) {
                println!("starting");
            }

            fn stop(&mut self) {
                println!("stopping");
            }
        }

        impl EngineImpl {
            pub fn new() -> Self {
                EngineImpl {}
            }
        }
    }
}
\$\endgroup\$
0
\$\begingroup\$

Don't try to follow C# DI patterns in Rust. That path will lead you to nothing but pain. Rust is very different from C#, and the sort of patterns that make sense there won't fly in Rust.

#[cfg(all(feature = "test", feature = "test2"))]
mod Bindings {
    // stubs for test2 can be binded here
    pub type Door = crate::Autocar::DoorMod::DoorImpl;
    pub type Engine = crate::Autocar::EngineMod::EngineImpl;
}

This is horrific abuse of the cfg feature. Do NOT do this. Cfg settings are project-wide bits of configuration and not suited to changing out different configurations for testing scenarios.

The answer, most of the time, is that you don't need to stub things for testing. That's an antipattern you learned in other languages that you need to unlearn in Rust.

In the small number of cases that you really do to stub things out, use generics:

struct Car<Engine: IEngine, Door: IDoor>

Then you can simply instantiate with different implementions of your traits.

pub use std::cell::RefCell;
pub use std::rc::Rc;
pub type Mrc<T> = Rc<RefCell<T>>; // Mutable Reference Counter
pub fn Mrc<T>(v: T) -> Mrc<T> {
    Rc::new(RefCell::new(v))
}

If you find yourself reaching for RefCell or Rc in Rust, it means you need to rethink your design. They are sometimes neccessary, but usually it means you are still thinking in another language.

\$\endgroup\$
7
  • \$\begingroup\$ "struct Car<Engine: IEngine, Door: IDoor>" - this breaks the #2 item in my list. Everyone who uses Car now knows dependencies of it: Engine and Door. \$\endgroup\$ – Vlad Nov 14 '20 at 12:58
  • \$\begingroup\$ "The answer, most of the time, is that you don't need to stub things for testing." - why? Integration testing is cool but when you need to test your struct independently from its dependencies how would you do this in Rust besides inserting generics everywhere? \$\endgroup\$ – Vlad Nov 14 '20 at 13:22
  • \$\begingroup\$ "If you find yourself reaching for RefCell or Rc in Rust, it means you need to rethink your design." - isn't it common to have a subsystem that is used by multiple other systems? E.g. it can be MailSender which also tracks statistics on sent mails, and multiple parts of your app would use it in a mutable way. How would you do it without Rc/RefCell? \$\endgroup\$ – Vlad Nov 14 '20 at 13:26
  • \$\begingroup\$ @Vlad, I'd be happy to answer your questions. But, what I'd suggest is that you pick a real example that we could look at. This example doesn't do anything, so I can't give anything beyond really abstract advice on these subjects. If you had an example which you thought was helped by using this DI approach (maybe one not even in Rust), I could give more helpful indication of how it could be solved without using these Di techniques. \$\endgroup\$ – Winston Ewert Nov 14 '20 at 22:54
  • \$\begingroup\$ If you insist, you can avoid the necessity of users of Car knowing about its dependencies using an ICar trait. Users of Car would then only use it via the trait, thus not needing to know about its dependencies. \$\endgroup\$ – Winston Ewert Nov 15 '20 at 0:05
0
\$\begingroup\$

I didn't want to use generics because I thought that all dependency users would be forced to specify all subdependencies too. It turned out to be false in Rust. Here is what I mean:

mod Usings {
    pub use std::cell::RefCell;
    pub use std::rc::Rc;
    pub type Mrc<T> = Rc<RefCell<T>>; // Mutable Reference Counter
    pub fn Mrc<T>(v: T) -> Mrc<T> {
        Rc::new(RefCell::new(v))
    }
}

fn main() {
    // this code performs constructor injection itself
    // all constructors are called here

    use Autocar::DoorMod::*;
    use Autocar::EngineMod::*;
    use Autocar::*;
    use Usings::*;

    let engine = Mrc(Engine::new());

    // also we can make factory methods
    let make_door = || -> Door<Engine> { Door::new(engine.clone()) };

    let doors = vec![make_door(), make_door()];
    let mut car = Car::new(engine, doors);

    // all constructed, now run something
    car.doors[0].open();
}

// now application code
mod Autocar {
    use crate::Usings::*;

    // top-level struct so no interface
    pub struct Car<TEngine: IEngine, TDoor: IDoor> {
        // Since same Engine is used also by a Door too, I have to use Mrc.
        // This may become an issue as once a dependency becomes
        // used by multiple structs I have to change it everywhere to Mrc
        // and insert borrow_mut() everywhere.
        // Which doesn't look like a good design. But no choice. Or?
        pub engine: Mrc<TEngine>,

        pub doors: Vec<TDoor>,
    }

    impl<TEngine: IEngine, TDoor: IDoor> Car<TEngine, TDoor> {
        pub fn new(engine: Mrc<TEngine>, doors: Vec<TDoor>) -> Self {
            Car { engine, doors }
        }
    }

    // declare Car dependencies:

    pub trait IDoor {
        fn is_opened(&self) -> bool;
        fn open(&mut self);
    }

    pub trait IEngine {
        fn is_running(&self) -> bool;
        fn start(&mut self);
        fn stop(&mut self);
    }

    pub(crate) mod DoorMod {
        use super::*;
        use crate::Usings::*;

        pub struct Door<TEngine: IEngine> {
            engine: Mrc<TEngine>,
        }

        impl<TEngine: IEngine> IDoor for Door<TEngine> {
            fn is_opened(&self) -> bool {
                unimplemented!()
            }

            fn open(&mut self) {
                if self.engine.borrow().is_running() {
                    self.engine.borrow_mut().stop();
                }
                println!("opening")
            }
        }

        impl<TEngine: IEngine> Door<TEngine> {
            pub fn new(engine: Mrc<TEngine>) -> Self {
                Door { engine }
            }
        }
    }

    pub(crate) mod EngineMod {
        use super::*;
        use crate::Usings::*;

        pub struct Engine;

        impl IEngine for Engine {
            fn is_running(&self) -> bool {
                true
            }

            fn start(&mut self) {
                println!("starting");
            }

            fn stop(&mut self) {
                println!("stopping");
            }
        }

        impl Engine {
            pub fn new() -> Self {
                Engine {}
            }
        }
    }
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.