フロントエンドとオブジェクト指向
フロントエンドの実装にオブジェクト指向をどのように取り入れるかを考えます。
動機
近年のフロントエンドは、Reactなどのフレームワークを使ったコンポーネントベースの設計が主流だと思います。コンポーネントは、HTMLによるマークアップ、CSSによるスタイリング、JavaScriptによる振る舞いがひとまとめにされた、再利用可能な部品です。
コンポーネントの設計を考えていると、次のような疑問が生じます。
- 何を基準にコンポーネントで分割すればよいか。
- コンポーネントの粒度はどれくらいが適切なのか。
- どのタイミングで抽象化すれば開発コストが無駄にならないか。
- 分業した際にコンポーネントの分割や粒度の基準をどのように統一するべきか。
そこで、いろいろ調べたり試したりしたところ、フロントエンドの設計にオブジェクト指向を取り入れることが、これらの答えの一つになるのではないかと考えました。
この記事では次のことについて記述します。
- オブジェクト指向の意味
- なぜオブジェクト指向か
- コンポーネントとオブジェクト指向
- モデル層とオブジェクト指向
※「モデル層とオブジェクト指向」は一部書きかけの節があります。後日追記予定です。[1]
オブジェクト指向の意味
一口に「オブジェクト指向」と言っても、その意味は文脈によって少し変わります。単に「オブジェクト指向」と言う場合、個人的には次の3つの文脈があると考えています。
- 物事の捉え方としてのオブジェクト指向
- オブジェクト指向プログラミング(OOP)
- オブジェクト指向ユーザインタフェース(OOUI)
物事の捉え方としてのオブジェクト指向
ソフトウェアを設計するにあたり、対象とする領域(ドメイン)をモデル化するための一つの方法です。物理的に存在する物もそうでない事も、あらゆる対象をオブジェクトとみなし、そのオブジェクトは何を知っていて、どのように振る舞い、他のオブジェクトとどう連携するかをデザインします。ユーザはオブジェクトを自由に連携させることで、個々のオブジェクトの責務を超えた、より大きな課題の解決を図ります。
哲学の分野では、グレアム・ハーマンのオブジェクト指向存在論(OOO)というものがあるようです。私は詳しくないのでここで説明することはしませんが、このような物事の捉え方の基礎になっているのではないかと思います。
オブジェクト指向プログラミング
オブジェクト指向プログラミングは、さらに「メッセージング」としてのものと「抽象データ型」としてのものとに二分されると考えます。
メッセージングとしてのオブジェクト指向は、アラン・ケイがSmalltalkで提唱したもので、動的性を追求し、全てをオブジェクト間のやり取り(メッセージング)で表現します。Smalltalk以外で一般によく利用されているプログラミング言語の中では、Rubyが最も近い思想を持っていると思います。
一方、抽象データ型としてのオブジェクト指向は、ビャーネ・ストロヴストルップがC++で提唱したもので、カプセル化や継承、多態性(ポリモーフィズム)を特徴とし、静的な抽象データ型としてのクラスをベースとしたものです。Javaもこちらに当てはまります。一般にオブジェクト指向プログラミングを言う場合、こちらのほうがよく知られています。
メッセージングと抽象データ型については次の記事を参考にしました。
オブジェクト指向ユーザインタフェース
この言葉の初出については把握していませんが、近年ではソシオメディア株式会社の『オブジェクト指向UIデザイン』でよく耳にするようになったと感じます。オブジェクト指向ユーザインタフェースは、コマンドラインインタフェース(CLI)のような「動詞-名詞」ではなく「名詞-動詞」形式の対話を特徴としたユーザインタフェースです。
詳細は、同著者による次の記事を参考にしてください。
それぞれの関係性と本記事での意味
ここで述べたもののうち、メッセージングとしてのオブジェクト指向プログラミングとオブジェクト指向ユーザインタフェースは、いずれも物事の捉え方としてのオブジェクト指向を基礎としていると見ています。我々人間が豊かになるために用いてきた「道具」に対するメンタルモデルをコンピュータに反映したものが、それぞれのオブジェクト指向です。
よって、本記事での「オブジェクト指向」も、物事の捉え方としてのオブジェクト指向を基礎として、これをコンピュータを介した道具としてのアプリケーションを構成する要素の一つとしてのフロントエンドで表現する方法を考えていきます。
なぜオブジェクト指向か
では、なぜフロントエンドの実装にオブジェクト指向を取り入れるのでしょうか。それは、オブジェクト指向でデザインされた道具としてのアプリケーション(もっと言えばそれを含めたコンピュータ)において、それを構成する各要素の実装で一貫した考え方を表現するためです。
コードは開発者にとってのユーザインタフェースである
コードはコード、ユーザはユーザ、そしてそれらの橋渡し役がユーザインタフェースというように別々に見るのではなく、同じオブジェクト指向という考え方をコードとユーザインタフェースの双方に取り入れることで、ユーザのメンタルモデルとコンピュータの実装モデルとが一貫したものになります。妙に小難しいコードやユーザインタフェースが生じる場合、これらのどこかにギャップが生じており、それを埋め合わせるためのものが混ざってしまっているのです。
もちろん、各要素の技術的な問題から、オブジェクト指向での表現にこだわるよりも優れた実装方法を採用する場合もあります。データベースの設計はその一例でしょう。しかし、個人的には、コンピュータというメタ道具(道具を作る道具)を使ってユーザが自由に道具を開発できるようにし、そのためにコード自体が開発者にとってのユーザインタフェースとなるべきだと思っています。
プログラムは計算機への指示書であると同時に人間に向けた創作的表現なのだということ。
もしプログラムが人間に向けたものでないのであれば、リーダビリティなど必要ない。そもそも高級言語が必要ない。しかし現実はそうなっていない。プログラミングとは純粋な技術の問題ではない。人文の問題でもある。
ユーザ自身で使い方をデザインできる「道具」
一貫した表現が重要だとすると、今度はそもそもなぜオブジェクト指向であるべきなのかという疑問が生じます。これについてはいくつかあると思いますが、アプリケーションを開発する立場であるエンジニアから共感を得やすいものとしては、「要件は常に変化するものであり、そのたびにコードの変更を加えていては大変だから」というものがあると思います。
要件は文脈(ユーザ固有の考え方やユーザを取り巻く環境から受ける影響など)によって変化します。さらに、ユーザがアプリケーションを使用することで自身の知能を拡張し、行動に変化を与え、周囲にも変化を与えることで、要件はさらに変化していきます。文脈は設計者の意図した範囲を超え、再現性もありません。だから、要件をそのままコードとして実装することができません。
しかし、我々人間が道具を使って能力を拡張し、それによって新たな道具を作り出し、またそれによって知能を拡張していったように、そのような道具としてのアプリケーションを実装することで、ユーザ自身で要件の変化に対応していけるようなものを作ることができるのではないでしょうか。そのための方法がオブジェクト指向であり、各オブジェクトが十分にシンプルで疎結合であれば、それらの無数の組み合わせによりユーザ自身で使い方をデザインすることができます。その意味では、アプリケーションの開発者はメタデザインをしていると言えます。
これ以上は、私の拙い言葉で説明するより、先ほど触れた『オブジェクト指向UIデザイン』や次の記事を読んでいただくのがよいと思います。私はこれらから強くインスパイアを受けました。
ちなみに、私はここで「全てのアプリケーションはオブジェクト指向で作られるべきだ」などと主張するつもりはありません。オブジェクト指向が適さないアプリケーションもあるかもしれません。ビジネス上の戦略やアプリケーションによって構成されるサービスの特性、ユーザの性質などを踏まえたうえで、コンセプトにオブジェクト指向を適用することが適当かどうかを考える必要があります。
コンポーネントとオブジェクト指向
本題に入ります。冒頭でも触れたとおり、近年のフロントエンドはコンポーネントベースの設計が主流です。幸いなことに、コンポーネントとオブジェクト指向は相性が良いと思います。コンポーネントはビュー(ユーザインタフェース)上で実体化したオブジェクトそのものと見ることができます。
オブジェクトは自身の責務に基づき、外部からの入力に対して応答を返します。ユーザインタフェースにおいては、ユーザからの入力は、例えばボタンのクリックであったりテキストの入力であったりします。それに対する応答は、例えばテキストの表示であったり色の変化であったりします。このとき、オブジェクトの内部では状態が変化することがあり、これはフロントエンドにおいても同様です。オブジェクト指向とフロントエンドのコンポーネントベースの設計には共通点が多いのです。
もう少し具体的に見ていきます。
モデル層との接続
ユーザインタフェースを持つアプリケーションの設計パターンとして、Model-View-Controller(MVC)やModel-View-ViewModel(MVVM)というものがあります。ここでは後者のMVVMを適用します。モデル層については後述しますが、ここではコンポーネントベースで設計されたビュー層をモデル層と接続することを考えます。
アプリケーションが対象とする領域(ドメイン)をモデル化したものをドメインモデルと呼ぶことにします。オブジェクトとみなしたビュー層のコンポーネントがドメインモデルに基づいた知識や振る舞いを手に入れるために、これらのコンポーネントとモデル層とを接続する必要があります。
例として、シンプルなブログのドメインモデルを示します。記事のモデルがあるとします。記事はタイトルと本文の2つの属性を持ち、フロントエンド特有の属性として同期状態を持ちます。同期状態は、フロントエンドがネットワークリクエストを開始してからレスポンスを得るまでの間の表示を管理するための属性です。また、振る舞い(メソッド)として記事の取得と保存を定義します。
このブログのフロントエンドを実装するとして、ここに一つの記事を表すコンポーネントを示します。これは一つの記事のオブジェクトであり、モデル層の記事と接続することにより、記事のドメイン知識を得ます。なお、ここではコンポーネントにReact、モデル層(単一状態管理)にReduxを利用しているとします。
import React, { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import ArticleStatus from "../../store/articles/status";
import { getArticleStatus } from "../../store/articles/selectors";
import { requestToGetArticle } "../../store/articles/actions";
import ArticleTitle from "./ArticleTitle";
import ArticleBody from "./ArticleBody";
const Article: React.FC = (): JSX.Element => {
const status = useSelector(getArticleStatus);
const isLoading = status === ArticleStatus.Syncing;
const dispatch = useDispatch();
// コンポーネントがマウントされたら記事の取得を開始。
useEffect((): void => {
dispatch(requestToGetArticle());
}, [dispatch]);
return (
<article>
<heading>
{isLoading ? (
<div>読み込み中です。</div>
) : (
<ArticleTitle />
)}
</heading>
<ArticleBody />
</article>
);
};
export default Article;
import React, { useState, useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { getArticleTitle } from "../../store/articles/selectors";
import { requestToSaveArticle } "../../store/articles/actions";
const ArticleTitle: React.FC = (): JSX.Element => {
const title = useSelector(getArticleTitle);
const dispatch = useDispatch();
const [value, setValue] = useState(title);
// タイトルが変更されたら記事の保存を開始。
useEffect((): void => {
dispatch(requestToSaveArticle({ title: value }));
}, [dispatch, value]);
useEffect((): void => {
setValue(title);
}, [title]);
const handleChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
setValue(event.target.value);
};
return (
<h2>
<input type="text" value={value} onChange={handleChange} />
</h2>
);
};
export default ArticleTitle;
Article
は記事、ArticleTitle
は記事のタイトル属性についての責務を持つコンポーネントかつオブジェクトです。オブジェクトとその属性とを別のコンポーネントとして表現しています。こうすることで、コンポーネントあたりのコード量を小さくし、見通しをよくします。
オブジェクトが知識(属性)を持つように、オブジェクトに対応するコンポーネントは、モデル層から知識を(Reduxのセレクタによって)取得しユーザに表示します。オブジェクトが振る舞い(メソッド)を持つように、オブジェクトに対応するコンポーネントは、ユーザからの操作を受けてモデル層の振る舞いを(Reduxのアクションディスパッチによって)リクエストします。この対応関係がオブジェクトとしてのコンポーネントの要です。
コンポーネントの分類
ドメインモデルのオブジェクトとみなしたコンポーネントを「ドメインコンポーネント」と呼ぶことにします。ドメインコンポーネントはドメイン知識を持つコンポーネントですが、フロントエンドでユーザインタフェースを実装する際は、テキストやボタン、レイアウトなどといったものも必要になります。例えば、記事を表示(取得)するボタンと保存するボタンは、どちらも「ボタン」として同じマークアップやスタイリング、振る舞いを持つかもしれません。
こうなると、Bootstrapのようにユーザインタフェースの一部を切り出し、再利用可能な部品にしたくなると思います。これはドメイン知識に依存しないコンポーネントです。ここではこれを「要素コンポーネント」と呼ぶことにします。
一方、ドメインコンポーネントは同じオブジェクトを扱っていても、文脈によってその表示方法や受け付ける操作が変化する場合があります。例えば、一つの記事というオブジェクトを見ると、不特定多数の人が記事を参照する場合はテキストのみで編集は許可せず、限られた人(アカウントを持つ人など)だけが編集もできるようにするということはよくあると思います。編集する場合は、テキストだけでなく <input type="text">
などのコントロール要素が一般的に必要です。
また、記事の一覧を表示する場合、最新記事一覧はタイトルと本文の両方を表示して、アーカイブ(過去の記事一覧)ではタイトルのみ表示するといったようなこともあります。このように、同じドメインモデルのオブジェクトであっても、ユーザインタフェース上では多少姿を変えて現れる場合があります。
これ以外にもいくつか検討した結果、最終的に次のような分類を定義しました。
- 要素コンポーネント
- ドメインコンポーネント
- ページコンポーネント
- アプリケーションコンポーネント
要素コンポーネント
テキストやボタン、アイコン、コンテナ、メニュー、ダイアログ、レイアウトなどといった、ユーザインタフェース上の汎用的な部品です。ドメイン知識に依存せず、モデル層と直接接続することはありません。
ドメインコンポーネント
ドメインモデルのオブジェクトとみなしたコンポーネントです。要素コンポーネントを利用し、自身のオブジェクトをユーザインタフェース上で体現します。ドメイン知識を持っており、モデル層と直接接続します。
また、ドメインコンポーネントは同じドメインモデルのオブジェクトに対応するとき、文脈に応じた表示や振る舞いを持ついくつかの種類が存在する場合があります。
ドメインコンポーネントの命名規則は [形容詞]ドメインオブジェクト名[Collection][属性名]
を基本とします。例えば次のようになります。
- 単体の記事 —
Article
- 記事の一覧 —
ArticleCollection
- ある記事のタイトル属性 —
ArticleTitle
- 記事の数 —
ArticleCollectionCount
- 最近投稿されたの記事一覧 —
RecentlyPostedArticleCollection
ページコンポーネント
一つのページを構成するコンポーネントです。ドメインコンポーネントや要素コンポーネントを含みます。ページコンポーネントはHTMLドキュメント内で常に一つだけ表示されるようにします。
アプリケーションコンポーネント
コンポーネントツリーの根に位置する、エントリポイントとなるコンポーネントです。
コンポーネントの抽象化のステージ
しっかりしたデザインシステムがあれば、コンポーネントを実装するときはそれにならうだけで問題ありませんが、そのような体系化されたものがない状態で実装を始めなければならないということは珍しくないと思います。その際、要素コンポーネントはどこまで抽象化すればよいか必ずと言っていいほど迷います。
そこで、CSSワーキンググループのステージングプロセスのように、コンポーネントの抽象化の過程にステージを定義するとよいかもしれません。過度に抽象化に迷ってしまうのを防ぎ、完成度に不安があっても気軽にコンポーネントを実装することができます。また、コンポーネントを利用する場合にそのコンポーネントの完成度を知る指標となり、必要に応じて改良を加えやすくなります。
ステージの定義例を次に示します。
- ステージ 0 — 抽象化するには早すぎる
- ステージ 1 — とりあえず抽象化してみたけど今後大きく変更するか削除する可能性大
- ステージ 2 — 用途によって多少変更する可能性あり
- ステージ 3 — 用途がほぼ定まって安定してきた
- ステージ 4 — ライブラリとしてリリースできるぐらい安定している
モデル層とオブジェクト指向
ここまで主にビュー層について述べてきましたが、最後にモデル層の設計について考えてみます。
ビュー層はユーザのメンタルモデルとコンピュータの実装モデルとを橋渡しする、まさしくユーザインタフェースとしての役割を担っています。モデル層はコンピュータの実装モデルを表現するものですが、これはユーザのメンタルモデルを表現するもの(またはその構築を助けるもの)でもあります。オブジェクト指向である理由として述べた通り、ユーザインタフェースとコンピュータの実装モデルは、ユーザのメンタルモデルと一貫したものにする必要があります。ここで重要となるのは次の2点です。
- ドメインモデルのオブジェクトは、その知識(属性)と振る舞い(メソッド)がコードとして明確に表現されるべきである。
- ユーザがユーザインタフェースを通してコンピュータ上にあるオブジェクトを直接操っている感覚になるよう、モデル層はオブジェクトの状態をリアルタイムに更新するべきである。
オブジェクトの知識と振る舞いを明確に表現
オブジェクト指向なモデル層といえば、Ruby on RailsやBackbone.jsのフレームワークがわかりやすいのではないでしょうか。いずれもクラスベースでコードを記述することができます。しかし、オブジェクト指向の意味で述べたように、オブジェクト指向をコードとして表現するためにクラスを使わなければならないという決まりはありません。ここでは、Fluxアーキテクチャを採用する状態管理ライブラリのReduxを使って、オブジェクトの知識と振る舞いを表現してみます。
Fluxアーキテクチャでは、主にアクション、リデューサ、ステートの3要素で状態管理を構成します(これはFluxアーキテクチャの正確な説明ではないかもしれません)。アクションはステート更新のトリガーとなるもので、これが実行されると対応するリデューサがステートを作成します。
Fluxアーキテクチャ。ビューはユーザの入力を受けてアクションを実行し、モデル層は、必要な場合はサーバからデータを得て、リデューサによってステートを作成します。ステートの更新がビューに通知されることで、オブジェクトの状態変更をユーザインタフェースに反映できます。
これをオブジェクト指向で考えてみると、ステートはオブジェクトの知識(属性)そのもの、アクションはオブジェクトの振る舞い(メソッド)と見ることができるのではないでしょうか。先ほどの ArticleTitle.tsx
には次のようなコードがありました。
const title = useSelector(getArticleTitle);
useEffect((): void => {
dispatch(requestToSaveArticle({ title: value }));
}, [dispatch, value]);
useSelector(getArticleTitle)
は、Reduxのステートから記事のタイトル属性の値を取得しています。これはクラスベースのコードではゲッタのようなものです。dispatch(requestToSaveArticle({ title: value }))
は、記事のタイトル属性を指定した値に更新するReduxのアクションを実行しています。これはクラスベースのコードではメソッドのようなものです。
どうやら、クラスベースではないコードであっても、モデル層でオブジェクト指向を表現することができそうです。ここからは、引き続きReduxを例として、具体的な設計方法を考えます。
オブジェクトの状態をリアルタイムに更新
これは一見するとオブジェクト指向とは関係ないように思えるかもしれませんが、オブジェクトの状態をリアルタイムに更新してユーザインタフェースに反映することは、オブジェクト指向が目的としている「道具」感を作り出すことに大きく寄与すると考えています。ユーザは自身のメンタルモデルに基づいたオブジェクトが(ユーザインタフェースを通して)コンピュータ上に現れると、それを自身に帰属して自由に使いこなすことができます。この自己帰属感を得る、言い換えれば自分の身体の延長のように扱えるようにするには、自分の操作に対するフィードバックがすぐに返ってくることが自然(無意識的)である必要があります。
キーボードを使って文章を書くとき、普通は押したキーの文字が(実際には遅延があっても人間が意識できないため)リアルタイムに画面に表示されるため、私たちは思い通りに文章を書くことができます。しかし、これが1秒後や3秒後に表示されるとしたらどうでしょうか。とても自己帰属感を得るのは難しいと思います。
フロントエンドはネットワークを経由してサーバとやり取りするため、1秒を切ることはできても、どうしても人間が意識できる程度の遅延は発生してしまいます。High Performance Browser Networkingによれば、時間的に100ミリ秒以上の遅延が生じると、体感でも小さな遅延を感じるようになるとしています。個人的には、ユーザインタフェースを通して自己帰属感を得るには、0秒に限りなく近くないと厳しいのではないかと感じます。裏側にネットワーク通信の存在を感じさせてはダメなのです。
フロントエンドでこれを実現する一つの方法は楽観的更新です。楽観的更新は、サーバからのレスポンスを待たずに状態を更新することで、ユーザインタフェース上は即時に更新できているように見せる方法です。身近なところでは、Twitterの「いいね」ボタンが楽観的更新のようです。対して、レスポンスが返ってきてから状態を更新する方法は悲観的更新と呼ばれます。
楽観的更新によってモデル層の状態を同期的に更新し、その変更をビュー層にも反映することで、ユーザの入力に対するフィードバックの遅延は生じなくなります。これにより、ユーザはユーザインタフェースを通してコンピュータ上のオブジェクトを直接操作しているような感覚を得ることができます。
ユーザとクラウド上のコンピュータとをユーザインタフェースで橋渡しし、その背後でモデル層が楽観的更新することで、ユーザはコンピュータ上のオブジェクトに直接入力する感覚と、オブジェクトから直接出力される感覚を得ます。
Webアプリケーションにおけるフロントエンドのモデル層は、サーバ上のデータをクローンしたものと捉えるとよいかもしれません。フロントエンドではオブジェクトの状態変化を即時に反映し、モデル層がユーザインタフェースの背後でサーバと常に同期を行っているようなイメージです。Reduxの場合はRedux-Sagaのようなミドルウェアがサーバとの通信を担当します。
ただし、楽観的更新はサーバにデータを保存できなかった場合のフォールバックを適切に実装しないと、ユーザは保存できていると思ったのに実際には保存できていなかったということが起きてしまうため注意が必要です。この問題を回避する例を次に示します。
- 保存に一定時間以上要する場合は、保存中であることがわかるインジケータを表示する。
- 保存が完了する前にページが閉じられそうになった場合は、
beforeunload
イベントでユーザに確認する。 - 保存されていないオブジェクトがユーザインタフェース上に表示されていない場合でも、ユーザがエラーに気づくことができるようにする。
- ユーザの入力値を自動的に修正できる場合は、修正してからサーバにリクエストすることでエラーを回避する。
- 楽観的更新による保存の失敗がユーザに直接的な被害を与える可能性の高い場合(例えば決済)は、悲観的更新で処理する。
まとめ
ユーザ自身で要件の変化に対応していけるような、道具としてのアプリケーションを作るために、また、ユーザインタフェースとコードの設計を一貫させることでユーザのメンタルモデルを実装にも反映し、コードが開発者にとってのユーザインタフェースとなるようにするために、物事の捉え方としてのオブジェクト指向を基礎として、フロントエンドの実装にこれをどのように取り入れるかを考えました。
モデル層と接続したドメインコンポーネントは、ビュー(ユーザインタフェース)上で実体化したオブジェクトそのものと見ることができます。オブジェクトが知識(属性)を持つように、オブジェクトに対応するコンポーネントは、モデル層から知識を取得しユーザに表示します。オブジェクトが振る舞い(メソッド)を持つように、オブジェクトに対応するコンポーネントは、ユーザからの操作を受けてモデル層の振る舞いをリクエストします。
モデル層では、ドメインモデルのオブジェクトの知識(属性)と振る舞い(メソッド)がコードとして明確に表現されるよう、ドメインモデルごとにステートツリーを分割し、セレクタを知識、アクションを振る舞いとみなします。また、ユーザがユーザインタフェースを通して自己帰属感を得るために、楽観的更新によってオブジェクトの状態をリアルタイムに更新し、ビューに反映するようにします。
なお、本記事で述べたコンポーネントやモデル層の設計は、私が開発の経験を重ねたり、フロントエンドの技術が進歩したりする過程で、変化するかもしれません。より優れた設計ができた場合は、また何らかの形で共有できればと思います。
もし記事中に誤りなどがあれば、ご指摘いただけると幸いです。最後までご覧いただきありがとうございました。
-
なかなか更新するための時間を確保できず、延期を繰り返してしまっており申し訳ありません。この記事の考え方に沿ったリアルワールドなアプリケーションの実装や、RTK Queryのようなドキュメントベースの状態管理についての追記も検討しております。更新まで今しばらくお待ちいただけますと幸いです。 ↩︎
Discussion