Nature における「スマートロックとはなにか」をドメインモデリングする

バックエンドエンジニアの長田です。

2026年4月30日に、Nature はスマートロック製品である「Nature Lock」と、 その専用のスマートキーであり Apple の「探す」アプリにも対応した「Nature Key」を発売しました。

また、5月19日には iOS のショートカットアプリなどの Nature Home アプリ外からの操作に、6月3日には1ドア2ロック解錠/施錠に対応する追加リリースも行っています。

今回は特に「Nature Lock」について、Nature の サーバーサイドアプリケーション内で扱うためにどのようにモデリングしていったのか、その変遷を紹介します。

背景

Nature Lock 開発以前から Nature のサービスにあった概念として、「ホーム」「デバイス」「家電」があります。

  • ホーム: 現実世界の「家」に対応する。ユーザーが「デバイス」や「家電」を登録する
  • デバイス: ホームに登録される Nature 製品。Nature Remo シリーズNature Remo E シリーズNature EV Switchがある
  • 家電: ホームに登録される家電。Remo シリーズにより赤外線や BLE 通信、あるいは自らが持つ通信機能を通じて制御される対象。エアコンや照明、Nature EV Switch など。

ドメイン用語の定義

モデリングを始める前に、まずドメイン用語の定義から始めました。 新しい概念が登場するということは、それに関連した新しい用語が導入されるということです。 また、スマートロック関連の用語には紛らわしいものが多く、はじめに用語を定義しておかないと混乱が生じることが予想されました。 例えば・・・、

  • : Nature Key のこと?BLE通信の認証鍵?物理鍵のこと?サムターン部分?デッドボルト部分?
  • ロック: スマートロック本体のこと?施錠すること?
  • 解錠: それとも開錠?(表記揺れ)

ドメイン用語は Notion のデータベースとしてまとめ、各ユースケース定義から適宜リンクすることで定義を参照しやすくしました。

図1 ドメイン用語の定義(一部)

用語を定義・統一することで、開発チーム内部だけでなく、ユーザーとのコミュニケーションもスムーズに行えるようにすることも意識しました。 新しく定義された用語は関係メンバー間で周知し、合意が得られたものを採用することにしました。

「デバイス」とは何か

「背景」で デバイス という概念があることを説明しましたが、最初の課題はこのデバイスという概念を既存の製品シリーズと整合しつつ、 Nature Lock/Key についても説明できるよう、定義を改めることでした。

Nature Lock とはなにか

この度加わった Nature Lock について考えると・・・

  • Nature の製品だから デバイス だろう
  • Remo シリーズで BLE 通信で操作できるから 家電 だろう

という共通認識がありました。 つまり、 Nature Lockデバイス であり 家電 でもあるということです。

デバイス であり 家電 でもある、という点では Nature EV Switch も同様なのですが、 それに加えて別の デバイス (= Nature Remo)との連携も可能、という違いがあります。 つまり、いままで Nature のサービス内にはなかった新しい関係性を定義する 必要がありました。

Nature Key とはなにか

一方、Nature Key については、

  • Nature の製品だから デバイス だろう
  • Nature Key 自体を操作するわけではないので 家電ではない だろう

という共通認識がありました。 単なる デバイス であれば Nature Remo や Nature Remo E 等と同じように扱えそうですが、それらと Nature Key との違いとして、「Nature のサーバー1と通信できない」という性質があります。

改めて、「デバイス」とは何か

Nature のサーバーサイドアプリケーション内において、既存のデバイスは「サーバーと通信できるもの」としてモデリングされていました。 しかし、Nature Lock はサーバーと直接通信することはできないため、この定義には当てはまりません。

デバイスを使うためには、まず Nature Home アプリでホームに登録する必要があります。 これをもとに「ホームに登録できるものがデバイス」と規定できるのではという意見もありましたが、 Nature Key はデバイスではあるものの、必ず Nature Lock に紐づける必要があり、それ単体では上記の規程を満たすことができません。

表としてまとめると以下のようになります。

製品 デバイス? 家電? サーバーと通信できる? 単独でホームに登録可能?
Nature Remo ❌️ ⭕️ ⭕️
Nature Remo E ❌️ ⭕️ ⭕️
Nature EV Switch ⭕️ ⭕️ ⭕️
Nature Lock ⭕️ ❌️ ⭕️
Nature Key ❌️ ❌️ ❌️

最終的に、「Nature Home アプリに登録できる Nature 製品」という非常に広い範囲をカバーできる定義に落ち着きました。 「それは定義する意味があるのか?」と思われるかもしれませんが、詳細化は各製品を個別にモデリングする中で行えばよいですし、 なにより開発メンバー内の認識が合っていることが重要なので、十分に意味があると考えています。

図2 デバイスとは何か?を議論している様子(このあともまだまだ続いています)

ちなみに、 Nature Lock は既存のデバイスとは全く異なるモノとしてモデリングする案もありましたが、 既存のビジネスロジック2とあまりにもかけ離れてしまうので不採用となりました。

最初のモデル

用語が出そろったので、ようやく Nature Lock のモデリングです。

ℹ️ コード内では以下のように対応しています。
- ホーム: Home
- デバイス: Device
- 家電: Appliance
- Nature Lock: SmartLock

はじめは既存のビジネスロジックとの親和性を重視し、Appliance をルート集約とし、 その一種として SmartLock がある、という建付けにしていました。

既存の Appliance には、これを「コントロールするためのデバイス」を表すリレーションとして Device (= Remo)が存在していました。 このリレーションを活かしつつ、「デバイスであり家電である」ことを表すために、「仮想デバイス」という概念を導入しました。

図3 仮想デバイスの導入

「仮想デバイス」は、 SmartLock がそれを操作するための Nature Remo に紐付けられていない状態で、仮に紐付けられる Device です。 Appliance は必ず Device に紐付けられている必要があり、この制約を満たすために導入された概念でした。

この図をコードとして起こしたものは以下のようになっていました。

type SmartLock struct {
    // appliance は smartLock を家電として操作する際の対象としての Appliance
    appliance Appliance

    mainLock smartLock
    subLock  *smartLock
}

type smartLock struct {
    id       SmartLockID
    serialNo SerialNo
    position SmartLockPosition
 
    // device は smartLock そのものを表す Device
    device Device
}

type Appliance struct {
    id ApplianceID
 
    // Remo に紐づいていない状態では仮想デバイスに紐づく。
    // Remo に紐づいた状態では Remo を表す Device に紐づく。
    device Device

    ...
}

※ 雰囲気を伝えるために一部を抜粋したものです。以降のコードについても同様。

SmartLock 定義の見直し

図3の中に「?」が書かれていることからもわかるように、「仮想デバイス」という概念はメンバー内でもしっくり来るものではありませんでした。 実際、開発が進むにつれて「仮想デバイス」という概念は実態には合わないことがわかってきました (サーバーサイド、しかも一部のメンバー以外は「仮想デバイス」なんて言葉は使っていなかったので!)。

Appliance をルート集約として扱っているのが問題なのでは?という議論から、SmartLock をルート集約としてモデリングし直すことにしました。

図4 SmartLock が Appliance と Device を内包する

スマートロックというモノの特定には SmartLock を用い、家電として操作する場合は内包する Appliance を、 デバイスとして操作する場合は同じく内包する Device を用いてビジネスロジックを走らせるわけです。 これで会話の中で登場する「スマートロック」とモデルが一致するようになりました。

コードで表すと以下のようになります。

type SmartLock struct {
    id SmartLockID

    // device は SmartLock そのものを表す Device
    device Device

    // appliance は SmartLock を家電として操作する際の情報を持つための Appliance
    appliance Appliance

    // controllerDeviceID は SmartLock を Appliance として操作する際のコントローラーとなる Device の ID
    // Device と紐づけられていない場合は nil
    controllerDeviceID *DeviceID

    position Position
    role     Role

    // pairID は 1door2lock 設定時のペアになる SmartLock の ID
    // 1door2lock 設定がされていない場合はnil
    pairID *core.SmartLockID
}

1door2lock 構成の扱い

ひとつのドアに2つの錠がついている場合、その両方に Nature Lock を取り付けることで、 ワンアクションで両方の錠を操作できるようにする機能を「1door2lock 解錠/施錠」と呼んでいます。

ひとつのスマートロックを操作する場合は、 図4 で示したような SmartLockApplianceDevice を内包する形で自然に表現することができました。 しかし、1door2lock 構成時はひとつのドアに対してふたつの SmartLock を個別に扱う必要があります。 ユーザーから見ればひとつの家電に対する操作なのに、内部的には main/sub の SmartLock をそれぞれ呼び出すことになります。

実装上はそれでも問題なく動くでしょうが、これはユーザーから見えるモノとは異なってしまいます。 当然、開発チーム内でもひとつの「家電」を想定してコミュニケーションをとることになります。 会話の中で登場する言葉とモデルに乖離があるということは、よいモデリングとは言えないでしょう。

また、「1door2lock 構成時は解施錠操作時にふたつの SmartLock を同時に操作する」責務をアプリケーション層が担っており、 ドメイン知識が流出しているという問題もありました3。 それが1箇所ならまだなんとかなりますが、実際には複数個所でこの制約を守る必要があり、 sub の操作を呼び忘れる、といった実装漏れのリスクもありました4

ここで再び SmartLock 集約を見直すことにしました。

図5 SmartLockUnit の導入

SmartLock の子要素として、SmartLockUnit という概念を導入しました。 これは 1door2lock 構成時の「メインユニット」「サブユニット」を表すものです。 家電としてのエンティティを操作5する場合は SmartLock を、デバイスとしてのエンティティを操作する場合は SmartLockUnit を対象とします。 SmartLock には Appliance が、SmartLockUnit には Device が内包されているため、 家電・デバイス共通のエンティティ操作を行う場合は、SmartLock から内包されたこれらの集約にデリゲートすれば済みます。

役割 操作の対象 具体的な操作例
家電 SmartLock 解施錠、通知設定の変更など
デバイス SmartLockUnit FOTA、解施錠位置の調整など

コードだとこうなります。

// SmartLock は集約ルートとして Appliance および main/sub の SmartLockUnit を保持する。
// 1door2lock 構成では main/sub の SmartLockUnit が同一 Appliance を共有する。
type SmartLock struct {
    appliance Appliance
    main      SmartLockUnit
    sub       *SmartLockUnit
}

// SmartLockUnit は単一の物理 SmartLock デバイスを表す。
type SmartLockUnit struct {
    id   SmartLockID

    // device は SmartLockUnit そのものを表す Device
    device Device

    // controllerDeviceID は SmartLock を Appliance として操作する際のコントローラーとなる Device の ID
    // Device と紐づけられていない場合は nil
    controllerDeviceID *core.DeviceID

    position Position
    role     Role

    // remoteOperationEnabled はリモート解施錠操作が有効かどうかを表す。
    // false の場合リモート操作不可。
    // controller device を経由したその他の処理(状態の同期など)には影響しない。
    remoteOperationEnabled bool

    // autoLockEnabled はオートロックが有効かどうかを表す。
    autoLockEnabled bool
}

家電として SmartLock を操作する場合は、そのメソッドを呼出し、内部で SmartLockUnit の非公開メソッドを呼び出します。 これにより、「sub の操作を呼び忘れる」事がなくなりました。

// BindToControllerDevice は SmartLock を controller device に紐づける
// 1door2lock 構成の場合は main/sub ともに同じ controller device に紐づける
func (s *SmartLock) BindToControllerDevice(deviceID core.DeviceID) {
    s.main.bindToControllerDevice(deviceID)
    if s.sub != nil {
        s.sub.bindToControllerDevice(deviceID)
    }
}

図らずも SmartLock の構成は(メンバーだけを見ると)仮想デバイスを導入していた最初の構造と一致することになりましたが、 SmartLock が内包する ApplianceDevice の関係が洗練され、より表現したいモノに近づいています。

・・・今となってはなぜ「仮想デバイス」のような存在しないモノを採用しようとしたのか、自分でもわかりません。 対象に対する理解が進むと、以前の理解の仕方が不自然にしか思えないことがしばしばありますが、 ドメインモデリングについても、というよりドメインモデリングではより顕著にその傾向が現れるように思います。

余談: 家電から見た Remo の呼び方

ところで、「家電を操作するためのデバイス(= Remo)」には、いままで特定の呼び名はついていませんでした。 今回スマートロックを開発するにあたり、ひとつの家電にスマートロックそのものを表すデバイスと、紐づけることでリモート操作を可能にするデバイス(= Remo)の2種類が関連することになります。

この2種類のデバイスを呼び分けるために、「家電を操作するためのデバイス」を「コントローラーデバイス」と呼ぶことにしました。 扱う概念が増加し、より解像度高く対象を認識するために、既存のなんとなく認識していた概念に名前がつけられた例と言えるでしょう。

まとめ

スマートロックの開発を進めるにつれて、製品への理解が深まり、その結果としてドメインモデルが洗練されていった様子を紹介しました。

Nature のサーバーサイドアプリケーションでは、既存のコードについてもドメインモデリングを進めながら継続したリファクタリングを行っています。 業務ドメインの理解を進め、アプリケーションに落とし込むことは、業務に対する曖昧さを是正する効果があります。 実態に合ったモデルには、チーム内の会話と実装の乖離を防ぎ翻訳コストを減少させるだけでなく、 ビジネスロジックの複雑さを低減させ、不具合を減らす効果もあるでしょう。

スマートロックについてはひとまずドメインモデリングを完了した状態にはなりましたが、 今後機能の追加や新しい製品が登場した際には、再度最適なモデルを検討し直すことになるでしょう。 ビジネスが変化していく限り、それを実現するためのモデリングは終わりません。

最近ではコードのほとんどを AI エージェントが書くようになっています。 アプリケーションの根幹であるドメインモデリングを丁寧に行えば、AI が生成するコードの精度向上が期待できます。 常に正しいドメインオブジェクトしか存在できないようにすれば、AI はもとより人間の実装ミスを減らすことができます。 各値に丁寧に型を定義し、それらについて説明のコメントを加えることで、コードを読ませるだけでコンテキストが伝わる状態を作ることができます。 ドメイン層の設計は、AI が普及しても人間が行い責任を持つべき重要な仕事だ、というのが私の持論です。


Nature では一緒に働く仲間を募集しています!

まずはカジュアル面談から、お気軽にどうぞ。


  1. 以降単に「サーバー」と書いた場合は、Nature のサーバーを指すものとします
  2. FOTA 可否の判定・実行など
  3. ドメインサービスとする手もありますが、ひとつのエンティティとして表したほうが会話に登場する言葉とも一致するので、今回は集約ルートを見直す方法を採用しました。
  4. 開発中は、実際この呼び忘れが原因で不具合も発生していました。
  5. 家電の操作と紛らわしいですが、ここではアプリケーションコード内のエンティティに対する操作のはなしをしています。