Idiomatic Rust - Builder Pattern

Поделиться
HTML-код
  • Опубликовано: 10 сен 2024

Комментарии • 102

  • @letsgetrusty
    @letsgetrusty  2 года назад +1

    📝Get your *FREE Rust cheat sheet* :
    www.letsgetrusty.com/cheatsheet

  • @redcrafterlppa303
    @redcrafterlppa303 2 года назад +8

    I was first introduced to the builder pattern in java. But in rust it really shines since it matches the language design better in my opinion.

    • @DipietroGuido
      @DipietroGuido 2 года назад

      That's coz Java is the worst programming language ever invented

    • @redcrafterlppa303
      @redcrafterlppa303 2 года назад +2

      @@DipietroGuido I disagree but if that's your opinion 🤷

  • @----__---
    @----__--- 2 года назад +106

    To be more idiomatic, I think the build method should have taken selfs ownership and consumed it. That way you wouldnt have needed to clone anything either.

    • @DrIngo1980
      @DrIngo1980 2 года назад +44

      Ideally I would not need an additional ServerBuilder struct. All those methods on the ServerBuilder struct could be directly implemented on the Server struct instead, and then instead of using `&mut self` everywhere use `Self`. This would mean those functions consume self and return a modified "new" self.
      Example:
      ```
      struct TlsConfig {}
      struct Server {
      host: String,
      port: usize,
      tls: Option,
      timeout: usize,
      hot_reload: bool,
      }
      impl Server {
      pub fn new(host: String, port: usize) -> Self {
      Server {
      host,
      port,
      tls: None,
      timeout: 2000,
      hot_reload: false,
      }
      }
      pub fn with_tls(mut self, config: TlsConfig) -> Self {
      self.tls = Some(config);
      self
      }
      pub fn with_hotreload(mut self, hot_reload: bool) -> Self {
      self.hot_reload = hot_reload;
      self
      }
      pub fn with_timeout(mut self, timeout: usize) -> Self {
      self.timeout = timeout;
      self
      }
      }
      pub fn test() {
      let server = Server::new("abc.de".to_owned(), 8080)
      .with_tls(TlsConfig {})
      .with_hotreload(true)
      .with_timeout(5000);
      }
      ```
      A little less boilerplate. I am not sure if this is more idiomatic or not or if it is even correct to call this "builder pattern", since at no point I am calling a specialized "build()" function.
      Anyway, whatever the pattern is called that I used here, I kinda like it. What do you guys and gals think?

    • @LordOfTheBing
      @LordOfTheBing 2 года назад +35

      @@DrIngo1980 your approach requires mutation, and provides no guarantee that it is only used at initiation. what happens if someone changes those config later on at runtime? Going through the builder also ensures by design that the server itself will never need to be mutable.

    • @DrIngo1980
      @DrIngo1980 2 года назад +30

      @@LordOfTheBing Thanks for your comment. I am still learning Rust, so every insight that helps me better understand the language and the patterns (idiomatic or not), is greatly appreciate.
      So, yeah, my approach requires mutation ("mut self"), but the ServerBuilder struct's functions also require mutation "&mut self", don't they?
      But yes, I can see now how my suggestion allows changes at any time, while the ServerBuilder approach only allows changes at "construction time" of the object.
      Thank you for pointing that out to me. 🙂

    • @----__---
      @----__--- 2 года назад +9

      @@DrIngo1980 this could counted as builder pattern too I guess. I actually use this in a few of my library crates where the struct only have a few and optional fields. Its better to use another builder struct to ensure the functions cant be called again once you "build". The other thing is that it does a lot of moving of the ownership, that basically means doing many memcpy and memset s. I am not sure if they would be optimized away in all cases but most likely they are, so thats good.

    • @pwalkleyuk
      @pwalkleyuk 2 года назад +2

      @@DrIngo1980 In java its usually referred to as a fluent API.

  • @ixirsii
    @ixirsii 2 года назад +22

    Great video! I'd love if you could talk about project and file structure for larger projects. As a Java developer I'm used to packages and every file being nested 3 folders deep, every type having its own file, and tests being separate from implementation. AFAIK Rust doesn't do this but I'm still not really sure how to structure my projects.

  • @andymooseful
    @andymooseful 2 года назад +6

    Thanks for another great video. After watching a lot of your tutorials, and reading the rust-lang docs I was able to port a Scala microservice to Rust, and gain huge memory footprint reductions. I’m still struggling with the memory model though but it’s early days!

    • @MrEnsiferum77
      @MrEnsiferum77 2 года назад

      Can't go more wrong than this. Porting scala to rust and u even don't know to write rust properly and on top of that u changing microservice. Stick with one language with all microservices.

  • @saaddahmani1870
    @saaddahmani1870 2 года назад +8

    Really nice, .. more idiomatic rust please.

  • @MrYevelnad
    @MrYevelnad 2 года назад +1

    Wow, i been a fun of oop and this builder pattern is love at first sight. I always question my self why we need implements in every language and this explains all. Thank you brian.

  • @bayoneta10
    @bayoneta10 2 года назад +6

    I think that "new" method should be implemented in ServerBuilder and as you did, "build" method returns the Server instance. Thank you a lot for the content.

    • @bradywb98
      @bradywb98 Год назад +2

      By convention, ::new is used to construct an instance of Self, and that convention is broken if ServerBuilder’s ::new returns a Server.

    • @bradywb98
      @bradywb98 Год назад +1

      However, in his implementation, Server’s ::new returns a ServerBuilder so I’m not sure.

    • @ultimatedude5686
      @ultimatedude5686 11 месяцев назад

      I would either implement Server::new in ServerBuilder or I would rename the method to something like Server::new_builder.

  • @donwinston
    @donwinston 7 месяцев назад

    These videos are excellent. Just the right length but effectively covers the current topic.

  • @yapayzeka
    @yapayzeka 2 года назад +5

    you deleted later but at 2:22 for the sake of the convention the alternative constructors should be named with_tls, and with_advanced. similar to Vec::new() and Vec::with_capacity()

  • @RodrigoStuchi
    @RodrigoStuchi 2 года назад +5

    absolutely awesome content 👏👏, more idiomatic Rust pls

  • @soberhippie
    @soberhippie 2 года назад +19

    Two questions: is it a good idea to call a method that returns a builder instead of an instance of Self, new? Wouldn't it be more expressive to call the method `builder`? Server::builder(),tls().build() looks more understandable to me. The other question is why not just use setters on a Server instance? Is it mutability or something else?

    • @Luxalpa
      @Luxalpa 2 года назад +4

      The reason for not using setters is because it's more convenient to chain the builders. With setters you would have a bunch of lines like server.setTLS(...); server.setTimeout(...); ...

    • @gvcallen
      @gvcallen 2 года назад +5

      I'd definitely rather have a non-associated method for the builder as "new" general implies that the type is it impl on is being returned

    • @penguin_brian
      @penguin_brian 2 года назад +8

      The feature of this approach is that it enforces that all values must be set before returning the object. At compile time. Where as the setters approach allows changing the values at any time. Which may or may not be appropriate. E.g. if the server is already running, you may not want to support changing the certificate. Setters could also have a runtime error, but I think a compile time error is better if this is possible.

  • @epiderpski
    @epiderpski 2 года назад +3

    These are really helpful. Please keep doing these idiomatic Rust vids.

  • @bjugdbjk
    @bjugdbjk 2 года назад

    Thank you so much for the Github link, That was only thing missing in this wonderful resourceful channel !!

  • @jonathanmoore5619
    @jonathanmoore5619 2 года назад +1

    As always, thks. Simple concept... Explained very well.

  • @jamesjenkins9480
    @jamesjenkins9480 2 года назад

    Oh nice. I'd not realized how this was done before. Great concise video!

  • @marcobuess
    @marcobuess 2 года назад +6

    Is there a specific reason to encapsulate the functions in ServerBuilder instead of Server directly?

    • @gvcallen
      @gvcallen 2 года назад +1

      Usually, the idea is that the Builder doesn't have all the information to represent the State fully/correctly, but the build method creates the State and changes types to ensure correctness. In this example it is trivial, since the optional values have reasonable default, but in more complex builders, various checks and configurations may need to take place in order to build the State

    • @marcobuess
      @marcobuess 2 года назад +2

      @@gvcallen Why even have the build method altogether? Why not just have the methods be more like a Server -> Server signature and mutate the field's as needed? So you could so something like Server::new().withPort(1234)...

    • @supersonictumbleweed
      @supersonictumbleweed 2 года назад +1

      @@marcobuess because if you wanted a field that is settable only once you'd have to have the methods throw after initiialization which is a bit unweildy.

    • @gvcallen
      @gvcallen 2 года назад +4

      @@marcobuess In this case the builder is acting like a constructor. This allows certain aspects of the Server to be "locked in", like the port number, which cannot be changed while the server is up and running. Another example I can think of is embedded - you can "build" a struct containing IO information (such as pin numbers) but you often want that IO to be in a locked, built state once configured. It also allows separation of concerns when mutating the Server - what if a function like with_home_page(...) allowed you to specify a home page for the server via the builder, but if no home page was provided then one had to be fetched from a database for example. Using the builder, this configuration can be done right before building the server (like a constructor with optional fields). With a server, this would need to be done every time on startup. Also, the code is neater with all the constructor impl in one place. There are other examples on the web of why the builder pattern (not rust specific) is good. Hope this helps

  • @gilatron1241
    @gilatron1241 2 года назад +1

    I really like this builder pattern

  • @nathanbanks2354
    @nathanbanks2354 Год назад

    I'm used to the builder pattern in other languages. I like it--tough to build, but easy to use. Then you mention the derive_builder macro (7:25), and I start to realize why Rust is so popular.

    • @stysner4580
      @stysner4580 Год назад +1

      Until you use it a bunch and realize that proc macros skyrocket your compile times... Ye be warned.

  • @a_maxed_out_handle_of_30_chars
    @a_maxed_out_handle_of_30_chars 10 месяцев назад +1

    thank you :)

  • @phenanrithe
    @phenanrithe Год назад +4

    *Overloading* function and *default parameters* are two functionalities that are dearly missed in Rust. It's sad that it takes years to make any decision when it's so easy to add, here and in similar cases, because it really impedes the language.

  • @dmdeemer
    @dmdeemer Год назад

    It seems like the builder pattern is meant to be combined with the type-state pattern. Like you have a "building" state that allows you to mutate the optional items, and then you call .build() to get the "finished" state. If you want to, you can add other states, like "connected" and "disconnected".
    Example (sorry for any typos or syntax errors):
    enum ServerState { Building, Finished };
    type ms = u32;
    struct Server {
    host: String,
    port: u16,
    tls: Option,
    hot_reload: bool,
    timeout: ms
    };
    impl Server {
    fn new(host: String, port: u16) -> Server { /* ... */ }
    }
    impl Server {
    fn tls(&mut self, tls: TLSCert) -> Self { self.tls = Some(tls); &mut self }
    fn hot_reload(&mut self, hot_reload: bool) -> Self { self.hot_reload = hot_reload; &mut self }
    fn timeout(&mut self, timeout: ms) -> Self { self.timeout = timeout; &mut self }
    fn build(self) -> Server { /* ... */ }
    }

  • @daque1960
    @daque1960 2 года назад +2

    Thanks again for all the great work. While doing Rustlings I came across a match statement containing binder patterns using @. Is this idiomatic?
    match (slice[0], slice[1], slice[2]) {
    (r @ 0..=255, g @ 0..=255, b @ 0..=255) => Ok(Color {
    red: r as u8,
    green: g as u8,
    blue: b as u8,
    }),
    _ => Err(IntoColorError::IntConversion),
    }

  • @rregid
    @rregid 2 года назад +7

    I'm trying to learn Rust as it looks nice and perfomant but addition of some quality of life things like default argumets would make it so much easier to learn and write

    • @DrIngo1980
      @DrIngo1980 2 года назад +1

      While I do love Rust, I agree with this. I'd love to have optional default values for arguments as well.

    • @redcrafterlppa303
      @redcrafterlppa303 2 года назад +1

      The problem with default values is, that they can make a function difficult to call quickly. Function overloading is the same in green. I think by not including both developers are pushed to use cleaner solutions like the builder pattern. I first started with Java which has constructor and function overloading and I used it often. But today for any more complex class I try to use some sort of building pattern instead of a public constructor.

    • @----__---
      @----__--- 2 года назад

      @@redcrafterlppa303 Optional arguments being not included in the language is not a design choice by the devs. It just needs to be implemented cleanly without breaking type inference and whatnot.

    • @DrIngo1980
      @DrIngo1980 2 года назад

      @@redcrafterlppa303 The problem I see when not having optional default values for function arguments is that you end up with a lot of `Option` kinda properties/member variables that you then have to check for Some/None later on. Which makes that part of the code a bit more convoluted. Well, you could have "some_arg.unwrap_or()" instead, sure. It's just that in my eyes it "looks" a bit "uglier" when reading code. That's really it for me.
      Also, I am not sure I understand your first argument here:
      > "The problem with default values is, that they can make a function difficult to call quickly."
      Having default values for function arguments makes calling a function difficult to do quickly? What do you mean by "calling a function quickly"? Could you elaborate, please? I'm pretty sure I am missing something and your argument will start to make sense to me once I understand what you mean by "call a function quickly".

    • @rregid
      @rregid 2 года назад +2

      @@redcrafterlppa303 perhaps you're right here, Im coming from c++/java/python background and there are some concepts in rust that from the starl look kinda alien. As for speed and safety - I guess a bit more complexity while coding is nessecery tradeoff for speed and safety and I'm not yet used to the style)

  • @bradywb98
    @bradywb98 Год назад

    How is ServerBuilder’s build method able to initialize a Server like that, given that Server’s fields are not public?

  • @flippert0
    @flippert0 9 месяцев назад

    As far I understood the docs about "derive_builder", tuple structs and structs with generics are not supported? That leaves this crate for simple data objects.

  • @joelgodfrey-smith3655
    @joelgodfrey-smith3655 2 года назад

    Great video, as always!

  • @mod7ex_fx
    @mod7ex_fx Год назад

    great video thanks

  • @itsdazing1279
    @itsdazing1279 2 года назад +2

    Nice video 👍

  • @ksnyou
    @ksnyou 2 года назад

    Nice! Next spring for rust :)

  • @joelmontesdeoca6572
    @joelmontesdeoca6572 2 года назад

    This one was great!

  • @GlobalYoung7
    @GlobalYoung7 2 года назад

    thank you 😊

  • @strangnet
    @strangnet 2 года назад +1

    Is it really idiomatic to not return Self for a new function for the Server struct? I would have to know about the ServerBuilder implementation and its build function to be able to use the Server struct.

    • @----__---
      @----__--- 2 года назад +1

      To be able to use Server struct you need to know about its implementation too?
      Thats basically builder pattern.

    • @strangnet
      @strangnet 2 года назад +1

      @@----__--- sure, but the semantics of returning a ServerBuilder on Server::new() is convoluted. Why not just work with ServerBuilder from the start?

    • @LordOfTheBing
      @LordOfTheBing 2 года назад +2

      @@strangnet you can start from the builder, and that is often done, but isn't deemed very user-friendly. people want the Server, so having the api designed such that the object of interest already guides them to the proper pattern without having to dig into documentation to know what extra hoops to jump through is much more helpful.

  • @dimitrobest5293
    @dimitrobest5293 2 года назад

    awesome

  • @user-wv1in4pz2w
    @user-wv1in4pz2w 2 года назад +1

    wouldn't it be better to have the build method take ownership of the builder functions take ownership of self instead of introducing a layer of indirection with &mut? And the .build() could take an immutable reference, if you want to reuse the builder.

  • @NareionSD
    @NareionSD Год назад

    This would be an ok demostration of the builder pattern if you hadn't changed the Server::new function. You not only overrode an idiomatic function from another video, but also made a function which name lies to you (you would expect a "new" Server to be a Server, not a builder). You could have easily just made a Server::builder() function and either left Server::new as is (having then a factory method as an option) or erased it completely.

  • @Xeros08
    @Xeros08 2 года назад

    Question about Trait objects in Rust.
    In the Vec, is the Box really needed?
    Asking because on your Java OOP vs Rust Trait Objects, you showed the example of a Vec and that cunfused me a bit.
    Anyways, great videos, really liking the Idiomatic Rust series :D

    • @agfd5659
      @agfd5659 2 года назад

      Yes, not necessarily Box, but some pointer is needed because the inner type's size is not known, whereas a pointer's size is known. dyn Animal is not Sized, i.e. its size is not known at compile time.

  • @phenanrithe
    @phenanrithe Год назад

    The builder/fluent pattern is a nice way to create objects, but in Rust it suffers from life-of-time analysis constraints that goes in the way. It's not possible to interrupt the flow and continue it, for example in an if/else construct, which is a frequent case, because the lifetime of the reference dies immediately when the build() is not included. Unfortunately I haven't found a satisfactory solution yet (moving the value generates a lot of other problems). To cope with that, weird shadowing techniques must be used, but it's once more a problem for code clarity.

  • @RoamingAdhocrat
    @RoamingAdhocrat Год назад

    I don't love `unwrap_or_default` - imo `unwrap_or(false)` would be clearer that the param defaults to false

  • @Jordan4Ibanez
    @Jordan4Ibanez 2 года назад

    I love your videos. Have you thought about uploading to odysee as well? You can automatically upload there without thinking about it once you set it up

  • @felixst-gelais6722
    @felixst-gelais6722 2 года назад

    What would happen if the fields on Server were private? Because then ServerBuilder couldnt just call the constructor on Server

    • @LordOfTheBing
      @LordOfTheBing 2 года назад +3

      the fields on Server already are private in the example. the ServerBuilder can create the Server without going through extra custom constructors because they're in the same module.

    • @bezimienny5
      @bezimienny5 2 года назад +1

      @@LordOfTheBing and thats the beauty of rust for you

  • @davidvernon3119
    @davidvernon3119 2 года назад

    I am very new to rust, but this seems like a lot of hocus-pocus just to make rust seem dynamic when it (intentionally) isn’t.

  • @stefankyriacou7151
    @stefankyriacou7151 2 года назад

    Why did the build method need a mutable reference to self, could it not have been immutable? It doesn't seem to be modifying any of self's attributes?

  • @PokeNebula
    @PokeNebula 2 года назад

    Why not make it so that server only had the advanced constructor, but just make it take Option values for each of the optional parameters, then inside the constructor, unwrap or default each of the optional values?
    Instead of the programmer using another struct, the programmer just has to either wrap some paramaters in Some(), or just say None a few times.

  • @cjt9150
    @cjt9150 2 года назад

    I found that: something about extension traits
    FileName: sample\src\lib.rs
    ----------------------------------------------
    pub trait Core {
    fn core(&self) -> String;
    }
    FileName: sample\src\main.rs
    --------------------------------------------------
    use sample::Core;
    trait Derived: Core {
    fn derived(&self);
    }
    pub struct Sample {
    pub name: String,
    }
    impl Core for Sample {
    fn core(&self) -> String {
    "core".to_owned()
    }
    }
    impl Derived for Sample {
    fn derived(&self) {
    println!("{} & derived", self.core());
    }
    }
    fn main() {
    let s = Sample { name: "name".to_owned() };
    s.derived();
    }

  • @donwinston
    @donwinston 7 месяцев назад

    Who decided the standard style for writing Rust code for functions is xyzzy_abcd() and not xyzAbc()? Underscore characters in names is ugly.

  • @ErikBongers
    @ErikBongers 2 года назад +2

    I don't understand why this would be better than overloaded functions. So much boilerplate. The fact that there's a macro for this, kind of proves the point.

    • @saadisave
      @saadisave 2 года назад

      Better than overloaded functions because:
      1. a single signature for each function makes it far easier for the compiler
      2. no complicated algorithms required to determine which function to call
      3. Easier for dynamic libraries
      4. Rust is not Java

    • @----__---
      @----__--- 2 года назад +1

      Do you really think the existence of every convenience third party library proves that the feature needs to be embedded in the language. Rust's type system is powerful enough that basically renders the overloading unnecessary. It is also confusing, in docs and whatnot, which makes not including it favourable against ergonomics. We need named arguments and defaults though but those are hard to implement without breaking type inference and not bloat the language like in c++'s case.

    • @G11713
      @G11713 2 года назад

      Great point. I find Rust verbose though the more I learn the less annoying it feels and the presence of the macro system does express a systematic way to eliminate verbosity. Ultimately, we may get macros of the form:
      sql! { some SQL syntax }
      haskell! { some Haskell syntax }
      fsharp! { some F# syntax }
      dented_rust! { indent sensitive rust syntax ... get it, dented mental }
      It is a brave new world... perhaps.

    • @ErikBongers
      @ErikBongers 2 года назад

      Have thought about this a little more, and as often with examples, this Builder pattern is a bit overkill in this case. But the pattern itself is quite useful. As someone pointed out here, it's mainly the lack of default values that is a pain. The lack of overloading is less of an issue as overloading hides what is happening while functions with names like new_this() and new_that() improve readability, as it's clear what's happening.

  • @redcrafterlppa303
    @redcrafterlppa303 2 года назад

    I just realized how waste the gui api world is in rust. Shure some exist, but they are either very high level designed or very barebones. I decided that I will create my own api for guis for Windows. Maybe a cross platform version will follow but don't quote me on that.

  • @aftalavera
    @aftalavera Год назад

    To be even more idiomatic and realistic, dump Rust!

  • @cjt9150
    @cjt9150 2 года назад

    Doubt: what is extension traits? docs.rs/rand/0.8.5/rand/trait.Rng.html, it says "trait Rng is automatically-implemented extension trait on RngCore", can we extend trait?

    • @proloycodes
      @proloycodes 2 года назад

      basically a trait that extends another trait's functionality in specific types, or in this case all types that implement the original trait. a bit like subclassed in OO languages.