おはようございますこんにちわこんばんわ。どうもぶたです。
ライブラリをラッピングすると便利だという記事を関数メインに書きました。
今回はクラスを用いたラッピングに触れていこうと思います。
なお、前回に引き続きTypeScriptで書いて行きます。
ラッピングのメリット
基本的なモチベーションやメリットは変わらないので、大枠は前回の記事をご参照いただければと思いますが、下記のようなメリットがあります。
- インターフェースをよりシンプルにできる
- 影響範囲を限定的にできる
- 独自ロジックの追加が容易
クラスでライブラリをラッピングとは
関数と同様に、インストールしたライブラリを直接利用せず、クラスでラッピングすることですが、
これはAdapterパターン(あるいはWrapperパターン)というデザインパターンの一つです。
ラッピングしたクラスや関数をラッパークラスとかラッパー関数と呼称したりします。
Adapterパターン(Wrapperパターン)がラッピング?
Adapterパターンを用いると、既存のクラスに対して修正を加えることなく、インタフェースを変更することができる。Adapterパターンを実現するための手法として継承を利用した手法と委譲を利用した手法が存在する。
この
「既存のクラスに対して修正を加えることなく、インタフェースを変更することができる。」
という点を、
「ライブラリに修正を加えることなく、利用方法を変更することができる」
と解釈してもらえるとラッピングに繋がると思います。
ラッピングには継承じゃなくて委譲を利用する
Wikiからの定義にて「継承を利用した手法と委譲を利用した手法が存在する」と書かれていますが、
ライブラリのラッパークラスにおいては、委譲を利用する手法を選択したほうが効果的だと思います。
継承は、extends
を用いて既存のクラスの機能を引き継ぎます。
委譲は、既存のクラスをメンバーとして保持して内部で利用をします。
AWSのS3操作を題材に説明して行きたいと思います。
import { GetObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
export class HogeS3Client extends S3Client {
async putObject(params: { Bucket: string; Key: string; Body: File }) {
await this.send(new PutObjectCommand(params));
}
}
///////////
const s3Client = new HogeS3Client()
// HogeS3Clientに独自でラップしたメソッド
await s3Client.putObject({ Bucket: 'hoge', Key: 'hogehoge', Body: file })
// S3Clientで用意されているメソッド
const response = await s3Client.send(new GetObjectCommand({ Bucket: 'hoge', Key: 'hogehoge' }))
「いちいちラッピングせずに使えるなら便利じゃないか」と感じるかも知れませんが、
インターフェースをシンプルにできるどころか、メソッドを増やしているため逆に複雑にしてしまっています。
影響範囲も限定的にできておらず、もしバージョンアップで修正が必要となると、
呼び出し箇所を洗い出して、独自のメソッドがライブラリのメソッドか判断して、、、と、結構な手間になることが予想できると思います。
import { GetObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
export class HogeS3Client {
constructor(private readonly client = new S3Client({})) {}
async putObject(params: { Bucket: string; Key: string; Body: File }) {
await this.client.send(new PutObjectCommand(params));
}
}
///////////
const s3Client = new HogeS3Client()
await s3Client.putObject({ Bucket: 'hoge', Key: 'hogehoge', Body: file })
// sendはHogeS3Clientに存在しないため利用できない
// const response = await s3Client.send(new GetObjectCommand({ Bucket: 'hoge', Key: 'hogehoge' }))
委譲を利用すると、定義したインターフェースのみが提供され、シンプルに、そして限定的にできます。
インターフェースはシンプルに保つ
独自のロジックを組み込んでライブラリの利用を何かの機能に特化させたくなることがあると思います。
ただ、ラッパークラスにおいてはあまり特化させないほうが使いやすいと感じています。
ライブラリの処理を純粋にラッピングするだけに留めて、メソッド名もそのラッピングした処理に直結する形がおすすめです。
import { GetObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
export class HogeS3Client {
constructor(private readonly client = new S3Client({})) {}
async putObject(params: { Bucket: string; Key: string; Body: File }) {
await this.client.send(new PutObjectCommand(params));
}
async upload(key: string, file: File) {
const params = { Bucket: 'hoge', Key: key, File: file };
const response = await this.client.send(new PutObjectCommand(params));
return response;
}
async uploadToHoge(file: File) {
const timestamp = dayjs().format('YYYYMMDD');
const params = {
Bucket: 'hoge',
Key: `${timestamp}.json`,
File: file,
ContentType: 'application/json',
};
await this.client.send(new PutObjectCommand(params));
return params;
}
}
uploadToHoge()
のようなputObejct()
と似ているけど特化した処理は、
用意するにしてもこのラッパークラスを利用する他のクラスに任せたほうが良いです。
また、メソッド名は「S3にアップロードする」とか言ったりもするのでupload
でも間違いではないと思いますが、
client.send(PutObjectCommand)
の形でライブラリの利用方法としてupload
という単語が出てこないので、
putObject
を活用するのが最も直感的なのかな、と。
余談ですが、このように似た処理が少しずつ増えてしまうのなんでなんでしょうね。
気を付けて行きたい所存。
最後に
クラスを用いたラッピング方法についてまとめてみました。
今回題材に用いているS3、というかAWS SDK for JavaScriptはv3が3年ほど前に公開されましたが、
v2からの変更点がなかなかに大きかったことを記憶しています。
少しでも皆様の開発の助けになれば幸いです。