TypeScriptを用いて実装しているプロジェクトの中で使用するライブラリの選定を行った時のこと
ライブラリをそのまま呼び出さずにラッパーを作成すると良いとのことで、色々調べながら実装したのでその覚え書きです。
目次
ラッパーとは
ラッパー(wrapper)とは、関数やクラス・ライブラリなどを包み込む外側の関数やクラスのことです。
例えば、関数を包み込む関数を作成することをラップすると言います。
今回は、TypeScriptでライブラリ丸ごと全部ラップするものを作成します。
ラッパーを作ると何が出来る?
そもそも、なぜラッパーを作る必要があるのか?作るとどんなメリットがあるのか?について整理していきます。
ラッパーの作成メリットの一つとして、ラッパーを作成することで仕様変更への対応コストが下がるという事が挙げられます。
関数・クラスをラップすると、元の関数に何か変更が生じた際に外側のラッパーに修正を加えるだけでプログラム全体の修正は行わずに済むようになります。
例えば、以下のようなstringとnumberの引数を持つ関数があるとすると
const func = (arg1:string, arg2:number) => {
// 何かの処理
}
hello("hello", 3);
ラッパー関数はこのようになります。
const wrap = (arg1:string, arg2:number) => {
func(arg1, arg2);
}
wrap("hello", 3);
プロジェクト全体では、func関数は使わずラッパー関数であるwrapを呼び出すようにしておきます。
もしfunc関数に仕様変更が発生した際は、下記のように修正を加えます。
const func = (arg1:string, arg2:number) => {
// 何かの処理
}
// ↓引数arg2の型が変更された
const func = (arg1:string, arg2:string) => {
// 何かの処理
}
// ラッパー関数のwrapを修正する
const wrap = (arg1:string, arg2:number) => {
func(arg1, String(arg2));
}
// 全体からの呼び出し方は変わらない
wrap("hello", 3);
もしプロジェクト全体でfunc関数を直接使用していたら、全体を見直して呼び出し箇所全ての修正が必要なところですが、ラッパー関数を作成していたことでwrap関数内の処理を修正するだけで対応出来ています。
このように、ラッパーを作成することで元の関数・クラスなどの仕様変更に強くなります。
他にも、使いにくい関数を自分なりにカスタムしたり、元の関数の処理の前後に特定の処理を加える(ログの吐き出しやパラメータチェックなど)ことも出来ます。
ライブラリをラップする
本題のライブラリのラッパーについてですが、
外部ライブラリは頻繁にアップデートが来ることがあるし、その時に大幅な仕様変更が加わることもあります。
また、使用していたライブラリ自体の使用をやめて類似する別のライブラリを使用するようになるという事も考えられます。
その度にプロジェクト全体のコードを修正するのは大変ですが、ラッパーを作成することでラッパー内部の実装のみ修正すれば良くなります。
実際に作成する
実際にライブラリのラッパーを作成していきます。
ライブラリが関数で実装されているものであれば、上記の通り使用する関数に対して一通りラッパー関数を作成します。
クラスで実装されている場合は、ライブラリのクラスをラップするクラスを作成し、メソッド・型も一通りラップしたものを用意します。
では、実際にクラス形式で提供されているDay.jsライブラリのラッパーを作成してみます。
import dayjs, { type Dayjs } from "dayjs";
// 初期化用の引数の型をラップ
interface ConfigTypeMap {
default: string | number | Date | Dayjs |
null | undefined;
}
export type Props = ConfigTypeMap[keyof ConfigTypeMap];
// クラスをラップ
export class MyDateTime {
datetime: Dayjs;
constructor(config: Props) {
this.datetime = dayjs(config);
}
// メソッドもラップ
toString(): string {
return this.datetime.toString();
}
}
// 呼び出す時は
const datetime = new MyDateTime().toString();
まず、ライブラリ全体をラップするクラスを作成します。
ライブラリのインスタンスを作成し、ラッパークラスのメンバとしてdatetimeという変数で管理するようにしています。
export class MyDateTime {
datetime: Dayjs;
constructor(config: Props) {
this.datetime = dayjs(config);
}
}
この時、ライブラリではコンストラクタを呼び出す引数の型が定義されていたので、その型も自作の型として定義し直しています。
メソッドの呼び出し等で使われる型定義があれば、それらも同様に自作の型として定義し直しておきます。
interface ConfigTypeMap {
default: string | number | Date | Dayjs |
null | undefined;
}
export type Props = ConfigTypeMap[keyof ConfigTypeMap];
そして、ライブラリで提供されている各メソッドもそれぞれラップしたものを定義します。
toString(): string {
return this.datetime.toString();
}
ここではただライブラリのメソッドを呼び出しているだけですが、必要に応じてメソッド内で追加の処理を入れたり、ライブラリでは提供されていないメソッドを追加することも可能です。
これで、例えばライブラリのバージョンアップによってtoString関数の引数にフォーマットの指定が必須になったら
toString(): string {
const format = 'YYYY/MM/DD HH:mm:ss';
return this.datetime.toString(format);
}
// 呼び出す側は変わらない
const datetime = new MyDateTime().toString();
このようにラッパーの修正のみで呼び出し側の修正は不要になります。
以上、TypeScriptでライブラリのラッパーを作成してみました。
まとめ
- ラッパーとは関数やクラスを包み込むもの
- ラッパーを作成すると仕様変更に強くなる
- ライブラリのラッパーでは関数・クラス・メソッド・型全てをラップする
使用していたライブラリが使えなくなるなんてことは起きないに越した事はないと思いますが、ライブラリを探していると久しく更新が止まっているものも見掛けますし、いざという時に対応しやすい実装を初めからしていけると良いなと思います。
また、今回はラッパーの仕様変更への強さの部分に着目しましたが、途中でチラッと書いたように
元々の関数が使いにくい場合に自分が使いやすい形にラッパーを作成したり、元の処理の前後に足りない処理を追加したり、複数の処理を同時に呼び出せるようにしたりなど、仕様変更に備える以外の使い方も沢山あります。
良いライブラリなんだけどなんだか使いにくい、なんていう時もラッパーを作成することで選択肢が増えるかもしれません。
Join our team!
ご覧いただきありがとうございました。テラドローンではエンジニアを募集しています!一緒にテラドローンで技術のアップデート・社会に役立つプロダクト開発を行っていきたい方
ぜひCasual Talk へ気軽にお申し込みください。
SUZUKI.m
webエンジニア/運行管理開発部
毎日勉強中です
Golang / React / AWS / Python