Rust型付きBuilderパターン実戦:コンパイル時安全オブジェクト構築の5つのコアパターン
Rustの世界にはnullもデフォルトコンストラクタもオプションパラメータもありません—複雑なオブジェクトを構築するには、画面いっぱいのFoo { a: 1, b: 2, ... }を書くか、Builderパターンを使うしかありません。しかし従来のBuilderには致命的な問題があります:.name("xxx")の呼び出しを忘れた?コンパイルエラーにならず、実行時にpanic。 2026年、Rustの型システムがより良い答えをくれます—型状態(Type State)でコンパイル時に必須フィールドの欠落を保証し、typed-builder crateでボイラープレートを排除し、ジェネリックBuilderで条件付き必須を実現します。今日は、手動Builderから型状態、typed-builderからプロダクション級コンポジションまで、Rust型付きBuilderの5つのコアパターンを徹底解説します。
コア概念一覧
| 概念 | 説明 | 主要技術 |
|---|---|---|
| Builderパターン | 複雑なオブジェクトの段階的構築 | struct FooBuilder + build() |
| 型状態 | ジェネリックパラメータでフィールドの設定状態を追跡 | Builder<Missing, Missing> |
| typed-builder | 型安全Builderを自動派生するcrate | #[derive(TypedBuilder)] |
| ジェネリックBuilder | ジェネリックパラメータで条件付き必須フィールドを実現 | Builder<NameSet = Set> |
| 検証Builder | build時にビジネスルール検証を実行 | fn build() -> Result<Foo, Error> |
問題分析:5つのペインポイント
- 必須フィールドの欠落:従来のBuilderの
build()はOptionを返すかpanic—必須フィールドの未設定を実行時まで発見できない - ボイラープレート爆発:10個のフィールドで10個のsetter、10個のOptionフィールド、1つの巨大な
build()関数 - 条件付き必須の表現不可:フィールドAが設定されたらBも必須、Aが未設定ならBはオプション—このロジックを従来のBuilderはコンパイル時に表現できない
- 構築順序の制約:一部のフィールドは他より先に設定する必要がある—従来のBuilderは順序を強制できない
- 検証ロジックの分散:フィールド検証が各setterに散在し、build時にもう一度検証—見落としやすい
パターン1:手動Builderと型状態パターン
ジェネリックパラメータで各フィールドの状態を追跡し、コンパイル時に必須フィールドの欠落を防止。
use std::marker::PhantomData;
/// フィールド未設定
pub struct Missing;
/// フィールド設定済み
pub struct Set<T>(PhantomData<T>);
#[derive(Debug, Clone)]
pub struct ServerConfig {
pub host: String,
pub port: u16,
pub max_connections: u32,
pub timeout_secs: u64,
pub enable_tls: bool,
pub tls_cert_path: Option<String>,
}
pub struct ServerConfigBuilder<HostSet, PortSet> {
host: Option<String>,
port: Option<u16>,
max_connections: u32,
timeout_secs: u64,
enable_tls: bool,
tls_cert_path: Option<String>,
_host: PhantomData<HostSet>,
_port: PhantomData<PortSet>,
}
impl ServerConfigBuilder<Missing, Missing> {
pub fn new() -> Self {
Self {
host: None, port: None, max_connections: 1000,
timeout_secs: 30, enable_tls: false, tls_cert_path: None,
_host: PhantomData, _port: PhantomData,
}
}
}
impl<PortSet> ServerConfigBuilder<Missing, PortSet> {
pub fn host(self, host: impl Into<String>) -> ServerConfigBuilder<Set<String>, PortSet> {
ServerConfigBuilder {
host: Some(host.into()), port: self.port,
max_connections: self.max_connections, timeout_secs: self.timeout_secs,
enable_tls: self.enable_tls, tls_cert_path: self.tls_cert_path,
_host: PhantomData, _port: self._port,
}
}
}
impl<HostSet> ServerConfigBuilder<HostSet, Missing> {
pub fn port(self, port: u16) -> ServerConfigBuilder<HostSet, Set<u16>> {
ServerConfigBuilder {
host: self.host, port: Some(port),
max_connections: self.max_connections, timeout_secs: self.timeout_secs,
enable_tls: self.enable_tls, tls_cert_path: self.tls_cert_path,
_host: self._host, _port: PhantomData,
}
}
}
impl<HostSet, PortSet> ServerConfigBuilder<HostSet, PortSet> {
pub fn max_connections(mut self, max: u32) -> Self { self.max_connections = max; self }
pub fn timeout_secs(mut self, secs: u64) -> Self { self.timeout_secs = secs; self }
pub fn enable_tls(mut self, enable: bool) -> Self { self.enable_tls = enable; self }
pub fn tls_cert_path(mut self, path: impl Into<String>) -> Self { self.tls_cert_path = Some(path.into()); self }
}
impl ServerConfigBuilder<Set<String>, Set<u16>> {
pub fn build(self) -> ServerConfig {
ServerConfig {
host: self.host.unwrap(), port: self.port.unwrap(),
max_connections: self.max_connections, timeout_secs: self.timeout_secs,
enable_tls: self.enable_tls, tls_cert_path: self.tls_cert_path,
}
}
}
fn main() {
// ✅ コンパイル成功:全必須フィールドが設定済み
let config = ServerConfigBuilder::new()
.host("0.0.0.0")
.port(8080)
.max_connections(5000)
.timeout_secs(60)
.enable_tls(true)
.tls_cert_path("/etc/ssl/cert.pem")
.build();
println!("サーバー設定: {:?}", config);
// ❌ コンパイルエラー:portフィールドが欠落
// ServerConfigBuilder::new()
// .host("0.0.0.0")
// .build();
}
重要ポイント:
MissingとSet<T>はゼロサイズ型(ZST)、実行時オーバーヘッドなしbuild()はSet<String>, Set<u16>状態でのみ利用可能、コンパイル時に必須フィールドを保証- オプションフィールドはどの状態でも設定可能、型状態を変更しない
- フィールド設定順序は問わない—host→portでもport→hostでもOK
パターン2:typed-builder crate自動派生
型状態Builderを手動で書くのは煩雑?typed-builder crateが1行のマクロで解決。
use typed_builder::TypedBuilder;
use std::time::Duration;
#[derive(Debug, Clone, TypedBuilder)]
pub struct DatabaseConfig {
#[builder(default_code = r#""localhost".into()"#)]
pub host: String,
#[builder(default = 5432)]
pub port: u16,
pub database: String,
pub username: String,
pub password: String,
#[builder(default = 100)]
pub max_connections: u32,
#[builder(default = 30)]
pub timeout_secs: u64,
#[builder(default = false)]
pub enable_ssl: bool,
}
#[derive(Debug, Clone, TypedBuilder)]
pub struct HttpClientConfig {
pub base_url: String,
#[builder(default = Duration::from_secs(30), setter(into))]
pub timeout: Duration,
#[builder(default = 3)]
pub max_retries: u32,
#[builder(default, setter(transform = |pairs: Vec<(&str, &str)>| {
let mut map = reqwest::header::HeaderMap::new();
for (k, v) in pairs {
if let (Ok(name), Ok(val)) = (
reqwest::header::HeaderName::from_bytes(k.as_bytes()),
reqwest::header::HeaderValue::from_str(v),
) {
map.insert(name, val);
}
}
map
}))]
pub headers: reqwest::header::HeaderMap,
}
fn main() {
let db_config = DatabaseConfig::builder()
.database("toolsku_prod")
.username("admin")
.password("secret123")
.max_connections(200)
.build();
println!("データベース設定: {:?}", db_config);
let http_config = HttpClientConfig::builder()
.base_url("https://api.toolsku.com")
.timeout(Duration::from_secs(60))
.headers(vec![("Authorization", "Bearer token123")])
.build();
println!("HTTPクライアント設定: {:?}", http_config);
}
重要ポイント:
#[builder(default = ...)]でオプションフィールドをマーク、必須フィールドにはdefaultなし#[builder(setter(into))]でsetterがimpl Into<T>を受け付け、より柔軟に#[builder(setter(transform = ...))]でsetterロジックをカスタマイズ#[builder(default_code = ...)]でコード式をデフォルト値として使用
パターン3:ジェネリックBuilderと条件付き必須フィールド
一部のフィールドは「条件付き必須」—Aが設定されたらBも必須、そうでなければBはオプション。ジェネリックパラメータで正確に表現。
use std::marker::PhantomData;
pub struct TlsEnabled;
pub struct TlsDisabled;
#[derive(Debug)]
pub struct SecureServerConfig {
pub host: String,
pub port: u16,
pub enable_tls: bool,
pub tls_cert: Option<String>,
pub tls_key: Option<String>,
}
pub struct SecureServerBuilder<TlsState> {
host: Option<String>,
port: Option<u16>,
enable_tls: bool,
tls_cert: Option<String>,
tls_key: Option<String>,
_tls: PhantomData<TlsState>,
}
impl SecureServerBuilder<TlsDisabled> {
pub fn new() -> Self {
Self {
host: None, port: None, enable_tls: false,
tls_cert: None, tls_key: None, _tls: PhantomData,
}
}
pub fn enable_tls(self) -> SecureServerBuilder<TlsEnabled> {
SecureServerBuilder {
host: self.host, port: self.port, enable_tls: true,
tls_cert: self.tls_cert, tls_key: self.tls_key, _tls: PhantomData,
}
}
}
impl SecureServerBuilder<TlsEnabled> {
pub fn tls_cert(mut self, cert: impl Into<String>) -> Self { self.tls_cert = Some(cert.into()); self }
pub fn tls_key(mut self, key: impl Into<String>) -> Self { self.tls_key = Some(key.into()); self }
}
impl<TlsState> SecureServerBuilder<TlsState> {
pub fn host(mut self, host: impl Into<String>) -> Self { self.host = Some(host.into()); self }
pub fn port(mut self, port: u16) -> Self { self.port = Some(port); self }
}
impl SecureServerBuilder<TlsDisabled> {
pub fn build(self) -> Result<SecureServerConfig, String> {
let host = self.host.ok_or("host is required")?;
let port = self.port.ok_or("port is required")?;
Ok(SecureServerConfig { host, port, enable_tls: false, tls_cert: None, tls_key: None })
}
}
impl SecureServerBuilder<TlsEnabled> {
pub fn build(self) -> Result<SecureServerConfig, String> {
let host = self.host.ok_or("host is required")?;
let port = self.port.ok_or("port is required")?;
let tls_cert = self.tls_cert.ok_or("tls_cert is required when TLS is enabled")?;
let tls_key = self.tls_key.ok_or("tls_key is required when TLS is enabled")?;
Ok(SecureServerConfig { host, port, enable_tls: true, tls_cert: Some(tls_cert), tls_key: Some(tls_key) })
}
}
fn main() -> Result<(), String> {
let config_no_tls = SecureServerBuilder::new()
.host("0.0.0.0")
.port(8080)
.build()?;
let config_with_tls = SecureServerBuilder::new()
.host("0.0.0.0")
.port(443)
.enable_tls()
.tls_cert("/etc/ssl/cert.pem")
.tls_key("/etc/ssl/key.pem")
.build()?;
Ok(())
}
重要ポイント:
TlsEnabled/TlsDisabledでTLS状態をマーク、異なる状態で異なるbuild()実装- TLS有効化で状態遷移、コンパイラがTLS有効化を「認識」
- 条件付き必須フィールドの検証は
build()で完了、エラーメッセージがより明確
パターン4:検証Builderとbuild時チェック
ビジネスルール検証はsetterに散在させるべきではなく、build()時に統一実行すべき。
use std::collections::HashMap;
#[derive(Debug)]
pub enum ValidationError {
FieldRequired { field: String, reason: String },
FieldRange { field: String, min: String, max: String, actual: String },
Custom(String),
}
impl std::fmt::Display for ValidationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::FieldRequired { field, reason } => write!(f, "フィールド'{}'は必須: {}", field, reason),
Self::FieldRange { field, min, max, actual } => {
write!(f, "フィールド'{}'の範囲エラー: 期待{}~{}, 実際{}", field, min, max, actual)
}
Self::Custom(msg) => write!(f, "{}", msg),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum EvictionPolicy { Lru, Lfu, Fifo, Ttl }
#[derive(Debug, Clone)]
pub struct CacheConfig {
pub max_size_mb: u64,
pub ttl_secs: u64,
pub eviction_policy: EvictionPolicy,
pub namespace: String,
pub enable_compression: bool,
pub compression_level: u32,
pub tags: HashMap<String, String>,
}
pub struct CacheConfigBuilder {
max_size_mb: Option<u64>,
ttl_secs: Option<u64>,
eviction_policy: Option<EvictionPolicy>,
namespace: Option<String>,
enable_compression: Option<bool>,
compression_level: Option<u32>,
tags: HashMap<String, String>,
}
impl CacheConfigBuilder {
pub fn new() -> Self {
Self {
max_size_mb: None, ttl_secs: None, eviction_policy: None,
namespace: None, enable_compression: None, compression_level: None,
tags: HashMap::new(),
}
}
pub fn max_size_mb(mut self, size: u64) -> Self { self.max_size_mb = Some(size); self }
pub fn ttl_secs(mut self, secs: u64) -> Self { self.ttl_secs = Some(secs); self }
pub fn eviction_policy(mut self, policy: EvictionPolicy) -> Self { self.eviction_policy = Some(policy); self }
pub fn namespace(mut self, ns: impl Into<String>) -> Self { self.namespace = Some(ns.into()); self }
pub fn enable_compression(mut self, enable: bool) -> Self { self.enable_compression = Some(enable); self }
pub fn compression_level(mut self, level: u32) -> Self { self.compression_level = Some(level); self }
pub fn tag(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.tags.insert(key.into(), value.into()); self
}
pub fn build(self) -> Result<CacheConfig, Vec<ValidationError>> {
let mut errors = Vec::new();
let max_size_mb = match self.max_size_mb {
Some(v) if v > 0 => v,
Some(v) => { errors.push(ValidationError::FieldRange { field: "max_size_mb".into(), min: "1".into(), max: "unlimited".into(), actual: v.to_string() }); v }
None => { errors.push(ValidationError::FieldRequired { field: "max_size_mb".into(), reason: "キャッシュには最大容量が必要".into() }); 0 }
};
let ttl_secs = match self.ttl_secs {
Some(v) if v > 0 => v,
Some(v) => { errors.push(ValidationError::FieldRange { field: "ttl_secs".into(), min: "1".into(), max: "unlimited".into(), actual: v.to_string() }); v }
None => { errors.push(ValidationError::FieldRequired { field: "ttl_secs".into(), reason: "キャッシュにはTTLが必要".into() }); 0 }
};
let enable_compression = self.enable_compression.unwrap_or(false);
if enable_compression && self.compression_level.is_none() {
errors.push(ValidationError::FieldRequired { field: "compression_level".into(), reason: "圧縮有効時は圧縮レベルの設定が必要".into() });
}
if !errors.is_empty() { return Err(errors); }
Ok(CacheConfig {
max_size_mb, ttl_secs,
eviction_policy: self.eviction_policy.unwrap_or(EvictionPolicy::Lru),
namespace: self.namespace.unwrap_or_else(|| "default".into()),
enable_compression,
compression_level: self.compression_level.unwrap_or(6),
tags: self.tags,
})
}
}
fn main() -> Result<(), Vec<ValidationError>> {
let config = CacheConfigBuilder::new()
.max_size_mb(1024)
.ttl_secs(300)
.eviction_policy(EvictionPolicy::Lru)
.namespace("toolsku_cache")
.enable_compression(true)
.compression_level(6)
.tag("env", "production")
.build()?;
println!("キャッシュ設定: {:?}", config);
Ok(())
}
重要ポイント:
- 全検証エラーを収集して一度に返す、最初のエラーで停止しない
- 検証ロジックは
build()に集中、setterは純粋に保つ - 条件付き検証(「圧縮有効時は圧縮レベル必須」など)はbuildで統一処理
Result<T, Vec<ValidationError>>を返す、UI層での全エラー表示に便利
パターン5:プロダクション級Builderコンポジションパターン
実際のプロジェクトでは、複数のBuilderを組み合わせる必要があります。コンポジションパターンで複雑な設定を構築。
use typed_builder::TypedBuilder;
use std::collections::HashMap;
#[derive(Debug, Clone, TypedBuilder)]
pub struct NetworkConfig {
#[builder(default_code = r#""0.0.0.0".into()"#)]
pub bind_address: String,
#[builder(default = 8080)]
pub port: u16,
#[builder(default = 100)]
pub max_connections: u32,
#[builder(default = 30)]
pub timeout_secs: u64,
#[builder(default = true)]
pub enable_keepalive: bool,
}
#[derive(Debug, Clone, TypedBuilder)]
pub struct StorageConfig {
pub backend: StorageBackend,
#[builder(default = 1024)]
pub max_size_mb: u64,
#[builder(default = 300)]
pub ttl_secs: u64,
#[builder(default = 3)]
pub replication_factor: u32,
}
#[derive(Debug, Clone, PartialEq)]
pub enum StorageBackend { Redis, Memcached, InMemory }
#[derive(Debug, Clone, TypedBuilder)]
pub struct SecurityConfig {
#[builder(default = true)]
pub enable_auth: bool,
#[builder(default_code = r#"Some("HS256".into())"#)]
pub jwt_algorithm: Option<String>,
#[builder(default)]
pub allowed_origins: Vec<String>,
#[builder(default = 3600)]
pub token_expiry_secs: u64,
}
#[derive(Debug, Clone, TypedBuilder)]
pub struct LoggingConfig {
#[builder(default_code = r#""info".into()"#)]
pub level: String,
#[builder(default = true)]
pub enable_json_format: bool,
#[builder(default_code = r#"Some("stdout".into())"#)]
pub output_path: Option<String>,
#[builder(default)]
pub extra_fields: HashMap<String, String>,
}
#[derive(Debug, Clone, TypedBuilder)]
pub struct AppConfig {
pub app_name: String,
#[builder(default_code = r#""1.0.0".into()"#)]
pub version: String,
pub network: NetworkConfig,
pub storage: StorageConfig,
pub security: SecurityConfig,
pub logging: LoggingConfig,
}
impl AppConfig {
pub fn development(app_name: impl Into<String>) -> AppConfigBuilder {
AppConfig::builder()
.app_name(app_name)
.network(NetworkConfig::builder().bind_address("127.0.0.1").port(3000).max_connections(10).build())
.storage(StorageConfig::builder().backend(StorageBackend::InMemory).max_size_mb(128).build())
.security(SecurityConfig::builder().enable_auth(false).build())
.logging(LoggingConfig::builder().level("debug").enable_json_format(false).build())
}
pub fn production(app_name: impl Into<String>) -> AppConfigBuilder {
AppConfig::builder()
.app_name(app_name)
.network(NetworkConfig::builder().bind_address("0.0.0.0").port(8080).max_connections(10000).timeout_secs(60).build())
.storage(StorageConfig::builder().backend(StorageBackend::Redis).max_size_mb(4096).replication_factor(3).build())
.security(SecurityConfig::builder().enable_auth(true).jwt_algorithm("RS256").allowed_origins(vec!["https://toolsku.com".into()]).token_expiry_secs(7200).build())
.logging(LoggingConfig::builder().level("info").enable_json_format(true).output_path("/var/log/toolsku/app.log").build())
}
}
fn main() {
let dev_config = AppConfig::development("toolsku-dev").version("0.1.0").build();
println!("開発設定: {:?}", dev_config);
let prod_config = AppConfig::production("toolsku").version("2.5.0").build();
println!("本番設定: {:?}", prod_config);
}
重要ポイント:
- 各サブ設定に独立したBuilder、責務が明確
AppConfigはサブBuilderのコンポジションで構築、階層が明確- プリセットメソッド(
development()/production())がすぐ使える設定を提供 - プリセットは
AppConfigBuilderを返す、さらにカスタマイズしてプリセット値を上書き可能
よくある落とし穴
落とし穴1:型状態BuilderでPhantomData忘れ
// ❌ 間違い:ジェネリックパラメータが未使用
struct Builder<HostSet> {
host: Option<String>,
// error[E0392]: parameter `HostSet` is never used
}
// ✅ 正しい:PhantomDataでジェネリックパラメータをマーク
struct Builder<HostSet> {
host: Option<String>,
_host: PhantomData<HostSet>,
}
落とし穴2:typed-builderのデフォルト値型不一致
// ❌ 間違い:default値の型がフィールド型と不一致
#[derive(TypedBuilder)]
struct Config {
#[builder(default = "localhost")] // &strをStringに代入不可
pub host: String,
}
// ✅ 正しい:default_codeを使用
#[derive(TypedBuilder)]
struct Config {
#[builder(default_code = r#""localhost".into()"#)]
pub host: String,
}
落とし穴3:BuilderがSend/Syncでない
// ❌ 間違い:PhantomData<*const ()>はSend/Syncではない
struct Builder<HostSet> {
_host: PhantomData<*const ()>,
}
// ✅ 正しい:PhantomData<fn() -> HostSet>を使用
struct Builder<HostSet> {
_host: PhantomData<fn() -> HostSet>, // fn() -> TはSend+Sync
}
落とし穴4:型状態遷移でのフィールド欠落
// ❌ 間違い:遷移時に全フィールドの引き渡し忘れ
impl Builder<Missing> {
fn host(self, host: String) -> Builder<Set<String>> {
Builder {
host: Some(host),
_host: PhantomData,
// portフィールドが欠落!
}
}
}
// ✅ 正しい:全フィールドが引き渡されることを確認
impl Builder<Missing> {
fn host(self, host: String) -> Builder<Set<String>> {
Builder {
host: Some(host),
port: self.port,
_host: PhantomData,
}
}
}
落とし穴5:検証Builderが単一Errorを返す
// ❌ 間違い:最初のエラーのみ返す
fn build(self) -> Result<Config, ValidationError> {
let host = self.host.ok_or(ValidationError::FieldRequired("host"))?;
let port = self.port.ok_or(ValidationError::FieldRequired("port"))?;
Ok(Config { host, port })
}
// ✅ 正しい:全エラーを収集して一度に返す
fn build(self) -> Result<Config, Vec<ValidationError>> {
let mut errors = Vec::new();
// ... 全エラーを収集 ...
if !errors.is_empty() { return Err(errors); }
Ok(Config { /* ... */ })
}
エラートラブルシューティング表
| エラー現象 | 可能な原因 | 調査方法 | 解決策 |
|---|---|---|---|
| "no method named build" | 必須フィールド未設定 | Builderのジェネリック状態を確認 | 全必須フィールドのsetterが呼ばれていることを確認 |
| E0392 parameter never used | PhantomData欠落 | ジェネリックパラメータが使用されているか確認 | PhantomData<T>フィールドを追加 |
| 型不一致 | default値の型エラー | #[builder(default = ...)]を確認 |
default_codeまたは正しい型を使用 |
| BuilderがSendでない | PhantomDataが生ポインタを使用 | PhantomDataの型パラメータを確認 | PhantomData<fn() -> T>を使用 |
| フィールド値の欠落 | 型状態遷移でフィールド引き渡し漏れ | implブロックのフィールド引き渡しを確認 | 遷移で全フィールドが代入されることを確認 |
| コンパイル時間が長い | ジェネリックパラメータが多すぎる | Builderのジェネリック数を確認 | typed-builderで手動ジェネリックを削減 |
| setterがチェーン不可 | setterがSelfを返すがmutを使用 | setterシグネチャを確認 | Selfを返すことを確認(&mut Selfではない) |
| 循環依存 | AのbuildがBを必要、BのbuildがAを必要 | Builder間の依存関係を確認 | 独立したBuilderに分割 |
| 検証ロジックの重複 | setterとbuildの両方で検証 | 検証コードの位置を確認 | buildでのみ検証 |
| デフォルト値が反映されない | #[builder(default)]の付け忘れ |
オプションフィールドの注釈を確認 | デフォルト値のあるフィールドにdefaultを追加 |
高度な最適化
-
マクロで型状態を簡素化:
declare_builder!マクロを書き、型状態Builderのボイラープレートコードを自動生成 -
Builderシリアライズ:Builderが
serde::Serializeをサポート、半完成の設定をファイルに保存可能に -
環境変数統合:Builderのsetterが環境変数をデフォルト値として自動読み取り、
.port_from_env("APP_PORT")など -
設定ホットアップデート:Builderで構築した設定オブジェクトが
merge()メソッドをサポート、実行時に新設定をマージ可能 -
コンパイル時フィールドカウント:const genericで設定済みフィールド数を追跡、コンパイル時に最低N個のフィールド設定を保証
アプローチ比較
| アプローチ | コンパイル時安全性 | ボイラープレート | 柔軟性 | 適用シナリオ |
|---|---|---|---|---|
| 従来のBuilder | ⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | 単純なオブジェクト |
| 型状態Builder | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | 厳格な必須フィールド |
| typed-builder | ⭐⭐⭐⭐⭐ | ⭐ | ⭐⭐⭐⭐ | ほとんどのプロジェクト |
| 検証Builder | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | 複雑なビジネスルール |
| コンポジションBuilder | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐ | マルチモジュール設定 |
まとめ
Rustの型システムはBuilderパターンを「実行時の祈り」から「コンパイル時の保証」へと進化させます—型状態がコンパイル時にフィールド状態を追跡し、typed-builderがボイラープレートを排除し、ジェネリックBuilderが条件付き必須を表現し、検証Builderがビジネスルールを統一し、コンポジションBuilderが複雑な設定を構築します。 覚えておいてください:Rust Builderの真髄は「メソッドチェーン」ではなく「不正な状態を表現不可能にすること」です。設定が構築できるなら、それは必ず正当です。
オンラインツールおすすめ
- JSONフォーマッター — Builder出力のJSONをフォーマット、構造を素早く検証
- cURL→コード変換 — APIリクエストをRust Builderコードに変換
- ハッシュ計算 — 設定ファイルのハッシュを計算、ビルドの一貫性を確保
ブラウザローカルツールを無料で試す →