Skip to content

Commit 70c6ca4

Browse files
docs(patterns): Add concrete examples for newtype and struct decomposition design patterns (#368)
1 parent 3a9e5de commit 70c6ca4

File tree

2 files changed

+83
-74
lines changed

2 files changed

+83
-74
lines changed

src/patterns/behavioural/newtype.md

Lines changed: 17 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -17,42 +17,31 @@ This creates a new type, rather than an alias to a type (`type` items).
1717

1818
## Example
1919

20-
```rust,ignore
21-
// Some type, not necessarily in the same module or even crate.
22-
struct Foo {
23-
//..
24-
}
25-
26-
impl Foo {
27-
// These functions are not present on Bar.
28-
//..
29-
}
30-
31-
// The newtype.
32-
pub struct Bar(Foo);
20+
```rust
21+
use std::fmt::Display;
3322

34-
impl Bar {
35-
// Constructor.
36-
pub fn new(
37-
//..
38-
) -> Self {
39-
40-
//..
23+
// Create Newtype Password to override the Display trait for String
24+
struct Password(String);
4125

26+
impl Display for Password {
27+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28+
write!(f, "****************")
4229
}
43-
44-
//..
4530
}
4631

4732
fn main() {
48-
let b = Bar::new(...);
49-
50-
// Foo and Bar are type incompatible, the following do not type check.
51-
// let f: Foo = b;
52-
// let b: Bar = Foo { ... };
33+
let unsecured_password: String = "ThisIsMyPassword".to_string();
34+
let secured_password: Password = Password(unsecured_password.clone());
35+
println!("unsecured_password: {unsecured_password}");
36+
println!("secured_password: {secured_password}");
5337
}
5438
```
5539

40+
```shell
41+
unsecured_password: ThisIsMyPassword
42+
secured_password: ****************
43+
```
44+
5645
## Motivation
5746

5847
The primary motivation for newtypes is abstraction. It allows you to share
@@ -108,4 +97,4 @@ with `Bar`.
10897
- [Type aliases](https://doc.rust-lang.org/stable/book/ch19-04-advanced-types.html#creating-type-synonyms-with-type-aliases)
10998
- [derive_more](https://crates.io/crates/derive_more), a crate for deriving many
11099
builtin traits on newtypes.
111-
- [The Newtype Pattern In Rust](https://www.worthe-it.co.za/blog/2020-10-31-newtype-pattern-in-rust.html)
100+
- [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)

src/patterns/structural/compose-structs.md

Lines changed: 66 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
# Compose structs together for better borrowing
2-
3-
TODO - this is not a very snappy name
1+
# Struct decomposition for independent borrowing
42

53
## Description
64

@@ -20,71 +18,93 @@ Here is a contrived example of where the borrow checker foils us in our plan to
2018
use a struct:
2119

2220
```rust
23-
struct A {
24-
f1: u32,
25-
f2: u32,
26-
f3: u32,
21+
struct Database {
22+
connection_string: String,
23+
timeout: u32,
24+
pool_size: u32,
2725
}
2826

29-
fn foo(a: &mut A) -> &u32 { &a.f2 }
30-
fn bar(a: &mut A) -> u32 { a.f1 + a.f3 }
27+
fn print_database(database: &Database) {
28+
println!("Connection string: {}", database.connection_string);
29+
println!("Timeout: {}", database.timeout);
30+
println!("Pool size: {}", database.pool_size);
31+
}
3132

32-
fn baz(a: &mut A) {
33-
// The later usage of x causes a to be borrowed for the rest of the function.
34-
let x = foo(a);
35-
// Borrow checker error:
36-
// let y = bar(a); // ~ ERROR: cannot borrow `*a` as mutable more than once
37-
// at a time
38-
println!("{}", x);
33+
fn main() {
34+
let mut db = Database {
35+
connection_string: "initial string".to_string(),
36+
timeout: 30,
37+
pool_size: 100,
38+
};
39+
40+
let connection_string = &mut db.connection_string;
41+
print_database(&db); // Immutable borrow of `db` happens here
42+
// *connection_string = "new string".to_string(); // Mutable borrow is used
43+
// here
3944
}
4045
```
4146

42-
We can apply this design pattern and refactor `A` into two smaller structs, thus
43-
solving the borrow checking issue:
47+
We can apply this design pattern and refactor `Database` into three smaller
48+
structs, thus solving the borrow checking issue:
4449

4550
```rust
46-
// A is now composed of two structs - B and C.
47-
struct A {
48-
b: B,
49-
c: C,
50-
}
51-
struct B {
52-
f2: u32,
53-
}
54-
struct C {
55-
f1: u32,
56-
f3: u32,
51+
// Database is now composed of three structs - ConnectionString, Timeout and PoolSize.
52+
// Let's decompose it into smaller structs
53+
#[derive(Debug, Clone)]
54+
struct ConnectionString(String);
55+
56+
#[derive(Debug, Clone, Copy)]
57+
struct Timeout(u32);
58+
59+
#[derive(Debug, Clone, Copy)]
60+
struct PoolSize(u32);
61+
62+
// We then compose these smaller structs back into `Database`
63+
struct Database {
64+
connection_string: ConnectionString,
65+
timeout: Timeout,
66+
pool_size: PoolSize,
5767
}
5868

59-
// These functions take a B or C, rather than A.
60-
fn foo(b: &mut B) -> &u32 { &b.f2 }
61-
fn bar(c: &mut C) -> u32 { c.f1 + c.f3 }
69+
// print_database can then take ConnectionString, Timeout and Poolsize struct instead
70+
fn print_database(connection_str: ConnectionString,
71+
timeout: Timeout,
72+
pool_size: PoolSize) {
73+
println!("Connection string: {:?}", connection_str);
74+
println!("Timeout: {:?}", timeout);
75+
println!("Pool size: {:?}", pool_size);
76+
}
6277

63-
fn baz(a: &mut A) {
64-
let x = foo(&mut a.b);
65-
// Now it's OK!
66-
let y = bar(&mut a.c);
67-
println!("{}", x);
78+
fn main() {
79+
// Initialize the Database with the three structs
80+
let mut db = Database {
81+
connection_string: ConnectionString("localhost".to_string()),
82+
timeout: Timeout(30),
83+
pool_size: PoolSize(100),
84+
};
85+
86+
let connection_string = &mut db.connection_string;
87+
print_database(connection_string.clone(), db.timeout, db.pool_size);
88+
*connection_string = ConnectionString("new string".to_string());
6889
}
6990
```
7091

7192
## Motivation
7293

73-
TODO Why and where you should use the pattern
94+
This pattern is most useful, when you have a struct that ended up with a lot of
95+
fields that you want to borrow independently. Thus having a more flexible
96+
behaviour in the end.
7497

7598
## Advantages
7699

77-
Lets you work around limitations in the borrow checker.
78-
79-
Often produces a better design.
100+
Decomposition of structs lets you work around limitations in the borrow checker.
101+
And it often produces a better design.
80102

81103
## Disadvantages
82104

83-
Leads to more verbose code.
84-
85-
Sometimes, the smaller structs are not good abstractions, and so we end up with
86-
a worse design. That is probably a 'code smell', indicating that the program
87-
should be refactored in some way.
105+
It can lead to more verbose code. And sometimes, the smaller structs are not
106+
good abstractions, and so we end up with a worse design. That is probably a
107+
'code smell', indicating that the program should be refactored in some way.
88108

89109
## Discussion
90110

0 commit comments

Comments
 (0)