3 ms·
Every time I try to learn more about Rust, I keep hitting a wall around OOP. I know Rust takes the stance that composition is better then inheritance, but I've
by seanalltogether 6y ago
Every time I try to learn more about Rust, I keep hitting a wall around OOP. I know Rust takes the stance that composition is better then inheritance, but I've never found the examples to be very convincing. How do rust developers deal with complex structs that contain lots of properties and features. For instance, if I need to create "CustomButton" that means I have to implement all the Traits from Button, which maybe requires Traits from LayoutView, which requires Traits from View, etc.... Additionally I need to maintain duplicate properties of those objects in my Custom Button rather then inheriting from a Button struct. It sounds like a headache to maintain.
- zozbot234 6y agoYou can do almost everything with a combination of traits (for interface inheritance), simple composition and the Deref trait (to auto-forward unimplemented functions to the contained class). The one feature you're missing compared to actual OOP inheritance is open recursion, which is a big footgun more than anything (see: the "fragile base class" problem). The Rust docs include an in-depth explanation of how Rust relates to traditional OOP: https://doc.rust-lang.org/book/ch17-00-oop.html https://doc.rust-lang.org/book/ch17-00-oop.html
- afranchuk 6y agoThere's a lot of detail and intricacy here (in that you could likely architect a design that didn't really rely on the sort of hierarchy you describe at all), but generally if you wanted something similar to [single] inheritance you could implement std::ops::Deref for the child types. That doesn't necessary cover things like virtual methods, but those could be implemented simply as boxed functions that child instances assign on creation.
- estebank 6y agoYou use composition and leverage the Deref trait to make the methods of specific fields available on the parent. This works on arbitrary levels of nesting which makes it look like OOP, but depending on what you're doing this is a good idea or not. https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=6961c82a243ed64594a7263db88110c8 https://play.rust-lang.org/?version=nightly&mode=debug&editi... As you can see this can be verbose, but you could have derive macros (like the Debug one I'm using here) to do this boilerplate for you. An alternative way of dealing with OOP inheritance is by using traits, where you implement the behavioral relationships by writing new traits for the behavior you want, adding super traits for them. https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=f895c919b5aa26ecb2f32e1922061454 https://play.rust-lang.org/?version=nightly&mode=debug&editi...
- Rusky 6y agoI can't speak for all Rust developers, but I try to avoid getting into situations where I need lots of properties and features on one type to begin with. Similar to the usual rule of thumb about keeping functions small, keeping types small requires that the APIs you're using support you in that goal. You're totally right that, once you're trying to pass a CustomButton somewhere that wants a T: LayoutView, where LayoutView: View, it's already too late for that! Some approaches I've used instead of the "all the traits from Button" approach: * Maybe I don't need quite that much abstraction, and I can instead use concrete types in the layout tree (to run with the Button example). Maybe instead of replacing an entire Button with a CustomButton, I can just set some fields on a Button, or inject the Custom part into a "userdata"-like field of Button. * Maybe I don't need everything about CustomButton to be stored in one place, and I can instead use a CustomButtonLayout type in the layout tree. Maybe the "rest" of the CustomButton just holds a reference or handle to connect it with CustomButtonLayout. (This is where people will hype the ECS pattern. IMO that is easy to go overboard with, but it is a good source of alternatives.) * The two points above are basically just each other's inverse, while still assuming a LayoutView trait or similar. But sometimes that is the thing getting in the way, and I just need to redraw the API boundaries to avoid the problem. Maybe a lower-level API that shifts more work to the client isn't so bad after all, and then it can just work with rectangles. Or maybe a higher-level API can take on the responsibilities of CustomButton and avoid the need for so many traits. To be fair, though, I do still wind up with inheritance-like patterns occasionally: * I have used `struct Base<D: Derived> { .., derived: D }` where the methods of `trait Derived` take a reference to `derived` and some of the other contents of `Base`. This emulates virtual methods and protected fields, where `Base`-using code works in terms of `Base<dyn Derived>`. * I have used `struct Derived { base: Base, .. }` with a `trait HasBase { fn base(&self) -> &Base; }`. This is closer to what you describe, but `Base`-using code instead works in terms of `B: HasBase` or `dyn HasBase`, and sprinkles in `.base()` to access `Base` directly, which collapses most of the annoying delegation boilerplate. (You can even hide the `.base()` calls if you replace `HasBase` with `Deref<Target = Base>`, though I prefer not to do that.) * I would eventually like to see fields in traits (as in this RFC: https://github.com/rust-lang/rfcs/pull/1546 https://github.com/rust-lang/rfcs/pull/1546). This would be a lot like the previous point, but a little more direct and perhaps more efficient. And also reminiscent of row polymorphism, which appeals to the language designer in me. :)