To download the app, visit: [tulip](https://<span translate=).co/library/apps/unit-converter--temperature/">Library
この記事では、当社のライブラリ開発者の一人であるタマシュが、ライブラリで利用可能なビルディングブロックアプリ「単位変換ツール:温度」の開発における判断基準について解説します。
なぜテーブルではなくオブジェクトリストを採用したのか
こんにちは、Tulipのタマスです。
このコンポーネントでは、データセットがめったに変更されず、テーブルベースの管理を必要としないため、デフォルトの変数値として保存されたオブジェクトリストを使用することにしました。
この場合、温度の換算係数やオフセットは静的な数学的関係です。 これらは、実行時の編集、集計、またはリレーショナルクエリを必要としません。そのため、私は意図的にテーブルの使用を避けました。代わりに:* 変換定義を変数内に格納する* 変数はデフォルト値で初期化される* オブジェクトリストは、構造化され読みやすいデータビューを提供する* テーブルの集計ロジックは不要
このアプローチにより、コンポーネントは軽量かつ自己完結型を維持できます。
なぜテーブルを使わないのか?
テーブルを使用すると、以下の課題が生じます:* アプリに追加のテーブルを追加する必要が生じる* そのテーブルに対するクエリを作成する必要が生じる* そのクエリの集計ロジックを設定する必要が生じる* アプリ内でそのクエリと集計ロジックを追加・維持する必要も生じる
めったに変更されないデータセットの場合、そのようなオーバーヘッドは不要です。
変換定義を変数内のデフォルト値として保存することで:* コンポーネントは完全に移植可能になる* アプリをコピーすると、データ構造もコピーされる* 外部への依存関係は不要
オブジェクトリストを使用することで、リレーショナルなロジックを必要とせずに、関連する属性(inputUnit-targetUnit、factor、offset)を整理された構造で明確に把握できます。
設計原則:保護されたロジック、柔軟な設定
このコンポーネントは、1つの重要な原則に基づいて設計されました:
中核となるロジックは、決して変更を必要とすべきではない。
これを支えるため、内部コンポーネント変数を導入し、先頭にアンダースコアを付けて識別しました(例:_temperatureInputUnit)。これらのアンダースコア付き変数は、保護されたコアロジックが依存できる安定した内部インターフェースとして機能します。
**トレードオフ:**内部変数を使用するには、2つの小さな設定トリガーが必要です:* 01 – ユーザー設定:入力ステージ(INPUT STAGE)が外部ソースを内部変数にマッピング* 03 – ユーザー設定:出力ステージ(OUTPUT STAGE)が内部結果を目的の宛先にマッピング
これにより、保護されたエンジンの周囲に薄い設定レイヤーが追加されますが、MAGICトリガーには手を加えず、コピー&ペーストが容易な状態を維持できます。
別の選択肢
より単純ですが、保守性が低いアプローチとしては、外部参照をコア式に直接貼り付ける方法があります(たとえば、アプリ変数やtable record field プレースホルダーをインラインで使用するなど)。
これにより、余分な設定トリガーは回避できますが、移植性が低下し、コピーするたびにコアロジックを編集する必要があるため、再利用が難しくなります。
コンポーネントの配布に関しては、内部変数インターフェースのパターンの方が管理が容易です。入力と出力はエッジで設定され、計算エンジンは安定した保護された状態を維持します。
MAGIC トリガーはルックアップを実行し、アフィン変換を適用します。
result = (value × factor) + offset
変換の定義は内部オブジェクトリストに一元化されているため、このトリガーは変更を加えることなく複数のケースを処理できます。
この実装では:* °C、K、°Fがサポートされています* すべての3×3の組み合わせが事前定義されています* ロジックが正しい係数とオフセットを動的に算出します
アプリビルダーが行う必要があるのは、以下の2点のみです:1. 入力ソースの設定2. 出力先の設定
数式の書き換えは不要です。条件分岐ロジックの編集も不要です。構造の変更も不要です。
スケーラビリティの理由
この構造により、以下が可能になります:* オブジェクトリストを拡張して新しい単位を追加* 計算エンジンを変更せずに済む* アプリ間のアーキテクチャの一貫性を維持* 複数のプロジェクト間で迅速に再利用
このコンポーネントは以下を分離しています:* データソースの設定(入力ステージ)* 実行ロジック(MAGIC)* 結果のルーティング(出力ステージ)
この分離により、システムは安全で、予測可能、かつスケーラブルな状態が保たれます。
トリガーの内訳 – MAGIC(式)の仕組み
保護されたMAGICトリガーは、単一の式として実装されています:(@Variable._temperatureInputValue * array_value_at_index( map_to_number_list(@Variable._temperatureConversionDefaults , 'factor'), array_index_of( map_to_text_list(@Variable._temperatureConversionDefaults , 'inputUnit-targetUnit'), @Variable._temperatureInputUnit + '-' + @Variable._temperatureTargetUnit )))+array_value_at_index( map_to_number_list(@Variable._temperatureConversionDefaults , 'offset'), array_index_of( map_to_text_list(@Variable._temperatureConversionDefaults , 'inputUnit-targetUnit'), @Variable._temperatureInputUnit + '-' + @Variable._temperatureTargetUnit ))大まかに言えば、この式は次の3つのステップを実行します:
1. ルックアップキーの構築
キーは、設定された入力変数から動的に構築されます:
inputUnit-targetUnit
例:
°C-°F
これにより、ロジックが汎用性を保ち、サポートされているすべての組み合わせで機能することが保証されます。
2. オブジェクトリストから係数とオフセットを解決する
この式は、1回の処理で係数とオフセットを解決します:***map_to_text_list(..., 'inputUnit-targetUnit')**は、その場でルックアップキーのリストを構築します。***array_index_of(...)**は、一致するキーのインデックスを検索します。***array_value_at_index(...)**は、そのインデックスを使用して、係数リストおよびオフセットリストから対応する値を取得します。
ここでの重要な設計上の選択は、array_index_of(...) を map_to_text_list(...) と直接組み合わせることです。これにより、中間キーリストを別の変数に格納する必要がなくなります。その代わりに、ルックアップリストはインラインで生成され、単一の式内でファクターとオフセットの両方を取得するために即座に使用されます。
これにより、MAGICトリガーは自己完結性を保ち、設定の複雑さを軽減しつつ、テーブルや集計ロジックを必要とせずに、構造化されたルックアップと同様の動作を実現します。
3. アフィン変換の適用
正しい係数とオフセットが特定されたら、計算が適用されます:
result = (value × factor) + offset
ルックアップロジックはインデックスベースかつデータ駆動型であるため、このトリガーでは条件分岐やswitch-case構造を必要としません。
この構造が選ばれた理由
- ハードコーディングされた条件分岐ロジックを回避できるためです。
- 新しい単位の組み合わせが追加されても、自然に拡張できます。
- 計算を完全にデータ駆動型に保つことができます。
- 複数のトリガーではなく、単一の保護された式を維持します。
この設計により、コンポーネントは決定論的かつ拡張性が高く、意図しないロジックの変更から保護されます。
新しい単位による拡張
新しい単位を追加するには、_temperatureConversionDefaults を拡張し、以下を含む新しいエントリを追加します。* inputUnit-targetUnit (キー)* 係数* オフセット
新しいマッピングがオブジェクトリストに存在すると、保護された MAGIC トリガーがそれを自動的に解決できます。コアロジックの変更は必要ありません。
失敗時の処理(安全なルックアップパターン)
MAGIC トリガーには、変換ペアが見つからない場合に安全な実行を保証するための明示的な失敗検出機能が含まれています。トリガーは、結果が空白であることを頼りにするのではなく、ルックアップインデックスを直接チェックします:
ARRAY_INDEX_OF( MAP_TO_TEXT_LIST(_temperatureConversionDefaults, 'inputUnit-targetUnit'), _temperatureInputUnit + '-' + _temperatureTargetUnit )
返されたインデックスが -1 の場合、一致する変換ルールは存在しません。
実行フロー:1. トリガーの開始時に _temperatureConversionResult をクリアします。2. ルックアップインデックスが -1 の場合 → 計算を行いません。3. それ以外の場合 → 計算を行い、結果を格納します。
その後、OUTPUT ステージで以下を確認します:
_temperatureConversionResult が空白の場合 → エラーメッセージを表示します。
メッセージ例:
「選択された単位の組み合わせに対応する変換ルールが見つかりませんでした。単位の形式(°C、K、°F)を確認し、その組み合わせが _temperatureConversionDefaults に存在するか確認してください。」
これにより、以下のことが保証されます:• 以前の実行から古い値が残らない • 有効なルールがない場合は計算が試みられない • ユーザーには明確で具体的な対応が可能なエラーメッセージが表示される
中核となるロジックは保護されたまま変更されず、確定性があり安全な動作が維持されます。
キー、係数、オフセットを別々の配列に分けないのはなぜですか?
別のアプローチとして、以下のように格納することも考えられました:* ルックアップキーを 1 つの配列に* 係数を別の配列に* オフセットを 3 つ目の配列に
技術的には機能するものの、構造的なリスクが生じます。つまり、各配列間の位置関係は常に完全に一致していなければならないのです。
ある配列の順序が変わり、他の配列の順序が変わらない場合、キー・ファクター・オフセットの関係が黙って崩れてしまい、誤った変換が生じることになります。
関連する属性をオブジェクトリスト内にまとめて格納することで、各エントリはinputUnit-targetUnit、係数、オフセットの値を論理的にグループ化した状態を維持できます。これにより、位置のずれが生じるリスクが軽減され、構造の安全性が高まり、メンテナンスも容易になります。
参加しよう
コミュニティに参加して、改善案の共有、追加単位の提案、問題の報告、あるいはアーキテクチャ上の決定事項について議論しましょう。
