人間のあるべき姿の探索

思索・人形・エンジニアリング

角度・位置の計算における値オブジェクトのすゝめ

はじめに

ロボットの姿勢やUnityオブジェクトを扱う時、その数値の種類は様々です。例えば、長さをメートルで扱うか、センチメートルで扱うか。角度については度数法で表現したりラジアンで表すこともあります。しかし、こういった数値を扱う際、結構ありがちなミスとして、既にラジアンで得られている値にToRadメソッドをかけてしまいとても小さな値になってしまった、といったことがあります。変数名をheadYawiInRadiansみたいにラジアンであることを明記すると良さそうですが、実際この値が正しいかどうかは変数を使うコードの都合で簡単に崩れていきます。

そこで、次善策として、型の力を使いましょう、というのが今回の趣旨になります。勿論使い方によっては不正な値が入ってしまいますが、型を定義して包んであげることで少しはマシになるという話です。

値オブジェクトとは

値オブジェクトと呼ばれる対象は3種類あるようですが、DDDにおける値オブジェクトをイメージしています。PoEAAについて知らないのですが、今回必要な性質としてImmutableであることと独自のクラスを定義していること、辺りがあれば十分です。

panda-program.com

 

例えば度数法で扱う値は以下のように定義できます。これによって、この型で宣言された値を使用している限りは度数法で扱っていることが(あくまで運用ベースで)保証されます。何かしらの計算に値を渡すときなど、必要なタイミングでラジアンに変換してあげることで、floatやdoubleで扱われていた値がどんな意味を持つのかより精緻に表現することができ、コードを読み書きする際に分かりやすくなります。



値オブジェクトの基礎として、数値型の場合は加算の演算子や同値判定を実装してあげましょう(かなり大雑把に書いてますが…)

また、DDDにおけるテクとしてドメインサービスというものがあり、使いどころはかなり議論がありますが二つ以上の値オブジェクトや(割愛しますが)エンティティが登場する場合等で使用します。今回のラジアンへの変換については試しに書いてみましたが、相互に依存してしまうことを考えるとドメインサービスが両方のクラスに依存する方が綺麗な気はしますね。依存関係が一方向に揃った方が嬉しくなります。

上のメソッドを見ると実は濃度が異なるだけで、度数法とラジアンは質としてはそれほど変わらないです。しかし、他の例を考えてみるとロボットの頭部のYaw軸は-90~90度で表現したいが、サーボモータは値域が0~180度でこのマッピングをしたい、みたいな事例があります。この場合Degree型とServoAngle型の値オブジェクトをそれぞれ定義してあげるのですが、仮にこれを忘れて値をそのまま流してしまうと、頭部が正面を向いているはずなのにサーボモータに渡した値としては真横を向いてしまっている、といったケースもあります。

角度計算の説明をしっかり目にしたので位置の計算はかなり割愛しますが、値域を考えるときにCentiMeter型とMeter型を用意してあげるイメージで上記コードに当てはめてもらえると、イメージが湧くのではないかと思います。より込み入った話になると座標系が変わって極座標で扱いたい場合とかにユークリッド座標系と併せて型定義しておくと扱いやすかったりするのではないでしょうか(ファクトリメソッドなど省略しますが、以下の感じで実装できると思います)

Limitation

パフォーマンスの低下

実際のC#コードを例にとって説明したのですが、値オブジェクトは同値性判定をそのフィールドやプロパティによって行います。要するに、変数を使いまわさず、都度インスタンス化します(これがDDDの文脈におけるエンティティとの違いになります)。よって、値が頻繁に変わり、かつリアルタイム性がシビアな要件においては、このインスタンス化が時間的なボトルネックになる可能性があり、それこそ遅延10ms以内でかつ重い計算をするようなケースにおいては難しいと思います。同様に、例えばArduinoで値を扱う場合はメモリ消費を抑えるために可能な限り変数を事前に定義して使いまわすことがあり、こういったリソースが限られる場面ではオーバーヘッドの影響が強く出てしまいます。

失敗するパターン

前述したとおり、次善策であり値オブジェクトのみで値域の検証等が完全に保証されるわけではありません。分かりやすい例としては、最初にセンサから取得した値がfloat型で与えられた際に、その値がラジアンなのにDegree型でインスタンス化してしまった場合です。一度定義された値オブジェクトの世界に入ればその後は型を見て判断できますが、その前後は不正が混入する余地があります。一度値オブジェクトに渡した値でも、例えば他のプロセスに送る際にJSONに変換してNumber型に統一されてしまう、といったケースも存在します。

勿論、明らかにおかしい値(ServoAngle型で負値を入れる)やインスタンス化する際の引数の型としておかしい(角度なのにstring型の値を入れる)場合はチェックして弾けるのですが、これらをすり抜けるケースは存在する懸念があるわけです。

では、意味がないのか?というとそうではなく、ここまでは型の恩恵を受けていない、ここから先は型の恩恵を受けているといった境界線を明確にすることが大切です。最初にセンサ値が入ってくるタイミングではセンサの仕様を確認したうえでその型を決めてやる必要がありますが、その値を処理したり変換する過程では型を見れば良くて、JSONに変換して他のプロセスや他のサービスに送る際はその双方のサービスで値の仕様を決めておく、といった切り分けができるようになります。

全てが完全に定義できてうまくいく、といった都合の良いコーディングは不可能なので、最善の手を考慮してうまく立ち回っていくことが必要で、その選択肢として値オブジェクトがある、といった立ち位置なのではないかと思います。

おわりに

値オブジェクトの使いどころについて説明してみました。僕はDDDにおける値オブジェクトしか知らず、その文脈で話してみたのですが、値オブジェクトの性質自体は今回のようにDDDのアーキテクチャの中に組み込むことを前提としなくても一つのテクとして活用するだけでも効果があるのではないか、と思います。僕はせっかくなのでロボットのコードを書く際にInfrastructure層を定義したりレイヤードアーキテクチャを採用してそのメリット・デメリットを実感していますが、やはりDDDの課題としてその厳密性とがパフォーマンスをはじめとした現実のワークロードに則さない美しさになってしまう点があります(N+1問題等が有名ですかね)。そういった部分を鑑みて、今回のように一つのテクとして値オブジェクトを活用するシーンを探してみてはいかがでしょうか。