🔖

Rustでドメイン固有型を作る際のコツ

8 min read

ドメイン固有型である、ドメインオブジェクトをRustで実装する際のコツみたいなものを簡単にまとめました。まぁ「ドメイン固有型」というと大袈裟なので、「独自に型を作る」を想定してもらえばよいと思います。OOPに慣れている人がハマりやすい点も簡単に言及しています。ご参考までに。

アドレス帳型を作る

アドレス帳型を例にします。
先にコードを以下に示します。コード全体はGitHubリポジトリを参照してください。

#[derive(Debug, Clone)]
pub struct AddressBook {
  name: String,
  entries: Vec<AddressEntry>,
}

impl Default for AddressBook {
  fn default() -> Self {
    Self {
      name: String::default(),
      entries: Vec::default(),
    }
  }
}

impl AddressBook {
  
  pub fn new(name: &str) -> Self {
    Self {
      name: name.to_owned(),
      entries: Vec::default(),
    }
  }

  pub fn name(&self) -> &str {
    &self.name
  }

  pub fn add_entry(&mut self, address_entry: AddressEntry) {
    self.entries.push(address_entry);
  }

  pub fn remove_entry(&mut self, address_entry_id: AddressEntryId) -> AddressEntry {
    let index = self
      .entries
      .iter()
      .position(|e: &AddressEntry| e.id == address_entry_id)
      .unwrap();
    self.entries.remove(index)
  }

  pub fn iter(&self) -> impl Iterator<Item = &AddressEntry> {
    self.entries.iter()
  }
}

使い方は以下のようになります。同じ変数名が再利用されていますが、Rustではコンパイルエラーになりません。シャドーイングと呼びます。

let mut address_book = AddressBook::new("社員名簿");
    
let address_entry_id = AddressEntryId::new(1);
let personal_name = PersonName::new("Junichi", "Kato");
let address = Address::new("111-0001", "Tokyo-to", "minato-ku 1", Some("hoge 1 building"));
let address_entry = AddressEntry::new(address_entry_id, personal_name, address);
address_book.add_entry(address_entry);

let address_entry_id = AddressEntryId::new(2);
let personal_name = PersonName::new("Taro", "Yamamoto");
let address = Address::new("111-0002", "Tokyo-to", "minato-ku 2", Some("hoge 2 building"));
let address_entry = AddressEntry::new(address_entry_id, personal_name, address);
address_book.add_entry(address_entry);

let address_entry_id = AddressEntryId::new(3);
let personal_name = PersonName::new("Hanako", "Yamada");
let address = Address::new("111-0003", "Tokyo-to", "minato-ku 3", Some("hoge 3 building"));
let address_entry = AddressEntry::new(address_entry_id, personal_name, address);
address_book.add_entry(address_entry);

println!("name = {}", address_book.name())
address_book.iter().for_each(|e| println!("{:?}", e));

/* 出力例
name = 社員名簿
AddressEntry { id: AddressEntryId(1), name: PersonName { first_name: "Junichi", last_name: "Kato" }, address: Address { postal_code: "111-0001", pref: "Tokyo-to", address: "minato-ku 1", building: Some("hoge 1 building") } }
AddressEntry { id: AddressEntryId(2), name: PersonName { first_name: "Taro", last_name: "Yamamoto" }, address: Address { postal_code: "111-0002", pref: "Tokyo-to", address: "minato-ku 2", building: Some("hoge 2 building") } }
AddressEntry { id: AddressEntryId(3), name: PersonName { first_name: "Hanako", last_name: "Yamada" }, address: Address { postal_code: "111-0003", pref: "Tokyo-to", address: "minato-ku 3", building: Some("hoge 3 building") } }
*/

構造体を定義する

クラス定義のような感覚で構造体を定義しましょう。構造体内部にはプロパティ相当のメンバーを定義します。

#[derive(Debug, Clone)]
pub struct AddressBook {
  name: String,
  entries: Vec<AddressEntry>,
}

メンバーにこのアドレス帳の名前(name)を持たせます。&str,Stringどっちなんだという人は以下の記事を読みましょう。String型にします。

https://qiita.com/Kogia_sima/items/6899c5196813cf231054

アドレス情報を保持するためのAddressEntry型を別途作ります。依存する型も適当に作ります。

#[derive(Debug, Clone)]
pub struct AddressEntry {
  pub id: AddressEntryId,
  pub name: PersonName,
  pub address: Address,
}

AddressBookは複数のAddressEntryを保持するので、entriesVec<AddressEntry>として定義します。長さが固定なら配列でよいですが、不定なのでVecにします。

ファクトリ(関連関数)

インスタンスを作成する際に以下のように記述できます。が、メンバーを可視にする必要があります。

let address_book1 = AddressBook {
  name: "従業員名簿".to_owned(),
  entries: Vec::default(),
};

メンバーをプライベートにする場合は、コンストラクタのような役割をする関連関数を定義します。

  pub fn new(name: &str) -> Self {
    Self {
      name: name.to_owned(),
      entries: Vec::default(),
    }
  }

スタティックメソッドみたいなものだと思えばよいです。

let address_book1 = AddressBook::new("従業員名簿");

Rustでは同じ関数名で引数のパターンを複数定義できません(オーバーロードができない)。基本的にはそれぞれ別々の名前を付ける必要があります。

追記:9/7

traitを使えばオーバーロードは可能です。まぁ多相性を求めないのにtraitでいいの?&記述量が増えるので良し悪しがあります。それならわかりやすい個別の名前を付けるのがよいと思いますが…。

https://ubnt-intrepid.hatenablog.com/entry/2016/09/21/063652
https://qiita.com/smicle/items/235fb6e3ead86c2b03f3

メソッドを定義する

第一引数にレシーバーを記述するメソッドを定義します。

pub fn name(&self) -> &str {
  &self.name
}

&selfself: &AddressBookと等価と思ってください(意味が同じでもself: &AddressBookとは書かないでください)。

このメソッドでは、nameを返すだけですので、以下のような書き方もできます。

pub fn name(&self) -> String {
  self.name.clone()
}

動く汚いコードを書く際は問題ないでしょう。でも、無駄なアロケーションが発生しています。このケースではself.nameの参照を返せばよく、cloneで複製するかどうか呼び出し元に任せるべきです。

可変性を扱う

&selfは不変参照であるため、状態を変更できません。状態を変更するには&mut selfを利用すれば、self.entriesに要素を追加できます。

pub fn add_entry(&mut self, address_entry: AddressEntry) {
  self.entries.push(address_entry);
}

「えっ、可変型にしてしまっていいの? 今どき不変でしょ?」と思った方

https://doc.rust-jp.rs/book-ja/ch04-02-references-and-borrowing.html#可変な参照

を読みましょう。

特定のスコープで、ある特定のデータに対しては、 一つしか可変な参照を持てないことです。

さらに不変な参照をしている間は、可変な参照をすることはできません。

Rustの可変は安全に扱えるようになっています。

定義されたシグニチャから分かるとおり、可変参照時でないと可変操作ができません。不変時に可変操作しようとするとコンパイルエラーになります。

let mut address_book = AddressBook::new("社員名簿");

address_book.add_entry(address_entry); // &mut self
let address_book = AddressBook::new("社員名簿");

address_book.add_entry(address_entry); // &selfはコンパイルエラー。そんなメソッドはない。

Javaなどの言語でクラスを不変するとプロパティが多いクラスだとビルダを別途作っていたと思いますが、Rustの場合は同じ型で不変と可変を定義できます。selfを引数に取るRustだからできることですね。

一応、不変も可能…

アロケーションを気にしないのであれば、以下のようにします。

pub fn add_entry(self, address_entry: AddressEntry) -> Self {
  let mut new_self = self;
  new_self.entries.push(address_entry);
  Self {
    ..new_self
  }
}

変更された新たなインスタンスを生成できます。たいてい、もとのインスタンスは使わないし、必要なときにcloneすればよいので、&selfではなくselfで十分でしょう。

let address_book = AddressBook::default();
let address_book = address_book.add_entry(address_entry);
// 更新前のインスタンスを残したいなら、呼び出し元でclone()すればいい
// let address_book_updated = address_book.clone().add_entry(address_entry);

いろいろな実装を読んでいますが、あまりこういった実装はみません。デフォルトとして不変参照を使っていれば副作用を抑制できますし、可変は狭いスコープで使って制御が抜けると不変に戻せるからではないかと思われます。

Vecではなくスライスを引数に取る

Rustには可変長引数がありません。同型の引数を複数個取る際は、Vecかスライスになります。が、以下の記事を読みましょう。

https://qiita.com/mosh/items/709effc9e451b9b8a5f4

Vec型でええやんとなりそうですが、

pub fn add_entries(&mut self, address_entries: Vec<AddressEntry>) {

スライスの参照を受け取ったほうが使い勝手がよさそうです。今回の場合は、要素型の実体が必要なのでto_vecVec型に変換しinto_iterを使っているのでアロケーションが発生します。(それがパフォーマンス上 問題になるようであれば、address_entries: Vec<AddressEntry>にしてムーブできるようにするかもしれませんが…)

pub fn add_entries(&mut self, address_entries: &[AddressEntry]) {
  address_entries
    .to_vec()
    .into_iter()
    .for_each(|e: AddressEntry| self.add_entry(e))
}

Vecもしくはスライスを使って値に集合を受け取れます。

let mut address_book = AddressBook::new("社員名簿");

let entries = [address1.clone(), address2.clone()]; // 配列
address_book.add_entries(&entries); // スライスの参照

let entries = vec![address1, address2]; // Vec
address_book.add_entries(&entries); // Derefの実装がスライスの参照を返す

追記:

https://b.hatena.ne.jp/entry/4707991669921759874/comment/Magicant

ご指摘ありがとうございます。

ということで、早速、実装してみた。

pub fn add_entries1(&mut self, address_entries: impl IntoIterator<Item=AddressEntry>) {
  address_entries
      .into_iter()
      .for_each(|e| self.add_entry(e))
}
  
let entries1= [address_entry1.clone(), address_entry2.clone()];
address_book.add_entries1(entries1);

let entries2= vec![address_entry1, address_entry2];
address_book.add_entries1(entries2);  

スライスの参照で受け取ってto_vec()するとアロケーションが発生するので、こっちはムーブできるので効率よさそうです。

Discussion

ログインするとコメントできます