Model と View を分離する
ここまでのサンプルでは主に表示機能について説明してきましたが、Alier のアプリケーションアーキテクチャでは基本的に
- そのアプリケーションが実現しなければならない本質的な機能を実装する Model
- ユーザーが Model の機能を操作するための手段を実装する View
の2つの部分で構成されます。Alier フレームワークでは、このアーキテクチャの標準的な実装をサポートする機構を提供しています。この節では、簡単なプログラムを例にその使い方を学んでいきましょう。
今回はボタンを押すと数をカウントするカウントアップアプリケーションを作成します。このようなシンプルな機能であればアーキテクチャを意識しなくてもアプリケーションを作れますが、今回のテーマのために Model を定義してみましょう。
このアプリケーションではカウントアップを実現することが本質機能と言えます。ここではカウント数を保持し、何らかのイベントに応じてカウント数を更新し、更新した値を View 側に通知する、そんな Model を作れば良さそうです。
Model を実装する際には Model にアクセスするためのインターフェースを明確にしておくと良いでしょう。Alier ではモデルインターフェースを定義する機能も提供されています。
View では、以下の処理を実装します。
- ボタンを表示
- ボタンイベントを受けて Model インターフェースを介してカウントアップを通知
- Modelからカウント数の更新のイベントをコールバック等で通知されたら表示に反映
それではプログラムを見てみましょう。
ソースコード
- /app_res/main.js
Object.assign(globalThis, await Alier.import("/alier_sys/AlierFramework.js"));
Object.assign(globalThis, await Alier.import("counter_view_logic.js"));
async function main() {
setupAlier();
await setupModelInterface({ xml: "counter_model.xml" });
Alier.View.attach(new CounterView());
}
- /app_res/counter_model.js
class CounterModel extends AlierModel {
#count = 0;
onChange = new MessagePorter();
constructor() {
super()
return this.initialize();
}
countUp() {
this.setCount(this.#count + 1);
}
reset() {
this.setCount(0);
}
setCount(num) {
this.#count = num;
this.onChange.post(Alier.message("onChange", "", { count: this.#count }));
}
}
await Alier.export({ CounterModel });
- /app_res/counter_model.xml
<model>
<import path="counter_model.js"/>
<interface class="CounterModel">
<function name="countUp" path="countUp" locale="local"/>
<function name="reset" path="reset" locale="local"/>
<message-porter name="onChange" path="onChange" locale="local"/>
</interface>
</model>
- /app_res/counter_view_logic.js
class CounterView extends ViewLogic {
constructor() {
super();
this.relateElements( this.collectElements( this.loadContainer({ text: `
<alier-container>
<input type="button" id="counter" data-ui-component data-active-events="click" />
</alier-container>
` })));
Alier.Model.onChange.addListener(msg => this.post(msg));
Alier.Model.reset();
}
async messageHandler(msg) {
return await msg.deliver({
counter : msg => Alier.Model.countUp(),
onChange : msg => this.counter.value = `count: ${msg.param.count}`
});
}
}
await Alier.export({ CounterView });
以下は実装結果です。
起動画面
ボタンを押下
解説
エントリーポイントの実装
Object.assign(globalThis, await Alier.import("/alier_sys/AlierFramework.js"));
Object.assign(globalThis, await Alier.import("counter_view_logic.js"));
async function main() {
setupAlier();
await setupModelInterface({ xml: "counter_model.xml" });
Alier.View.attach(new CounterView());
}
エントリポイントの実装です。
今までは表示処理のみでしたが、今回は setupModelInterface
という関数を呼んでいます。
このメソッドは引数に渡した XML ファイルをパースしてモデルインターフェースオブジェクトを作成します。インターフェース定義した Model クラスのインスタンスもここで生成されます。作成されたオブジェクトは Alier.Model
の下に割り当てられ、アプリケーション全体からアクセスできるようになります。
Model を実装する
class CounterModel extends AlierModel {
#count = 0;
onChange = new MessagePorter();
Model の実装です。AlierModel
を継承した CounterModel
クラスを定義します。
カウント数を保持する変数と、数に変化があった時 View に変更の通知を行うための MessagePorter
クラスを生成します。
constructor() {
super()
return this.initialize()
}
コンストラクタの定義です。 AlierModel
のコンストラクタでは、 initialize()
を呼び、その返値をコンストラクタの返値として返す必要があります。これはModelクラスがSingleton
クラスから派生していることが理由で、ここでは詳しい説明をすることは避けますが、詳細を知りたい方はSingleton
のリファレンスを参照してください。
初期化処理が必要であれば、initialize()
の引数には初期化用の関数を渡すことができます。
countUp() {
this.setCount(this.#count + 1);
}
カウント数を 1 ずつアップするためのメソッド定義です。
reset() {
this.setCount(0);
}
カウント数をリセットするメソッド定義です。
setCount(num) {
this.#count = num;
this.onChange.post(Alier.message("onChange", "", { count: this.#count }));
}
カウント数の更新を行うメソッド定義です。
引数で受け取ったカウント数を変数に代入してデータを更新し、カウントが更新されたことを MessagePorter.post()
で通知しています。
Alier.message()
はメッセージオブジェクトを生成するメソッドです。
MessagePorter.post()
は登録されたイベントリスナーにイベントを通知し、引数に指定されたメッセージオブジェクトを渡します。
モデルインターフェースを構築する
<model>
<import path="counter_model.js"/>
<interface class="CounterModel">
<function name="countUp" path="countUp" locale="local"/>
<function name="reset" path="reset" locale="local"/>
<message-porter name="onChange" path="onChange" locale="local"/>
</interface>
</model>
Model の機能にアクセスするためのインターフェース、モデルインターフェースを 定義します。
使われているタグについては以下の通りです。
タグ | 説明 |
<model> |
XML のルート要素のタグです。この中にインポートするモジュールのファイルとインターフェース定義を記述します。 |
<import> |
インポートするモジュールを宣言するタグです。 |
<interface> |
インターフェース定義であることを示すタグです。<model> 直下に1つだけ置くことが出来ます。 |
<function> |
関数を表し、インターフェースとして登録します。 |
<message-porter> |
イベントハンドラを表し、インターフェースに登録します。 |
タグの詳細仕様や、上記以外のタグについては モデルインターフェースのタグ一覧 を参照してください。
また、CounterModel
のメソッドのうち setCount()
はインターフェース未定義ですが、クラス内の全てのメソッドをインターフェース定義する必要はありません。公開した方が有用だと判断したものだけ公開してください。
このようにインターフェースを介して Model の機能を提供するのは、 Model と View がお互いを知ることなく、その機能を利用するためです。
Alier の Model と View には明確な役割分担があります。Model はデータ処理やビジネスロジック等を担当し、View は表示とその論理部分( ViewLogic
)を処理します。この二つを分離することで View 側のみ、 Model 側のみでそれぞれ独立して実装することができます。
View を定義する
class CounterView extends ViewLogic {
constructor() {
super();
this.relateElements( this.collectElements( this.loadContainer({ text: `
<alier-container>
<input type="button" id="counter" data-ui-component data-active-events="click" />
</alier-container>
` })));
Alier.Model.onChange.addListener(msg => this.post(msg));
Alier.Model.reset();
}
ViewLogic
の定義です。ここでは CounterView
というクラスを定義しています。
コンストラクタでカウントアップするボタンだけを設置した HTML 文字列を読み込んでいます。
その下では、Alier.Model
のプロパティとして参照できるようになった onChang にアクセスして、 addListener()
を呼んでいます。
このメソッドは引数でイベントリスナーとなる関数を渡してイベントの通知を受けられるようにします。ここでは onChange の post()
が呼ばれた時、addListener()
の引数に渡している (msg)=>{this.post(msg)} が呼ばれることになります。
ProtoViewLogic.post()
は、呼ぶと対象の ViewLogic
の messageHandler()
を呼び出します。
つまりここでは、onChange からイベント通知がきたら、この ViewLogic
の messageHandler()
が呼ばれることになります。
最後に、カウント数を初期化するために Alier.Model.reset()
を呼んでいます。
async messageHandler(msg) {
return await msg.deliver({
counter : msg => Alier.Model.countUp(),
onChange : msg => this.counter.value = `count: ${msg.param.count}`
});
}
}
await Alier.export({ CounterView });
メッセージを処理するための messageHandler()
を定義します。
まず、counter ボタンがクリックされた時のメッセージ処理です。
こちらはボタンが押される度に カウント数が 1 ずつアップするようにしたいので、Model クラス に実装した countUp()
を呼び出します。これでボタンを押す度に Model クラス で保持しているカウント数が 1 ずつアップするようになりました。
Model はカウント数が更新された時に onChange イベントを通知してきますので、onChange イベントが通知されます。
messageHandler()
でそのイベントを処理します。
引数のメッセージオブジェクト msg の param プロパティには MessagePorter.post()
時に渡した最新のカウント数が入っています。counter ボタンの value にそのデータが表示されるように代入します。
実行結果
サンプルを動かしてみましょう。
- 起動画面が表示
- ボタンを押下でカウントが 1 上昇するのを確認
ここで、例えばこのアプリケーションに機能追加でカウントを 0 に戻すボタンを加えようと思ったとします。
その場合、表示するボタンを追加し、Alier.Model.reset()
を呼ぶ、View 側で完結した対応ができます。
このように Model と View の実装が分離していれば、仕様の変化があっても View 側だけ、Model 側だけの変更で対処できる場面が増え、結果的に実装の手間やバグの入るリスクを抑えられます。
大規模で複雑なアプリケーションを開発する場合でも、基本的な考え方は一緒です。