🦚

UI開発は「構造を書く」時代へ ~ Structive に見る、もう一つの設計思想 ~

に公開

はじめに

現代のフロントエンド開発は、コンポーネント指向やデータフロー管理といった考え方を主軸に進化してきました。複雑なUIを部品に分割し、データの流れを管理可能な形にすることで、大規模アプリケーション開発の効率は飛躍的に向上しました。

しかし、多くの開発者が共通して感じる課題も存在します。例えば、UIと状態(データ)の同期や、特にネストしたリストや計算値を含むデータの表示・更新に伴うコードの煩雑さ、フレームワーク特有のボイラープレートなどです。これらの課題は、UIの「見た目」や「振る舞い」を記述するコードの中に、データの加工や同期のための手続き的なロジックが多く混在することで発生しがちです。

このような状況に対し、Structive は既存の多くのフレームワークとは 「見ているものが違う」、すなわちUI開発に対する根本的に異なるアプローチを提示します。

「見ているものが違う」:構造への焦点

他のフレームワークがアプリケーションを「コンポーネント(部品)の組み合わせ」や「データの流れ」といった観点から捉えるのに対し、Structive はアプリケーションの「構造」そのものを最も重要な概念として捉えます

ここでの「構造」とは、UIの要素のネストや繰り返しといったUI構造、そして状態(データ)が持つオブジェクトのネスト、配列、計算値の配置といったデータ構造の両方を指します。

Structive の設計思想の核にあるのは、「UIは状態(データ)の構造を映し出す鏡であるべきだ」という考え方です。そして、この二つの構造を直接結びつけるための基盤となる、Structive独自の「言葉」が導入されます。それが「構造パス」です。

アプリケーションを「構造パス」で語る

では、「構造パス」とは具体的に何でしょうか?

構造パスとは、階層的なデータ構造やUI構造の中の特定の位置、またはパターンを文字列で表現したものです。

ファイルシステムのパス(例: /users/documents/report.txt)がファイルやフォルダの場所を示すように、構造パスはStateオブジェクトの中のデータや、UIテンプレートが描画する構造の中の特定の要素の場所を示します。JSONデータを扱う際のJSONPathや、XMLを扱う際のXPathに近い概念ですが、StructiveではこれがUIと状態を結びつける最も重要な、そして統一された「言葉」 として機能します。

構造パスは、主に以下の要素を組み合わせて構成されます。

  • プロパティ名: オブジェクトのプロパティやネストしたオブジェクトを辿る際に使用します。. (ドット) で区切ります。
    • 例: user.profile.name → State内の user オブジェクトの profile プロパティにあるオブジェクトの name プロパティを指します。
  • ワイルドカード *: 配列のような「繰り返し」構造の中の「現在の要素」全体を示します。Structiveが {{ for: }} で配列を繰り返す際に、そのループ内で現在処理しているアイテムを指すのに使われます。
    • 例: items.*.valueitems という配列を繰り返している際に、そのループで処理されている「現在のアイテム」の value プロパティを指します。
  • コンテキストインデックス変数 $1, $2, ...: ネストした繰り返しなどで、複数のワイルドカードがある場合に、外側の繰り返し構造のコンテキストにある要素のインデックスを指します。最も外側の * に対応するのが $1、その内側が $2 となります。
    • 例: 地域リストの中の州リストを繰り返す場合({{ for:regions.*.states }})、州のゲッター内 ("regions.*.states.*.share") で $1 は現在の地域オブジェクトのインデックスを指し、$2 は現在の州オブジェクトのインデックスを指す、といった使い方がされます。

Structive は、この構造パスをアプリケーション全体で共通の「言葉」として使います。特に、UIテンプレートにおける要素の記述は、この構造パスと、それを利用するためのいくつかのシンプルな構文によって成り立っています。

UIテンプレートを構成する「構造パス」と基本要素

Structive のUIテンプレートは、HTMLの構造をベースに、主に以下の要素を構造パスと組み合わせて記述します。これらの要素は、Stateクラスが提供する「構造」を、どのようにUIとして表現するかを宣言します。

  • 埋め込み (Interpolation): {{ 構造パス }}
    指定した構造パスが示すを、HTMLのコンテンツとして表示します。Stateのプロパティの値、あるいはゲッターの計算結果など、そのパスに存在するあらゆる値が対象となります。
    例: <h1>こんにちは、{{ user.name }}さん!</h1>

  • for ブロック (リスト繰り返し): {{ for: 構造パス }} ... {{ endfor: }}
    指定した構造パスが示す配列を繰り返し処理し、ブロック内のコンテンツを配列の要素ごとに描画します。繰り返しブロック内では、ワイルドカード * を含む構造パスが現在の配列要素を指します。
    例:

    <ul>
      {{ for:items }}
        <li>{{ items.*.text }}</li> {{!-- 現在のitems配列の要素のtextプロパティを表示 --}}
      {{ endfor: }}
    </ul>
    
  • if ブロック (条件分岐): {{ if: 構造パス }} ... {{ :endif }}
    指定した構造パスが示す真偽値 (boolean) の状態に基づいて、ブロック内のコンテンツを表示するかどうかを切り替えます。パスの値が true なら表示、false なら非表示になります。{{ else: }}と組み合わせることも可能です。
    例:

    {{ if:isLoading }}
      <p>ローディング中...</p>
    {{ endif: }}
    {{ if:errorMessage }}
      <p style="color: red;">{{ errorMessage }}</p>
    {{ endif: }}
    
  • 属性バインド (Attribute Binding): data-bind="属性名: 構造パス" および data-bind="イベント名: メソッド名"
    State の構造パスが示すを、HTML要素の様々な属性 (id, class, style, value, checked, disabled など) に動的に結びつけます。また、イベント(click, input など)とStateクラスのメソッドを結びつけることもできます。複数のバインドをセミコロン ; で区切って指定できます。
    例:

    <button data-bind="disabled: isSaving; onclick: saveProfile">保存</button>
    
    <input type="text" data-bind="value: user.profile.name">
    
    <div data-bind="class.active: isActive">...</div> {{!-- isActive が true なら class="active" が付く --}}
    

これらの要素と構造パスを組み合わせることで、UIテンプレートは「Stateクラスで定義された『どういう構造があるか』を、どのように画面上に『表現するか』」という、表示の論理を非常に簡潔に宣言できます。データの取得や更新、繰り返し処理、条件による要素の生成/破棄といった煩雑な手続きは、Structive が構造パスを解釈して裏側で自動的に行います。

コードの中心は「どういう構造があるか」

Structive でコードを書く際の中心は、「どうする」(手続き:DOMをどう操作するか)や「何をする」(結果:この状態なら最終的にこうなる)という側面に加えて、 「どういう構造があるか(どういう構造であるべきか)」 という側面に強く置かれます。そして、その「構造」を記述するための主要なツールが構造パスなのです。

開発者の主なタスクは以下のようになります。

  1. Stateクラスで「どういうデータ構造があるか(であるべきか)」を定義する:

    • アプリケーションが扱う生データや、そこから派生する計算値、加工されたデータが、どのようなプロパティ名、ネスト、配列を持つべきか、そして構造パスでどのようにアクセス可能であるべきかを定義します。
    • Stateクラスが単なるデータ保持庫ではなく、UIが必要とする構造(UI構造に一致したデータ構造)を提供する 「構造のプロバイダー」 としての責任を担います。
    • ここで重要なのが、スコープ付きゲッターという仕組みです。これは、計算結果や加工済みデータが、データ構造内の特定の構造パスに紐づく「仮想的なプロパティ」として 「存在すること」 を宣言します。これは、計算というロジックをデータ構造そのものに組み込む、Structive独自の構造化のアプローチです。ゲッターの名前自体が構造パスになります。
    class State {
      // 生データ構造(プロパティ名も構造パスの一部)
      users = [{ name: "Alice", age: 30 }, { name: "Bob", age: 25 }];
      isLoading = false;
      selectedIndex = -1; // 現在選択されているユーザーのインデックス
    
      // スコープ付きゲッターによる計算値(構造パスに対応する仮想プロパティ)
      // "users.*.isAdult" というパスに各ユーザーの成人判定結果が存在する
      get "users.*.isAdult"() {
        // this["users.*.age"] は現在のユーザーの年齢を指す
        return this["users.*.age"] >= 18;
      }
      // "users.*.selected" というパスにユーザーの選択状態が存在する
      get "users.*.selected"() {
        // this.$1 は現在のユーザーのインデックス
        return this.$1 === this.selectedIndex;
      }
    }
    
  2. UIテンプレートで「Stateのどういう構造の、どのパスを参照するか」を記述する:

    • UIテンプレートは、Stateクラスで定義された「どういう構造があるか」を、構造パスを使って参照することで、自身の表示構造や内容を決定します。
    • {{ user.profile.name }} (Stateの user.profile.name というパスの値を表示)
    • {{ for:users }} (Stateの users というパスが示す配列を繰り返す)
    • {{ if:isLoading }} (Stateの isLoading というパスが示す真偽値に応じて表示を切り替える)
    • data-bind="value:users.*.name" (Stateの users.*.name というパスの値とinput要素のvalueを同期)

このように、Structive では、まずStateで 「どういう構造が存在するか(存在するべきか)」を定義し、UIテンプレートはそれを構造パスを使って「読む」 ことに徹します。開発者は、データアクセス、リストの繰り返し、条件分岐、データバインディングといった手続きを記述する代わりに、データとUIの「構造」と、その間の「関係性(どの構造パスがどのUI要素に対応するか)」 を、構造パスという共通言語で宣言することに集中します。

構造パスによるUIと状態の「直結」と、きめ細やかな更新

UIテンプレートは、上記のようなシンプルな構文で構造パスを指定することで、Stateクラスとの「直結」を実現します。Stateクラスで定義されたデータやゲッターが、構造パスを介して直接UIテンプレートに提供されます。

この「直結」は、Structive が構造パスを 「第一級オブジェクト」 のように扱っている点に支えられています。構造パスは単なる文字列のラベルではなく、フレームワークが深く理解し、内部でデータ構造を辿る際の道標となり、データの変更を追跡し、必要なUI部分だけを効率的に更新するための核として機能します。パスを指定するだけで、フレームワークが自動的にデータへのアクセス、ゲッターの実行、UIの更新を行ってくれるのです。

そして、この構造パス単位でのバインディングが、UI更新の粒度を非常に小さく保つことを可能にしています。

Structive のリアクティビティシステムは、State のどのパスのデータが、UIテンプレートのどのパスで参照されているかを直結なので正確に追跡できます。Stateの特定のパスの値(またはそのパスに依存するゲッターの結果)が変化した場合、Structive はアプリケーション全体を再描画するのではなく、その変化したパスに直接バインドされているUI要素だけをピンポイントで更新します。

例えば、Todoリストの例で、特定のTodoアイテムの isCompleted という真偽値だけが変化したとします。

  • Structive は todos.*.isCompleted というパスでその変更を検知します。
  • UIテンプレートの中で todos.*.isCompleted を参照している要素(例: チェックボックスの checked 属性や、完了済みを示すテキストの表示/非表示など)だけが、Stateの変更を反映して更新されます。
  • 同じリスト内の他のTodoアイテムや、リスト外の他の要素は、その変更に直接依存していなければ一切更新されません。

これは、UI更新の粒度がコンポーネント単位や、さらに大きなUIツリー全体になる場合に比べて、無駄な処理が少なく、より効率的なUI更新に繋がります。Structive は、データ構造のどこが変化したかを構造パスで特定し、UIのどこを更新すれば良いかを構造パスで特定するという、構造パス単位でのきめ細やかな依存追跡とUI更新を実現しているのです。

構造パスがもたらす革新

この「構造を書く」というアプローチと、それを可能にする構造パスの仕組みは、UI開発に以下のような革新をもたらします。

  • 圧倒的なコードの簡潔さ: 階層的なデータアクセスや繰り返し、条件分岐、バインディングといった多くの定型的なタスクが、構造パスの指定だけで済み、多くのボイラープレートが排除されます。
  • 高い可読性と意図の明確さ: コードが「どうやるか」という手続き的な記述から解放され、「どのような構造のデータが、UIのどの構造に対応しているか」という、本質的な意図が構造パスを通じて直接的に伝わります。コードがまるでUIとStateの「仕様書」であるかのように読めます。
  • 構造的な美しさと一貫性: UIとStateの構造が意図的に一致され、パスという一貫したルールで結ばれているため、コード全体に構造的な美しさや整合性が生まれます。
  • 「状態に書いたこと」が「そのままUIに」: Stateクラスでゲッターとして定義した計算結果や加工済みデータが、UIテンプレートからはパスを介してそのまま利用できます。StateクラスをUIのための強力な「構造のプロバイダー」とする核心です。

もう一つの設計思想が切り拓く未来

Structive は、単に新しい構文やAPIを提案するだけでなく、「UI開発とは、いかに構造を設計し、構造パスでUIと状態を結びつけ、構造をコードの中心に置くか」という、既存のフレームワークとは異なる 「もう一つの設計思想(世界観)」 を提示しています。

ReactやVueなどが得意とする領域があるように、Structive は特に、構造化されたデータを扱い、それをUIに効率的かつ宣言的に表示・更新するシナリオにおいて、その「構造を書く」というアプローチの真価を発揮するでしょう。この構造的なコードは、開発支援ツールとの相性も良く、将来的な開発スタイルを先取りする可能性も秘めています。

UI開発における新しい視点、コードの驚くべき簡潔さ、そして構造的な美しさに魅力を感じるならば、Structive が切り拓く「構造を書く」時代は、非常に興味深く、試してみる価値があると言えるでしょう。

後記

構造駆動テンプレートを採用したフロントエンドフレームワークの名前をStructiveと決定しプロジェクトを開始しました。

https://zenn.dev/mogera/articles/408a0efd4cf78e/

https://github.com/mogera551/Structive

Discussion