項目7:複雑な型にはビルダを使おう
hr.icon
以下のような構造体を考える
code:rs
pub struct PhoneNumberE164(pub String);
pub struct Details {
pub given_name: String,
pub preferred_name: Option<String>,
pub middle_name: Option<String>,
pub family_name: String,
pub mobile_phone: Option<PhoneNumberE164>,
}
上記を生成する場合以下のような記述が必要になるが、このような 定型的なコードは壊れやすい(e.g. 新しいフィールドを追加したい場合)
code:rs
let dizzy = Details {
given_name: "Dizzy".to_owned(),
preferred_name: None,
middle_name: None,
family_name: "Mixer".to_owned(),
mobile_phone: None
};
code:rs
let dizzy = Details {
given_name: "Dizzy".to_owned(),
family_name: "Mixer".to_owned(),
..Default::default()
};
しかし、#[derive(Default)] はすべてのフィールドが Default を実装している場合のみ可能
code:rs
pub struct Details {
...
pub last_seen: Option<time::OffsetDateTime>,
// time::Date は Default を実装していない
pub date_of_birth: time::Date,
}
その場合は手動で実装する必要があるが、ユーザ定義型以外では 孤児ルール もあるため実装できない したがって、すべてのフィールドを指定する必要がある
具体例: Builder パターンを採用するケース
最もシンプルな実装法は、対象となるデータ構造を構築するのに必要な情報を保持する構造体(ビルダ型)を別に持つ方法である
code:rs
pub struct DetailsBuilder(Details);
impl DetailsBuilder {
pub fn new(
given_name: &str,
family_name: &str,
date_of_birth: time::Date
) -> Self {
DetailsBuilder(Details {
given_name: given_name.to_owned(),
preferred_name: None,
middle_name: None,
family_name: "Mixer".to_owned(),
mobile_phone: None,
date_of_birth,
last_seen: None
})
}
}
ビルダ型は、構築するのに必要なフィールドを埋めるための ヘルパメソッド を持つ code:rs
impl DatailsBuilder {
// ...
pub fn preferred_name(mut self, preferred_name: &str) -> Self {
self.0.preferred_name = Some(preferred_name.to_owned());
self
}
pub fn middle_name(mut self, middle_name: &str) -> Self {
self.0.middle_name = Some(middle_name.to_owned());
self
}
pub fn just_seen(mut self) -> Self {
self.0.last_seen = Some(time::OffsetDateTime::now_utc());
self
}
// ...
}
これらのメソッドは self を 消費 するが、新しい Self を返すため メソッドチェーン が可能 構築したデータ構造は build メソッドで返す
code:rs
pub fn build(self) -> Details {
self.0
}
呼び出し例
code:rs
let also_bob = DetailsBuilder::new(
"Robert",
"Builder",
time::Date::from_calendar_date(
1998, time::Month::November, 28
).unwrap(),
)
.middle_name("the")
.preferred_name("Bob")
.just_seen()
.build();
注意点
1. 構築プロセスを複数に分割できない
code:rs
let builder = DetailsBuilder::new(
"Robert",
"Builder",
time::Date::from_calendar_date(
1998, time::Month::November, 28
).unwrap(),
);
if informal {
builder.preferred_name("Bob");
}
let bob = builder.build();
2. 単一のインスタンスしか作れない
以下のように複数インスタンスを作成しようとすると、コンパイルエラーになる
code:rs
let smith = DetailsBuilder::new(
"Agent",
"Smith",
time::Date::from_calendar_date(1998, time::Month::November, 28).unwrap(),
);
回避策
1. については、消費したビルダを同じ変数に書き戻すことで回避できる
code:rs
let mut builder = DetailsBuilder::new(
"Robert",
"Builder",
time::Date::from_calendar_date(1998, time::Month::November, 28).unwrap(),
);
if informal {
builder = builder.preferred_name("Bob");
}
また、ヘルパメソッドで &mut self を受け取って、&mut Self を返すようにする ことでも回避できる
code:rs
pub fn preferred_name(&mut self, preferred_name: &str) -> &mut Self {
self.0.preferred_name = Some(preferred_name.to_owned());
self
}
しかし、この場合はビルダの構築とヘルパメソッドの呼び出しを続けて書くことができない
code:rs
let mut builder = DetailsBuilder::new(
"Robert",
"Builder",
time::Date::from_calendar_date(1998, time::Month::November, 28).unwrap(),
)
.middle_name("the")
.just_seen();
これは、new の戻り値を変数に 束縛 することで回避できる code:rs
let mut builder = DetailsBuilder::new(
"Robert",
"Builder",
time::Date::from_calendar_date(1998, time::Month::November, 28).unwrap(),
);
builder.middle_name("the").just_seen();
また、2. は build が self を消費しないようにすれば回避できる
code:rs
pub fn build(&self) -> Details {
self.0.clone()
}
対象のデータ構造が Clone を実装している場合、上記のように clone を呼び出すだけで良い
実装していない場合、build が呼ばれるたびに新しいインスタンスを作れる情報をビルダに保持する必要がある
前述のように自身でビルダを実装するのも良いが、Builder Pattern Derive クレートを用いるのも手
code:rs
use derive_builder::Builder;
pub struct PhoneNumberE164(pub String);
pub struct Details {
pub given_name: String,
pub preferred_name: Option<String>,
pub middle_name: Option<String>,
pub family_name: String,
pub mobile_phone: Option<PhoneNumberE164>,
pub date_of_birth: time::Date,
pub last_seen: Option<time::OffsetDateTime>,
}
impl DetailsBuilder {
pub fn new(given_name: &str, family_name: &str, date_of_birth: time::Date) -> Self {
Self {
given_name: Some(given_name.to_owned()),
preferred_name: None,
middle_name: None,
family_name: Some(family_name.to_owned()),
mobile_phone: None,
date_of_birth: Some(date_of_birth),
last_seen: None,
}
}
}