Vueでジェネリックと継承を使って処理の共通化をした話
前提
- Vue2系
- TypeScript + Vueのプロジェクト
- vue-property-decoratorを使用
背景
- いくつかのマスタの管理画面をVueで作ることになった。
- マスタの(View)ModelはTypeScriptの型が作ってある状態。
- マスタの数が多いぶん、管理画面もたくさん作らなきゃいけない。何とか処理の共通化ができないものか・・・。
- そこで、 各画面で共通の処理を親クラスに全部書いて、テンプレート(HTML)と各マスタ個別処理は各ページに書く ことにしてみた。
イメージ
-
models/
-
A.ts
: モデルAのインターフェース -
B.ts
: モデルBのインターフェース -
C.ts
: モデルCのインターフェース -
ModelUtil.ts
: モデルを操作する関数群の共通インターフェース -
AModelUtil.ts
: モデルAを操作する関数群 -
BModelUtil.ts
: モデルBを操作する関数群 -
CModelUtil.ts
: モデルCを操作する関数群
-
-
pages/
-
MasterPageBase.ts
: 今回の主役。3つのページの親クラス -
AMasterPage.vue
: Aを操作するためのページ -
BMasterPage.vue
: Bを操作するためのページ -
CMasterPage.vue
: Cを操作するためのページ
-
やったこと
ページの共通親クラス
MasterPageBase.ts
: ページの親クラス。各ページの処理の共通部分を担う。
中身はだいぶ省略。雰囲気だけお伝えします。
@Component
export default class MasterPageBase<T extends IMasterModel> extends Vue {
// サブクラスで上書く
protected masterUtil!: ModelUtil<T>;
protected masterApi!: string;
protected masterName!: string;
// ページで現在表示しているマスタの一覧
protected masters: T[] = [];
protected page: number = 1;
protected async save(): Promise<string> {
// 略
}
protected async load(): Promise<T[]> {
// 略
}
}
個別ページ
AMatserPage.vue
<template>
略
</template>
<script lang="ts">
import { Component } from 'vue-property-decorator';
import MasterPageBase from './MasterPageBase';
import { A } from '@/models/A';
import { AModelUtil } from '@/models/AModelUtil';
import { ModelUtil } from '@/models/ModelUtil';
@Component()
export default class AMasterPage extends MasterPageBase<A> {
// サブクラスで上書きするべきところ
protected masterUtil: ModelUtil<A> = AModelUtil;
protected masterApi: string = '/api/v1/a';
protected masterName: string = 'A';
}
</script>
中身だいぶ省略。でも実際ほぼこのくらいしか中身ないです。上書きすべきところに値を突っ込む。
ModelUtil
ジェネリックをうまく生かすねらいで、各モデル(ビューモデル?ドメインモデル?)への処理を抽象化した。
こちらも雰囲気で理解してください。clone()
を呼ぶとクローンされそうな雰囲気。
ModelUtil.ts
export interface ModelUtil<T> {
clone(target: T): T;
empty(): T;
getId(target: T): T;
fromServer(obj: object): T;
toServer(target: T): object;
}
-
MasterPageBase
では、扱うモデルの種類がわからないまま中身を書くことになる。モデルに対する処理を抽象化するためのインターフェースがこれ。「Tの正体はわからんけどコピーならできる」「Tの正体はわからんけどIDの取得ができる」状態にしておきたかったので。
AModelUtil
BModelUtil
CModelUtil
ModelUtil
を継承してクラスを作っても良かったのだけど、static
との相性が何か微妙だったので、次の2つのどちらかにしようとした。
- シングルトンにする
- 定数にする
シングルトンは大げさだということで定数にした。
export const AModelUtil: ModelUtil<A> {
clone(target: A): A {
// 略
},
empty(): A {
// 略
},
getId(target: A): A {
// 略
},
fromServer(obj: object): A {
// 略
},
toServer(target: A): object {
// 略
},
}
わかったこと・ベストプラクティス的な
👍 行数は大きく減った!
個別のページで、<script>
部分がほぼ皆無なページがいっぱいできた。大成功!
👍 プログラムの見通しも良くなった
特別な処理をする必要があったページでも、その特別な処理だけを書けばいいので、プログラムの見通しがぐっと良くなった。
👍 実装手順を手順化して、慣れてない人にも作業してもらえた
新しく.vue
ファイルを作って、
/// サブクラスで上書く
の部分を上書いて、
設計書にある通りにコンポーネント置いて、
のように、派遣の方に出す指示がかなり明確になって、慣れていない人にも作業してもらいやすくなった。
👎 「この処理ってどこにあるんですか?」と聞かれやすくなった
継承は、見かけ上そのクラスになんにも書いていないのに動くように見えますので、慣れていない人には「なんで動くの!?」と思われがちだった。
フロントエンド界隈はまだまだJavaScriptが強く、静的型付けに慣れていない人は結構いる。ジェネリックや継承などは、C#やJavaなどを触っていると当たり前だけど、そういう開発者ばかりではないので。。。
🌻 特別な処理をする際の工夫が必要になった
いくら共通化するといっても、特別な処理が必要な場合はどうしてもあって、その際どうしたか
- 引数でフラグを渡す: あんまりやらなかった
- 「サブクラスで上書きしてね」: 事前にわかっている、サブクラスで上書きする前提のフィールドやメソッドもいくつか作った。
- TypeScript(とJavaScript)は関数をオブジェクトとして使える言語なので、コールバック引数を受け取るのはよくやる方法。でも今回はあんまりやらなかった
- フックを作っておく: 「保存前に呼ばれる処理」のように、ほとんどの場合何もしないけど、あるページでは特別な処理が必要な場合が(途中で)判明して、どうしても共通親クラス側に何かひと手間加えなきゃいけない場合はこれをやった。共通側にはフックだけ作っておいて、特別な処理をしたい場合にはフック関数を上書いてもらう。
以上。似たようなことをしようとしている方の参考になりますように🎋