トップ 追記

2020-11-12 [長年日記]

_ clasp + gas-local + rollupで恐らく最も手軽なlocalでのGoogle Apps Script開発

気づいていなかったことに気づいちゃったので、よりシンプルなセットアップにしようと頑張った成果が出たので、共有。

というか

絶対にwebpackの設定はやりたくない。絶対にだ。

という気持ちがほとんど。そしてそれは(もちろん内容によるけど)

考え方を逆転させれば叶うぞ

という話。

これまでを整理

まずは前回日記で書いた時点までに分かっている GAS 開発に関する制約や考えたこと、行なったことをまとめるところから始める。

当たり前の開発を行いたい

当たり前の考え方として以下が挙げられると思う。

  • 1 ファイルにすべての function を並べるような開発は見通しが悪いのでやりたくない
  • すべてのコードをゼロから書くのではなく適切に npm などに依存したい
  • テストコードを書き、パーツごとにテストを行いたい
  • 手元のコードのバージョンを管理したい
Apps Script上のコードの制限

一方で GAS ならではの制限としては以下のようなものがある。

  • CommonJS や AMD などの方法ではコードの共有を行えない
  • 実行する function は global に定義されている必要がある
  • GAS 固有の「ライブラリ」はあるが、これを npm で管理することはできない
以前採用した解決方法

以前 GAS 開発をモダンにしたいと思った際には情報としてすでに多く出回っていた

require を GAS 上で再現する

という方向で頑張ってみた。

これはこれでよかったが、この辺りのツールチェインは残念ながら流行り廃りが激しい。

bundler選びをやめて考え方を変えてみよう

じゃあ今改めてやるとして、しかし Browserify はさすがにもう厳しいかなと思い、じゃあ Webpack にするのかなぁ、Webpack でも Parcel でも GAS 化するところには実は必ず fossamagna 氏がいて、どれでも実現はできそうではある。

ただいずれにせよこの bundler の設定自体をやりたくないんだよなぁと思っていたところ、一つ新しく気が付いたことがある。それは

Apps Scriptはブラウザ上で実行されているJavaScriptのように他のファイルの内容にアクセスできる

というもので、実は Script Editor 上で異なるタブで開いているコードで定義されている内容は import / export なしにアクセスできるのだ。ということは

Node.js 上で import / export なしに他のファイルの内容にアクセスすることができれば問題解決では?

となる。いや、言ってることが矛盾してるように聞こえるんだけど、条件を限定さえしてしまえば実現できることが分かった。それが

mzagorny/gas-local: Execute and test your google app scripts locally in node.js

だ。

実は上に挙げた Google Apps Script開発をもうちょっとモダンにしてみる - あーありがち(2017-05-27) の中でも先の fossamagna 氏の資料には触れていて、その

Apps Scriptによるより高度な開発プロセス/More Advanced Development Process with Apps Script // Speaker Deck

中ですでに gas-local には触れられているのだが、この時は正直 gas-local の意味がよく分かっていなかった。今回調べていて改めて分かったことは、gas-local を利用して

const gas = require('gas-local')
const app = gas.require(<コードの置かれているディレクトリ>, { globalObject })

app.myFunction()

と呼んであげることで、直接 export / import ( require ) していない function を実行することができる。しかもその中で他のコードに書かれている class などに依存していてもまったく問題なく動く。 のだ。これすごくない?

書き方としては

  • gas-local で読み込むファイルの置かれているディレクトリを指定してまとめて読み込むことで GAS の実行環境を作る
  • できた実行環境上の function を call する

という形になっている。

ということはこのコードさえ書けばあとは Node.js の普通のテスティングフレームワークでテストを実行できるわけだ。

gas-local利用上の注意点ふたつ

注意点の一つは上のコードに書いてある globalObject である。実は gas-local はこれを書いているバージョン 1.3.1 の時点で Node.js の VM という機能を利用していて、VM に共有するオブジェクトを渡してあげないといけないのだ。分かる人はこれだけ書けば十分話が伝わると思う。

要するに例えば

gas.require('./src', { console })

として console オブジェクトを渡してあげないと console を利用した log の出力を gas.require を呼んだ側と共有できないので、どんなに console.log() を書こうが、標準出力には何も表示されない。残念ながらここは GAS の知識ではなく Node.js の知識が必要になってくる。

もう一つは、外からアクセス可能なのは function のみということである。これは GAS の制限と同じと考えれば分かりやすい。例えば

class Klass {
}

というクラスが定義されていても上のサンプルコードで言う app.myFunction() と同じ要領で app.Klass と書いても Klass にはアクセスできない。

この解決方法は簡単で、function が必要なら function を作ればよい。

class Klass {
  constructor(..) {
    ..
  }
}

createKlass(..) {
  return new Klass(..)
}

この createKlass には外から自由にアクセスすることができる。これで Klass クラスのテストを自在に行うことができる。

gas-localを利用したGASのlocal開発

ここまでをおさらい。

  • gas-local を利用することで export / import なしに他のファイルの内容にアクセスできる GAS と同じ動作を実現できる

ということが分かった。残る課題は

  1. GAS 固有のオブジェクトは Node.js 上では動作しないので stub/mock する必要がある
  2. 外部の依存パッケージは依然として import ( require ) が必要なのでは?
  3. TypeScript と仲良くしたい

になる。

stub/mockはglobalについてはご自由に

1 については gas-local 自体が mock の機能を持っており、これで解決するのが早そうだ。この mock は default で Logger などが定義されており、また独自の mock の追加も普通にオブジェクトを組み立てていくだけなので簡単にできる。

一方でこの gas-local の用意してくれる mock は単に GAS のコードをなんとか動かせるようにするためには使えるが、結局ただの global object なので、TDD 好きとしては完全にスペック不足と言わざるを得ない。

そこで毎度おなじみの sinon で以下のように自分で定義してもよい。

const gas = require('gas-local')

const sinon = require('sinon')
const Logger = { log: () => {} }
sinon.stub(Logger, 'log').callsFake((message) => console.log(message))

const app = gas.require('./src', {
  Logger,
  console
})

app.myFunction()

ここでは事前に stub 済みのオブジェクトを渡しているが、share されているので app 初期化後に sinon 側で手を加えてもよい。stub だけだと恩恵が薄いが、sinon なのでちゃんと mock もできる。

外部の依存パッケージはrollupでiifeとして持ち込む

rollup.js

もう一度改めて。

このアプローチは考え方を逆にする。

ここまでで gas-local を利用することで local でも import / export しないで外部のコードを利用することはできた。次にやるのは

build 時に bundle するのではなく、必要な依存パッケージは事前に準備する

になる。

依存パッケージは通常 CommonJS や ES Modules として提供されているが、これをこのまま持ち込んでも結局「import / export しない」を実現できない。しかし

rollupでiifeとしてコピーすれば自分で書いた素のコードと同じ扱いにできる

のだ。

例えば Ruby の Object#dig のような lodash.get というパッケージがあるが、これを利用する場合、以下のように rollup.config.js を書いて rollup -c すればよい。

import commonjs from '@rollup/plugin-commonjs'

export default {
  input: './node_modules/lodash.get/index.js',
  output: {
    file: 'src/dig.js',
    format: 'iife',
    name: 'dig'
  },
  plugins: [
    commonjs()
  ]
}

これで .src/dig.js に dig という名前で必要な function が置かれる。できあがった dig.js をそのまま git add してやる。

複数のパッケージを持ち込みたければ

export default [
  {},
  {}
]

のようにして複数の設定を書けばよい。

※ ただし、そのままでは独自の名前空間を持ちかつそれに対して plugin システムを持つようなものをうまく動作させるのは難しそうだ。何しろ名前自体は global でも実体としては IIFE として閉じているので。その場合は plugin まで解決し終わった class や function を export するような、rollup を利用した独自のスクリプトを書いてあげる形で解決する必要がありそうだ。

IIFEとは

IIFE - MDN Web Docs Glossary: Definitions of Web-related terms | MDN

Immediately Invoked Function Expression の略で、以下のようなコードのことを言う。jQuery 時代によく見た、あまりよい印象を持てないコードだ。

(function () {
  statements
})();

ただ rollup で生成されたコードは以下のようになるので、これで GAS の機能を利用して global にアクセスできる function を持ち込むことができる。

var dig = (function () {
  'use strict';

  ..

  function get(object, path, defaultValue) {
    var result = object == null ? undefined : baseGet(object, path);
    return result === undefined ? defaultValue : result;
  }

  var lodash_get = get;

  return lodash_get;

}());
TypeScriptと仲良くするには

残念ながら gas-local は TypeScript には対応していない。つまり

const gas = require('gas-local')
const app = gas.require('./src')

としても src の中身が TypeScript だったら require してくれないのだ。そこで test を実行する前に一度 tsc でコンパイルしてあげる必要がある。設定は clasp の利用している ts2gas のデフォルトの設定を参考にすればよい。ドキュメントには

{
  isolatedModules: true,
  module: "None",
  noLib: true,
  noResolve: true,
}

のように書かれているがこれは嘘で、コードの中を見ると

{
  experimentalDecorators: true,
  noImplicitUseStrict: true,
  target: ScriptTarget.ES3,
  ..
  emitDeclarationOnly: false,
  module: "None"
}

しか書かれていなくて、下二つは必ず適用されるということになっている。もろもろ考え合わせると以下のような感じにしておくと快適に開発できそう。

{
  "compilerOptions": {
    "target": "es2015",
    "lib": ["es2015"],
    "allowJs": true,
    "module": "None",
    "outDir": "./dist/",
    "types": [
      "google-apps-script"
    ],
    "typeRoots": [
      "./node_modules/@types"
    ]
  },
  "include": [
    "./src/**/*"
  ]
}

src/ に TypeScript を置いて dist/ にコンパイル済みの JavaScript が生成される設定だ。で、先ほどの例で言うと gas-local では

gas.require('./dist')

としてコンパイル済みの JavaScript を読み込むようにすればテストを行うことができる。

もしかしたら必要ないかもしれないが、このコンパイル済みのコードを Google 上でも利用したい場合は .clasp.json に保存されている rootDir も dist に設定しておく。

また、@types/google-apps-script を利用できるようにすると Node.js 向けの Type 定義と合わない、そもそも runtime は Node.js ではないなどの問題が出るので、lib と types で絞っておくのがよいようだ。

まとめ

Webpack などの asset bundler を利用しなくても GAS の local 開発をモダンに行うことはできる。

  • GAS の仕様に合わせて import / export を使わずにコードを書いて、テスト時には gas-local と組み合わせるとよい
    • gas-local の生成する VM では function しか呼び出せないので、class を定義した場合はインスタンス生成用の function も定義すること
    • Google 上ではなく local での動作になるので Google の runtime 上にしか存在しないオブジェクトは stub/mock してあげる必要アリ
  • 依存パッケージは rollup などを利用して iife として持ち込むとよい
  • gas-local は TypeScript に対応していないので TypeScript を利用したい場合はテスト実行前に自前で tsc で変換する。ディレクトリ丸ごと扱わないといけないので ts-node ではダメ。

以上をいい具合に package.json に定義してやると、結構快適に開発できる。

ここで「rollup で iife にしてリポジトリに持ち込むとかダサくね?」とか「もっと普通に Node.js の流儀で書けるべき」とか「Webpack の設定くらい書けて当然でしょ」みたいな考え方もあるかもしれない。そう考える人はそうすればよいのだが、自分はこのやり方の方がよいと考えている。判断のポイントは

どちらの API の方が寿命が長いか?

である。

Webpack などの bundler は恐らくまだまだ変化する。一方で GAS の API は今回の大型アップデートである V8 runtime の追加が行われても基本的には何も変わらなかった。gas-local の開発も3年前で止まっているが、なんら問題なく利用できる。

rollup は利用しているがこれは後発の bundler の割にかなり安定した開発になっており、今回の目的に利用している API も恐らく変わることはほぼないだろう。変えるメリットがないはずだ。

以上を踏まえて、あえて Webpack などの利用を避けていくのがよい選択なのではないかと考えている。

※ もちろん HTML Service を利用した Web アプリ開発に GAS を利用するのであれば Webpack の利用はよい選択肢になると思うが、最近自分はそもそも GAS を Web アプリとして使うこと自体に否定的な考えを持っている。Workspace add-ons が登場し、そこで利用できる Card Service が登場した。Card Service はデスクトップ向け Web UI にもモバイルアプリにも適用することができる。(残念ながらモバイル向けの Web UI には適用できない。)Google の考える未来の体験としては この Card Service を通じて HTTP で何らかのサービスと通信しながら機能を果たすものになるのだろう。Web アプリは GAE で作って Cloud IAP で制限を掛ける方がよほど素直に作ることができるので、今後は無理に GAS で Web アプリを作るのはやめた方がよさそう。

サンプルリポジトリ

wtnabe/example-clasp-gaslocal-rollup-typescript


2020-11-11 [長年日記]

_ Google Apps Scriptを取り巻く環境のおさらい

claspを利用した開発がそれなりにまともに動くと言ってよい状況になりそうなので、前提の情報をまとめておく。

これまでのApps Scriptの開発はパッチワークだった

以前 Google Apps Script + Node.jsで簡単なツールを作ってみた - あーありがち(2015-10-31)Google Apps Script開発をもうちょっとモダンにしてみる - あーありがち(2017-05-27) で取り上げた際には以下のような状況だった。

  1. 実行は Script Editor 上で手作業で行うかそれを実現する Web API を publish しておく
  2. ログは Script Editor 上で表示するか BetterLog - Google Apps Script Examples などを利用して Spreadsheet 上で確認する
  3. Drive API を利用して Standalone Script を local <-> remote ( Google ) で同期が可能
    • いわゆる local 開発が可能と言われていた部分はここ。この local 開発の成果物を VCS で管理できるという話
  4. テストの自動化 は 1 と 3 を組み合わせて local 開発しつつ remote でそのまま動かす Home | QUnitGS2 のようなアプローチか、リンク先で挙げたように stub/mock しまくるかのいずれか
  5. 4 をどこまで頑張るかは置いておくとして GitHub ポチーで merge してリリースすることはできる
  6. GAS側のバージョン(Herokuのdeployment IDのようなもの)管理は手動

なんとか、あれとこれを組み合わせればできんことはないですよ、というレベル。しょっちゅうやっているならともかく、たまにしかやらないとまぁ忘れてしまうし、手間が掛かる割にそこまでよくなっていないようにも見える。

結局、手元でやりたいことの多くは API アクセスではなくて純粋なロジックの部分だけだったりするのでそこには Node.js 流の開発手法を持ち込みたいけど、いざ実行して確認するためには GAS 固有の手法もちゃんと頭に入れた状態で行う必要があって、やってることはそんなに複雑でもないはずなんだけど妙に頭のリソースを使うものになってしまっていた。

※ また Google Apps Script開発にstaging環境を用意してContinuous Deployment - あーありがち(2017-06-19) で説明したように、production と staging などの情報をいわゆる普通の Web ホスティングのように環境変数で設定するようなことはそんなに簡単じゃないので、逆に CI までその情報を引っ張ってきて CI 上でどうにか頑張って判別して前処理を行うなどの涙ぐましい努力が必要だった。この部分は Cloud Build を利用することで多少マシにはなる。また Cloud Build を前提にするのであれば、環境変数しか利用手段のなかった秘密情報の部分については Secret Manager を利用することでかなり管理しやすくなる。これは Apps Script 周りの話ではないが、Google のインフラ全体が改善されたことで得られたメリットなので補足として挙げておく。参考は あーありがち - GCPのサービスアカウントの鍵を作って持ち出す必要がなくなった

Apps Script APIが登場した

2018年頃の話だったかな。

これからは clasp やでぇ、試してみたでぇという話で溢れかえった。

実際にはツールとしての clasp 以前の話として API が追加されていて、そっちが重要になってくる。大きく変わった部分は以下の二つ。

  • Apps Script API が登場し、Apps Script Dashboard というもので Apps Script の情報を俯瞰できるようになった
  • Apps Script Project というリソースを整理し、これを GCP Project と紐づけることで他のアプリと同じように Stackdriver で実行の状況を管理できるようになった

Apps Script API  |  Google Developers

これまでは Apps Script の情報を取得するのに Drive API しか使えなかったため、Apps Script だけを抜き出した画面などは存在しなかったが、Apps Script API の登場でだいぶ管理しやすくなった。

また GCP Project と紐づけることができるようになったことで、これまで Script Editor 上でしか確認できなかったログの情報、エラーの情報を他の GCP Project と同じように Stackdrive で管理できるようになった。これは非常に大きい。やっとまともにログを扱えるようになったと言っていい。

加えて script の run が API で可能になった。つまり Web API の publish という無理やりな技は不要になった。

そしてclasp

この Apps Script API を利用する CLI として clasp が登場した。これまでバラバラだった

  • script の読み書きとそれに必要な credentials の取得
  • script の実行
  • script の GAS 上でのバージョン管理
  • project の deploy ( add-on として配布できるようにするとか )

などがすべて手元の CLI から可能になっており、開発体験は非常に良好になったと言える。

加えて clasp push の際には grant/ts2gas: A function that transpiles TypeScript to Google Apps Script. を利用した TypeScript から Apps Script への変換も自動で行われるようになったので、clasp を利用した Apps Script の開発はモダンな書き味での読み書きを提供してくれる。

※ ついでになってしまうが 2020年に V8 runtime が登場したことにより、Script Editor 上でもそのままモダンな書き味で Apps Script を読み書きできるようになった。Script Editor 自体のシンタックスハイライトなどの対応レベルはまだ改善の余地があって、それは2021年に行われるようだが、ts2gas が ES3 に変換を行うことで Script Editor 上では非常に古めかしく刺々しいコードになってしまうという問題はいずれ解消されるかもしれない。(個人的には ts2gas を使わない方法にしてしまう方がよいと思っているのであまり関係ないが。)

cf. Apps Script’s new V8 runtime | Google Cloud Blog


2020-10-24 [長年日記]

_ fogをやめてShrineにしてみた

長いこと、Ruby で単にストレージアクセスを抽象化するためなら fog が有力候補だと思っていたのですが、どうもそんなことねーな?と気づいた話。

fogとは

fog - The Ruby cloud services library

fog は storage に限らず cloud resource に対する API の wrapper として機能するもの。そこそこ歴史もある。

例えばこれを使うと理屈上 S3 へのアクセスも Google Cloud Storage へのアクセスも provider 設定の切り替えと認証情報のセット、プロジェクトのセット以外は全部同じように動く。はず。

Shrineとは

Shrine &#183; File attachment toolkit for Ruby applications

Shrine は cloud 全般向けではなくて、Rails の model とかにファイルを添付する、その際に cloud storage も利用できるようにするためのもの。

やりたいこととfogの何で困ったのか

今回やりたかったことは Rails でもなければ Model とファイルの紐付けでもなんでもない、単にクラウドストレージのオブジェクトを読み書きしたい、それを local で手軽にテストしたい、という話だったのでファイル添付系の gem は最初から除外して考えていた。具体的には ActiveStorage とか CarrierWave とか Shrine とか Paperclip とかその辺。

で、じゃあ fog かと思って使い始めて fog-local でテストをがっつり動かしていざ fog-google に差し替えましたとなって急に雲行きがあやしくなった。

というのも、fog の API は

  • directories
    • directory
      • files
        • file

という階層を持つ形になっているんだけど、GCS 上で(権限不足が起きている際に)files がめちゃくちゃ遅くてtimeoutしてしまって何が起きているのか分からな

加えて、HTTP HEAD method を使ったオブジェクトの存在確認の API が GCS に存在しない。

ということでそもそも fog の API デザインが合わないという結論になった。

※ 実は GCS 側を S3 互換にして S3 の API を叩くという方法もあるんだけど、そうすると認証方法も変更になってしまうのでこれは避けた。具体的には GAE から GCS を叩く際に、イマドキは application default credentials というものがあるので、同一プロジェクト内の resource へアクセスするのであれば、アプリ側で認証情報に関して一切気にする必要がないんだけど、API を S3 互換にすると認証も S3 互換になるっぽいので、だいぶ面倒だなと思ってそこで考えるのをやめることにした。

[2020-10-29 追記] これについてはPermissionのエラーが確認できたのでfog固有の問題ではないようだ。ただしtimeoutでエラーの内容が確認できなくなってしまうという状態は非常に困るので、載せ替え自体は妥当な判断だったと言えるだろう。

Shrineは単なるオブジェクトアクセスでも便利

Shrine は Model へのファイル添付向けに Shrine::Attacher や Shrine::Attachment というものがあって、その例がトップページに掲載されているんだけど、実は単なるストレージアクセスを実現する Shrine::Storage という名前空間も存在している。この辺は fog などの他の gem に依存した実装になっているのかと思っていたがそうではなく、Shrine が独自に抽象化していて、この Shrine::Storage 以下の

* upload
* open
* exists?

のメソッド群を使うと、File.write や File.exist? みたいなことが簡単に実現できるので、サクッと載せ替えてしまいました。とさ。

Tags: Ruby GCP

2020-09-27 [長年日記]

_ NestJSでInjectされるオブジェクトの初期化のタイミング

実はここのところ NestJS を触っていました。これについての感想などはまたいずれ書くとして、DI コンテナそのものに不慣れだったので NestJS の DI でとても悩みましたという話を少々。(例によって分かってしまえばどうということはないんだけど)

※ 一応間違いないはずだけど、思い違いがあれば教えてください。

以下に(module定義を除いて)DI コンテナに登録する provider とそれを利用するコードを挙げる。仮に以下のようなコードだったとして、実際にどの部分のコードがどの順番で実行されるのかを確認し、それによって生じる制限と、その制限を克服する方法を整理する。

コード例

https://nestjs.com/

NestJS は TypeScript で動いてます。

@Injectable()
export class Dependee {
  constructor () {
    (1)
  }
}
import { Dependee } from '..'

export class Depender {
  constructor (
    dependee: Dependee
  ) {
    (2)
  }
}
  • Depender が Dependee に依存している
  • Depender の constructor で Dependee が inject される

実行順

  • Dependee の constructor が実行され(default の provider の挙動)て、インスタンスが生成される
  • 生成されたインスタンスが Depender の constructor で inject される

できないこと

常に Dependee が先にインスタンス化されるので、Depender の constructor で取得できる値を Dependee の constructor に渡す方法がない。

具体的にどう困るのか

例えば Controller で @Query や @Param で取得した値から「特定の値を持つ provider のインスタンスを生成する」ことは組み込みの DI の機能では実現できない。これは Controller の constructor が実行されるタイミングですでに Dependee のインスタンスは生成されていて、Action に該当するメソッドが呼ばれるタイミングは二周遅れになっているため。

ではどうするとよいのか。

Dependeeに値を渡す方法

どうにかして値を渡したい場合、どうするのがよいのか。方法としては大きく分けて2つ、全部で4つくらいありそう。

  1. 最初にあり得る値をセットするインスタンス生成法を全部 DI コンテナに登録しておいて必要なものを必要な人が取得する
  2. Inject の機構を利用せずに Depender の constructor の中で手動で Dependee をインスタンス化する
    • DI になっていない*1
  3. 実行する処理そのものが書かれているメソッドに渡す
    • property ではなくメソッドのシグニチャで class の特徴を表すことになる
    • 複数のオブジェクトにそれらを渡しながら処理していく場合にインターフェイスの変更が高コストになるのでカッチリ設計が固まっている場合以外は工夫が必要。例えば context オブジェクトを導入するにしても今回の constructor の実行順の問題は同様に残る。
  4. constructor に渡すのは諦めてsetter を用意して setter で値を渡す
    • この場合、Dependee の該当 property は readonly にはできなくなる
    • 言語の機能で immutable にはできないので、どうしてもこだわるなら何らかの工夫が必要
どうしてもインスタンスの初期値を最初に消めたのちは途中で挙動が変わってほしくない場合

1 は DI コンテナの機能も素直に活かしつつ、初期値で挙動を固定して途中で変わってほしくないという要望も完璧に満たすことができる。具体的には NestJS では provider 定義に useFactory を使うとこうしたことが可能となる。

Custom providers | NestJS - A progressive Node.js framework

ただし、例えば日付やお金など値の範囲が無限になってしまうものに対してはこの方法は使えない。曜日くらいなら可能。

2 は Dependee の挙動をインスタンス生成時に固定するという目的に対して最も解決が早く、新たな学習コストがない。ただし、用意された DI の機能は使っていないのでちゃんと依存関係の管理ができているかというとあやしくなってくる。

インスタンスの挙動が変わることを許容できるなら

いちばん素朴で素直なのは 3 かな。インスタンスそのものが何をするのか知っているという形ではなくなってしまうけど、その部分の責務をすべてメソッドに担わせる形。

副作用として interface でちゃんと設計を練ってあげてそれを type として利用するようにしておくと class そのものが変わっても耐えられる。こうなると本当に DI っぽい。

4 は 3 と似てるけど setter があるとメソッドの呼び出し順に依存するので避けられるなら避けた方がよさそう。setter で丸ごと放り込むのが雑にやるには早いのは早いけど。

*1 とは言え DI コンテナを使えば DI なのかと言われるとやや疑問は残る。依存先のオブジェクトの生成方法は隠蔽されているが、interface ではなく provider の実装そのものを type として指定している場合に、どこまで DI と呼んでよいのか…? いずれにせよ絶対にコンテナに乗っていないといけない、あるいは乗っていればよいと考えるのも何か違う気がする。ただし、NestJS の場合は DI コンテナに依存するために module 定義に乗せておくと compodoc https://compodoc.app/ というツールで依存関係を visualize できるので、これは大きい。また、どこで何を利用しているのかが実行前に分かればリファクタリングは行いやすい。