Factory and Builder in Rust: Validated Construction¶
A factory controls how a value is created; a builder collects construction choices before producing it. In Rust, an associated function and a small builder often solve this without an inheritance hierarchy. The important boundary is that every public creation path validates the same invariants.
Rust Factory and Builder requirements¶
Read structs and associated functions, Result, and typestate. These fixtures use stable Rust edition 2024 and C++20 with no dependencies or hardware access.
Construct a configuration with an inclusive integer range and a sampling period in milliseconds. Both bounds are required; lower <= upper, including equal bounds. Zero and negative bounds are valid. The period must be 1–60000; omission selects 1000, while an explicit zero is an error. Repeated builder setters use the last supplied value.
Error precedence is MissingLower, MissingUpper, Bounds, then Interval. A builder is allowed to hold incomplete or invalid choices; only the finished Config claims validity. This fixture configures no real sampling device. Both languages print:
C++: private constructor, checked factory and builder¶
Save this complete program as main.cpp. Config's static factory returns a C++20 variant of a valid value or an error. Builder delegates final value validation to that factory instead of duplicating it.
Compile with assertions enabled:
C++ access control keeps the unchecked constructor private, while a static member function does not require an existing Config. No public default constructor or mutating setter bypasses validation. Copying this scalar configuration preserves its valid values.
The C++ builder setters return references to the builder. Chaining on a temporary is safe within the build expression above; retaining that returned reference after the temporary dies would not be safe. This fixture uses std::get only when its known inputs specify the selected alternative. A real input handler must check or visit the variant instead of assuming success.
Rust: one validator behind all public construction¶
Run cargo new construction --edition 2024 and replace src/main.rs with the entire source. Config has no Default implementation. The builder's default represents choices not yet supplied, not a finished valid configuration.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 | |
In the project:
NonZero excludes zero but does not enforce our upper limit or ordered bounds. Private fields prevent callers outside the module from bypassing the public validator. Code inside the module still bears responsibility for preserving invariants; privacy is not a proof that every internal constructor is correct.
The consuming Rust builder returns its updated value, not a reference to a temporary. This API discards the builder after build, including a failed build. If callers must edit and retry the same choices, a borrowed build operation or an error that retains the builder is a different contract. Choosing a builder receiver is an ownership decision, not a syntax preference.
Factory names are not a required hierarchy¶
This is a checked factory function, not a complete Abstract Factory or subclass-based Factory Method framework. Such patterns solve additional requirements, such as choosing an open product family or coordinating related products. If those requirements arise, a trait, generic factory or runtime interface can supply creation behaviour; see traits and dispatch.
When converting an existing raw configuration into a validated one, TryFrom is another suitable Rust boundary for a fallible conversion. It does not replace the validation logic. Avoid maintaining a second validator merely because conversion and direct construction have different names.
For three simple mandatory arguments, the direct factory may be clearer than a builder. Named setters help when options grow, call-site ambiguity matters or construction is assembled across steps. A builder is not automatically necessary because one exists in the C++ design.
Changed requirement: new input paths and cross-field rules¶
If a file, CLI or deserialiser becomes a new input source, treat its raw values as unvalidated choices and call the same constructor. Successfully parsing an integer does not establish its domain validity. A deserialisation shortcut or new public setter must not silently bypass ordered bounds and period limits.
If the configuration becomes a real device operation, separate pure value validation from acquiring resources. Valid configuration does not guarantee access permissions, connectivity or successful startup. Constructing the value here performs no I/O and is not a readiness test.
If required fields must be supplied at compile time, a typestate builder can expose build only when its type records those fields as present. It still needs runtime validation of period limits and relationships between runtime bounds. More types do not remove the domain checks.
Intentional failures: bypassing construction boundaries¶
Compile this independent Rust program; the private field is not externally constructible:
Use the full program's Config::try_new or Builder::build and handle Result instead of exposing fields to silence the compiler. The miniature example isolates privacy; the complete value type above also enforces the cross-field rules.
This C++ program independently fails because its constructor is private:
Repair through the checked factory in the complete C++ program. Neither making the constructor public nor assigning a default zero interval meets the requirement.
Selection criteria and exercises¶
| Construction requirement | Candidate | Remaining obligation |
|---|---|---|
| Few required inputs | Checked associated/static factory | Validate every input and relationship |
| Incrementally assembled choices | Builder | Distinguish omission, explicit zero and defaults |
| Existing raw value becomes validated | TryFrom / conversion factory | Delegate to the same validator |
| Compile-time presence of required choices | Typestate builder | Still check runtime values and cross-field rules |
| Open family of created products | Trait/generic/runtime factory interface | Specify ownership, creation failure and family compatibility |
- Add a raw-input conversion and test that it rejects the same zero period and reversed bounds as the direct factory.
- Remove the upper-period check and retain the 60001 test. The resulting valid-looking value must fail the contract test.
- Supply a lower bound of zero, omit the upper bound, and set an invalid period. Confirm the documented MissingUpper precedence.
- Explain why a Default implementation that manufactures a zero-period Config would undermine this API, even though Builder::default is useful.
- Decide whether a failed build consumes or retains choices before adding a retry API. Test the state callers can observe after that failure.
Verification and next step¶
The extracted programs were checked on Raspberry Pi 4B with rustc/cargo 1.99.0, GCC 14.2.0 and kernel 6.18.50+rpt-rpi-v8. Rust passed five tests in debug and release, exact-source formatting and both output checks. The raw-input conversion also passed both profiles; reuse after consuming build was rejected with E0382, and external private-field construction with E0451. Removing the upper-period check compiled but failed the retained boundary test. C++20 passed output and assertions at -O0 and -O2, with assertions enabled; its private-constructor bypass failed compilation. No device-readiness, allocation or performance claim follows from these fixtures.
Next: Visitor, enums and open behaviour, the trade-off between adding data variants and adding operations.