docs(patterns): Add concrete examples for newtype and struct decomposition design patterns (#368)

pull/369/head
Owen Leung 12 months ago committed by GitHub
parent 3a9e5de41c
commit 70c6ca4c86
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -17,42 +17,31 @@ This creates a new type, rather than an alias to a type (`type` items).
## Example
```rust,ignore
// Some type, not necessarily in the same module or even crate.
struct Foo {
//..
}
impl Foo {
// These functions are not present on Bar.
//..
}
// The newtype.
pub struct Bar(Foo);
```rust
use std::fmt::Display;
impl Bar {
// Constructor.
pub fn new(
//..
) -> Self {
//..
// Create Newtype Password to override the Display trait for String
struct Password(String);
impl Display for Password {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "****************")
}
//..
}
fn main() {
let b = Bar::new(...);
// Foo and Bar are type incompatible, the following do not type check.
// let f: Foo = b;
// let b: Bar = Foo { ... };
let unsecured_password: String = "ThisIsMyPassword".to_string();
let secured_password: Password = Password(unsecured_password.clone());
println!("unsecured_password: {unsecured_password}");
println!("secured_password: {secured_password}");
}
```
```shell
unsecured_password: ThisIsMyPassword
secured_password: ****************
```
## Motivation
The primary motivation for newtypes is abstraction. It allows you to share
@ -108,4 +97,4 @@ with `Bar`.
- [Type aliases](https://doc.rust-lang.org/stable/book/ch19-04-advanced-types.html#creating-type-synonyms-with-type-aliases)
- [derive_more](https://crates.io/crates/derive_more), a crate for deriving many
builtin traits on newtypes.
- [The Newtype Pattern In Rust](https://www.worthe-it.co.za/blog/2020-10-31-newtype-pattern-in-rust.html)
- [The Newtype Pattern In Rust](https://web.archive.org/web/20230519162111/https://www.worthe-it.co.za/blog/2020-10-31-newtype-pattern-in-rust.html)

@ -1,6 +1,4 @@
# Compose structs together for better borrowing
TODO - this is not a very snappy name
# Struct decomposition for independent borrowing
## Description
@ -20,71 +18,93 @@ Here is a contrived example of where the borrow checker foils us in our plan to
use a struct:
```rust
struct A {
f1: u32,
f2: u32,
f3: u32,
struct Database {
connection_string: String,
timeout: u32,
pool_size: u32,
}
fn foo(a: &mut A) -> &u32 { &a.f2 }
fn bar(a: &mut A) -> u32 { a.f1 + a.f3 }
fn print_database(database: &Database) {
println!("Connection string: {}", database.connection_string);
println!("Timeout: {}", database.timeout);
println!("Pool size: {}", database.pool_size);
}
fn baz(a: &mut A) {
// The later usage of x causes a to be borrowed for the rest of the function.
let x = foo(a);
// Borrow checker error:
// let y = bar(a); // ~ ERROR: cannot borrow `*a` as mutable more than once
// at a time
println!("{}", x);
fn main() {
let mut db = Database {
connection_string: "initial string".to_string(),
timeout: 30,
pool_size: 100,
};
let connection_string = &mut db.connection_string;
print_database(&db); // Immutable borrow of `db` happens here
// *connection_string = "new string".to_string(); // Mutable borrow is used
// here
}
```
We can apply this design pattern and refactor `A` into two smaller structs, thus
solving the borrow checking issue:
We can apply this design pattern and refactor `Database` into three smaller
structs, thus solving the borrow checking issue:
```rust
// A is now composed of two structs - B and C.
struct A {
b: B,
c: C,
}
struct B {
f2: u32,
}
struct C {
f1: u32,
f3: u32,
// Database is now composed of three structs - ConnectionString, Timeout and PoolSize.
// Let's decompose it into smaller structs
#[derive(Debug, Clone)]
struct ConnectionString(String);
#[derive(Debug, Clone, Copy)]
struct Timeout(u32);
#[derive(Debug, Clone, Copy)]
struct PoolSize(u32);
// We then compose these smaller structs back into `Database`
struct Database {
connection_string: ConnectionString,
timeout: Timeout,
pool_size: PoolSize,
}
// These functions take a B or C, rather than A.
fn foo(b: &mut B) -> &u32 { &b.f2 }
fn bar(c: &mut C) -> u32 { c.f1 + c.f3 }
// print_database can then take ConnectionString, Timeout and Poolsize struct instead
fn print_database(connection_str: ConnectionString,
timeout: Timeout,
pool_size: PoolSize) {
println!("Connection string: {:?}", connection_str);
println!("Timeout: {:?}", timeout);
println!("Pool size: {:?}", pool_size);
}
fn baz(a: &mut A) {
let x = foo(&mut a.b);
// Now it's OK!
let y = bar(&mut a.c);
println!("{}", x);
fn main() {
// Initialize the Database with the three structs
let mut db = Database {
connection_string: ConnectionString("localhost".to_string()),
timeout: Timeout(30),
pool_size: PoolSize(100),
};
let connection_string = &mut db.connection_string;
print_database(connection_string.clone(), db.timeout, db.pool_size);
*connection_string = ConnectionString("new string".to_string());
}
```
## Motivation
TODO Why and where you should use the pattern
This pattern is most useful, when you have a struct that ended up with a lot of
fields that you want to borrow independently. Thus having a more flexible
behaviour in the end.
## Advantages
Lets you work around limitations in the borrow checker.
Often produces a better design.
Decomposition of structs lets you work around limitations in the borrow checker.
And it often produces a better design.
## Disadvantages
Leads to more verbose code.
Sometimes, the smaller structs are not good abstractions, and so we end up with
a worse design. That is probably a 'code smell', indicating that the program
should be refactored in some way.
It can lead to more verbose code. And sometimes, the smaller structs are not
good abstractions, and so we end up with a worse design. That is probably a
'code smell', indicating that the program should be refactored in some way.
## Discussion

Loading…
Cancel
Save