DDD のパターンを Rust で表現する ~ Entity 編 ~

こんにちは。CADDi でバックエンドエンジニアをしている @kuwana-kb です。 この記事は CADDi Advent Calendar 12日目の記事です。昨日は、山下さんによる GitOpsの概要と実践例 〜Kustomize + CircleCI編〜 でした!

本日は「DDD のパターンを Rust で表現する ~ Entity 編 ~」と題しまして、 Rust で DDD のパターンを表現してみたいと思います。

目次

[toc]

はじめに

DDDとは、 Domain-Driven Design(ドメイン駆動設計)の略です。アプリケーションの扱う業務領域に焦点をあてた設計手法であり、エリック・エヴァンスが提唱しました。詳細については、以下の記事で紹介していますので、そちらをご覧ください。 DDDのパターンをRustで表現する ~ Value Object編 ~

また、今回の記事を書く上で「ドメイン駆動設計入門」(著: 成瀬 允宣氏)という書籍を参考にさせていただきました。書籍のサンプルコードは C# でして、本記事ではそのコードを参考に Rust で DDD のパターンを表現しています。

なお、本記事に登場するコードは kuwana-kb/ddd-in-rust に格納しています。

DDD における Entity とは

まず、Entity について整理しましょう。

Entity とは、一意なものを表現する概念です。人(Human)で例えてみましょう。

ここに二人の人がいます。

二人の名前はどちらも「山田太郎」さんです。では、この二人は全くの同一人物といえるでしょうか?現実世界では、同姓同名がありえます。つまり、二人は同じ名前でありながら、異なる一意な存在です。コード的な表現でいえば name という属性は同じでも、二人を区別できる必要が有ります。

これは、Entity は属性が同じであっても区別されることを示します。また、二人を区別するための識別子が必要となることも示しています。

また、人は時を経て状態が変わります。例えば、身長が伸びたり、体重が増えたりしますね。これは、ライフサイクルを通じて height や weight といった属性が変化することと同義です。つまり、Entity は可変であるといえるでしょう。

属性として登場した name, height, weight は、属性単位で見たときに値が同じであれば同一とみなせます。これらは Value Object として定義できるでしょう。一方で、Human はその構成要素である name, height, weight がすべて同一だとしても、同じ存在とは限りません。そして、ライフサイクルによって変化することがある。

これが Entity です。

実装パターンの紹介

まずはシンプルに実装してみる

ここでは、 EC サイトのユーザーを例にとってみます。ユーザーの仕様は以下とします。

  • ユーザーには名前を設定できる
  • 名前は 3 文字以上である必要がある

Rust の型で表現すると以下のような実装になるでしょう。

use common::MyError;

// User
// 現時点では、可変性と同一性を持たない。
#[derive(Clone, Debug)]
pub struct User {
    name: String,
}

impl User {
    // Userのコンストラクタ
    pub fn new(name: &str) -> Result {
        if name.chars().count() < 3 {
            return Err(MyError::type_error("ユーザー名は3文字以上です"));
        }
        Ok(Self {
            name: name.to_string(),
        })
    }
}

とてもシンプルですね。User 型は属性として name を持っています。また、コンストラクタとして new() 関数を持っています。

しかし、現在の実装だと後から名前を変えたくても変える方法がありません。

可変性を与える

次に User 型に対して、 Entity のもつ特性である可変性を与えます。具体的には、User.name 属性を変更できるようにしてみましょう。

use common::MyError;

// User
pub struct User {
    name: String,
}

impl User {
    // User のコンストラクタ
    pub fn new(name: &str) -> Result {
        let mut user = Self {
            name: Default::default(),
        };
        user.change_name(name)?;
        Ok(user)
    }

    // ふるまいを通じて属性を変更する
    // 変更ロジックはメソッド内に閉じ込めている
    pub fn change_name(&mut self, name: &str) -> Result<(), MyError> {
        if name.chars().count() < 3 {
            return Err(MyError::type_error("ユーザー名は3文字以上です"));
        }
        self.name = name.to_string();
        Ok(())
    }
}

User 型に対して名前を変更できる change_name() メソッドを追加しました。これで名前を変更できるようになりましたね。

さて、change_name() メソッドには、冒頭に述べた「名前は 3 文字以上である必要がある」という仕様が含まれています。この User.name の制約は名前変更時だけでなく、 User を作成するときにも満たす必要が有ります。したがって、new() 関数の処理は、 User インスタンスを生成してから change_name() メソッドを通じて引数である name を注入しています。なお、change_name() はバリデーションによって失敗する可能性がある(= Result 型を返す)ため、 new() の返り値も Result になっています。

この実装でも仕様は満たせているのですが、 名前の制約を満たすために毎回 change_name() を呼ぶのは面倒です。また、new() の処理で user.change_name(name)? のような形で制約を満たすようにしていますが、フィールドが追加される度に user.change_***() といったメソッド呼び出しを追加する必要がでてきそうな点も気になります。

そこで、名前の持つ制約を型に閉じ込めてしまいましょう。具体的には、 Name 型を定義して「名前は 3 文字以上である必要がある」という仕様は Name 型が持つ形にします。

use common::MyError;

// User
pub struct User {
    // name 属性は、Name 型を持つ形に変更
    name: Name,
}

impl User {
    // input の型も String 型 から Name 型に変更
    pub fn new(name: Name) -> Self {
        Self { name: name }
    }

    // バリデーションのロジックは Name 型に移譲している
    pub fn change_name(&mut self, name: Name) {
        self.name = name;
    }
}

// ユーザー名
//
// 制約として、3文字以上である必要がある
// Name 型を新たに Value Object として定義した
pub struct Name(String);

impl Name {
    pub fn new(s: &str) -> Result {
        if s.chars().count() < 3 {
            bail!(MyError::type_error("ユーザー名は3文字以上です"))
        }
        Ok(Name(s.to_string()))
    }
}

Name 型を新たに定義しました。

「名前は 3 文字以上である必要がある」という仕様は、 Name::new() に移譲されています。Name 型のインスタンスが生成できた時点で、上記の仕様を満たした状態であるということが保証されます。

Name 型を定義したことで、 User 型にも変更を加えています。これまで User.name 属性や引数の型は String 型でしたが、 Name 型になっていますね。Name 型のインスタンスを生成するタイミングでバリデーションが入るため、User::new()change_name() の返り値の型から Result 型 が消えています。

さて、今回はドメイン上の仕様をメソッド(change_name())ではなく、型(Name 型)に移譲してみました。

// name は String 型
// どのような値が入っているかはパット見ではわからない
pub struct User {
    name: String,
}

// name は Name 型
// Name 型特有の値であることがひと目でわかる
// 型の定義に飛べばその制約も知ることができる
pub struct User {
    name: Name,
}

私としては、型で表現した方がオブジェクトの持つ特性がより伝わりやすいと感じます。

同一性を与える

次は、 User 型に対して Entity の特性である同一性を与えたいと思います。名前が一緒だからといって、同じユーザーとは限りません(少なくとも今回の例においては)。したがって、 User.name 属性が同じだったとしても、別々の User であると識別できるようにしたいと思います。

use derive_getters::Getters;
use common::MyError;

/// Userモデルに対して可変性と同一性を与えた
#[derive(Clone, Debug, Getters)]
pub struct User {
    id: UserId,
    name: Name,
}

impl User {
    pub fn new(id: UserId, name: Name) -> Self {
        Self { id, name }
    }

    // nameフィールドは可変性を持つ
    pub fn change_username(&mut self, name: Name) {
        self.name = name;
    }
}

さて、今回は User に対して id 属性を追加しました。User.id 属性の型は UserId 型です。 簡単ですが、UserId 型は以下のような文字列を受け取る型とします。

/// ユーザーID
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct UserId(String);

impl UserId {
    pub fn new(s: &str) -> Self {
        Self(s.to_string())
    }
}

次に User 型の同一性をふるまいとして実装します。同値関係をあらわすには、 Eq, PartialEq という Trait を利用します。コード的な表現でいうと x user_a のような形で、同一かどうか検証できるようになります。この時、User が同一であるかは User.id のみを比較対象とします。User.name は比較対象にしません。

// trait PartialEq は半同値関係をの性質を表す
impl PartialEq for User {
    fn eq(&self, other: &Self) -> bool {
        // 今回の実装は、idの値が同一か検証するようにしている
        self.id == other.id
    }
}

// trait Eq は同値関係の性質を表す
impl Eq for User {}

これで、 User に対して同一性の特性を与えることができました。実装だけだとわかりづらいと思うので、簡単な使用例をみてましょう。

/// 名前
#[test]
fn test_user_eq() {
    // User インスタンスを生成する
    let user_before = User::new(UserId::new("DummyId1"), Name::new("Hoge").unwrap());
    // User インスタンスをコピーし, before と after で2つにする
    let mut user_after = user_before.clone();
    // after のインスタンスの名前を変更する
    user_after.change_username(Name::new("Fuga").unwrap());

    // User が同一であるかを検証する
    // User の名前を変更しても同一性は同じままである
    assert_eq!(user_before, user_after); // Ok
}

名前を変更する前と後で User が同一であるかを検証しました。User は可変性と同一性の性質をもったオブジェクトであり、 Entity の性質を満たしています。

まとめ

今回は、Rust で Entity をどのように表現できるか、についてご紹介しました。みなさんに Rust の魅力が少しでも伝われば幸いです。


CADDiでは「モノづくり産業のポテンシャルを解放する」ために仲間を探しています。 実現したい世界に向け、作らなければならないもの、改善したいことが無限にあります。

少しでも興味を持って頂けましたら、リニューアルされたばかりの採用サイトをご覧ください。

また、カジュアル面談も行っていますので、実際にエンジニアに会ってみたい方はこちらから、 どうぞ宜しくお願いいたします。