JavaScriptのWebアプリケーション設計とMVCモデルについて

2021年3月22日JavaScript

JavaScriptのWebアプリケーション設計について、基礎的な部分をまとめました。
また、Webアプリケーション設計の考えが、どのようにMVCモデルに関係しているのかを説明します。

優れたWebアプリケーション設計について

優れた設計は次の3つの要素をすべて兼ね備えています。

構造化(structure)

建物のように、ソフトウェアにも構造が必要です。
ソフトウェアの構造というのは、どのようにしてモジュール、クラス、関数などを組織化することです。

メンテナンス性 (maintainability)

メンテナンス性が高いことで、将来に渡ってソフトウェアを変更・改善し続けることができます。

拡張性(expandability)

新しい機能を追加できる必要があります。

3つの要素をすべて兼ね備えている設計パターンの存在

自分ですべての設計をすることもできます。
小さなプロジェクトに関してはその方法で十分な場合も多いです。
ただし、拡張性の問題がのちに出てくる場合が多いです。
そこで、これらの要素を兼ね備えた、とても優れた設計パターンとして、MVC、MVP、Fluxなどがあります。

設計の考え方を知っておくと学習効率が上がる

多くの場合ReactやVueなどのフレームワークを利用することで、この設計について詳しく考える必要もなくなります。

ただし、設計を自分でする経験もなしにこれらを利用することは、JavaScript開発者としては不安が残ります。
また、設計の考え方がわかると、それらの習得もとても簡単になります。

Webアプリケーション設計の重要な5つの要素

Webアプリケーション設計の重要な5つの要素は次の通りです。

  • ビジネスロジック (business logic)
  • 状態管理 (ステート管理、state)
  • HTTPライブラリ HTTP library
  • アプリケーションロジック (Application logic, router)
  • プレゼンテーションロジック(Presentation logic, UI layer)

ビジネスロジック (business logic)

それぞれのビジネス固有の問題を実際に解決するコード
ビジネスに直結するもの
たとえば、LINEのようなビジネスではメッセージを送る、受け取るというコードが必要です。銀行なら送金・入金を格納する、確定申告のアプリなら、税金の計算をするなどのコードが必要です。

状態管理 (ステート管理、state)

ステート(状態)とは、ブラウザ上で表示されているアプリケーションに関するすべてのデータ(API、ユーザーが入力した値、ユーザーが見ているページなどフロントエンドに格納されているデータ)
信頼できる唯一の情報源 (Single source of truth)であること
ステートとUIは同期的であること(一方が変化すればもう一方も変化する)
ステートとUIが常に同期的に変化するのはとても重要でかつ難しいため、ステート管理のためのライブラリが存在します。(ReduxとMobXなど)

HTTPライブラリ HTTP library

AJAXリクエストを受け取ったり作成したりできる(fetch関数など)

アプリケーションロジック (Application logic(router)

アプリケーション自身の実装のみに関係しているコード
ナビゲーションやUIのイベントを処理する(ビジネスロジックに関係していないコード)
元々のルーターというものはデータの送信経路を決定する装置のことです。

プレゼンテーションロジック(Presentation logic (UI layer)

見える部分のアプリケーションに関するコード
アプリケーションの状態(ステート)を表示します。
つまり、このプレゼンテーションロジック(UI layer)とステートが同期的に変化する必要があります。

MVCモデルについて

MVCモデルは次の3つの大きな部品を持っている設計パターンのことです。

  • モデル(Model)
  • ビュー(View)
  • コントローラー(Controller)

ビュー(View)

ビューは、プレゼンテーションロジックです。ユーザーに見える部分を担当しています。

モデル(Model)

モデルは、アプリケーションのデータに関する部分です。そのためステート(データ)とステートに関するビジネスロジックです。
さらに、APIなどのWebからのデータの送受信をするのもモデルの仕事です。そのためHTTPライブラリも関わります。

コントローラー(Controller)

コントローラーは、アプリケーションロジックを担当します。
モデルとViewの間を取り持つ架け橋のような立場です。
モデルもViewは、お互いが存在しているかどうかも、コントロールの存在さえも知らないほどに完全に独立しています。
そして、このアプリケーションロジックを独立させることが、設計において重要な点になります。

MVCの実際の流れ

実際の流れをみてみましょう。
たとえば、ユーザーがクリックしました。それは、コントローラーがそのイベントを対処します。なぜなら、イベントの対処はアプリケーションで対処すること(アプリケーションロジック)だからです。
そして、コントローラーは、データが必要となるのであればモデルに、ユーザーインターフェースのアップデートが必要であればジューにつなぎます。つまり、コントローラーは、UIイベントを対処して、タスクをモデルとビューに渡します。(Controller dispatches tasksやorchistrates this entire actionといった表現が使われます。)

モデルは、必要であればWebにAJAXリクエストをしてデータを取得します。
そして、データが到着したら、コントローラーはそのデータを取って、ビューに渡します。
そして、ビューがそのデータをレンダー(データを基に表示内容をつくる)してユーザーが見れるようにします。

関数での呼び出しや、インポートで接続されるのはコントローラーのみ

実際に関数での呼び出しや、インポートで接続されるのはユーザーがクリックしたイベントをコントローラーが対処するときと、コントローラーがモデルとビューにタスクを渡すときのみです。
あとはデータが流れるだけとなります。

ユーザーがクリックしたり、何かを選択したり、ページをロードすると、コントローラーが対処します。そしてモデルとビューにタスクを課します。データをWebから取得するのはモデルの仕事です。そして、それをコントローラーが取得します。そしてコントローラーがビューに渡たして、ビューはレンダーします。

Publisher-Subscriberパターン

Publisher-Subscriberパターン(PubSubパターン)を説明するためにaddEventListenerの例をみてみましょう。

window.addEventListener('click', controlWords);

たとえば、controlWordsという関数が、クリックされたときに反応してほしいとします。
そして、そのcontrolWordsというものの中には、モデルにもビューにもタスクを渡す処理が書かれているとします。


const controlWords = async function () {
  try {

   await モデルのタスク
    wordView.render(); ビューのタスク
  } catch (err) {
    console.log(err);
  }
};

async/awaitに関しては次の記事を参考にしてみてください。


このような場合、DOMに関することは表示部分のためビューが担当するはずです。そのイベントがclickでもhaschangeでもloadでも、イベントを聞くのは、ビューが担当します。
ただし、controlWordsというのは、ビューにもモデルにも仕事を振り分けるので、コントローラーの仕事のはずです。
つまり、

window.addEventListener('click', controlWords);

こちらはビューとコントローラーの役割が混在した状態になっています。
このようなときに、Publisher-Subscriber パターンを使います。

PublisherとSubscriber

いつ反応するかをわかる必要があるのがPublisherです。つまり、ビューです。
何をしたいかをわかる必要があるのがsubscriberです。つまり、コントローラーです。
やることとしては、コントローラーの関数(subscriber)を引数としてビューの関数(publisher)に渡します。
この場合、ビューがコントロールしているのではなく、ビューは渡されたものをただただ実行しているだけということになります。

コントローラーとビューの仕事を分ける

以下の例ではhandlerという引数にします。controlWordsが引数handlerに渡されます。


//コントローラー
  try {

   await モデルのタスク
    wordView.render(); ビューのタスク
  } catch (err) {
    console.log(err);
  }
};


const init = function() {
  wordView.addHandlerRender(controlWords);
}
init();

そして、ビューは次のように記述します。


addHandlerRender(handler) {
window.addEventListener('click', controlWords);
}

最初にinit()関数でcontrolWordsを変数としてビューの引数に渡しておきます。
そして、ビューでイベントが確認されたら、ビューはただただ渡された変数を実行します。
そして、その実行された変数は、コントローラーにある関数のため、コントローラーがその関数を実行してビューやモデルにタスクを渡すことができます。