Google Apps Scriptでクラス構文を活用する【GAS】

V8 Runtime対応になったGoogle Apps Scriptですが、長年ES5での制限を受けてきた故に、ウェブ上でのコード紹介でも殆どが平打の関数を構築して呼び出すといった事による、原始的なコード記述も多く、またライブラリの場合ワンクッション入るせいか実行が遅いということで、コードの再利用性などが積極的に進んでいない現状があります。

元々大規模開発等ではGASは使わないという面もありますが、無理してクラスを使わなくても十分なアプリを構築可能であるため(小規模が多い為使うまでもない)、スルーされがちです。今回はV8から使えるようになったクラスについて、使い所や実際の現場で使うようなシーンを想定して掘ってみたいと思います。事前に以下のエントリーのClass構文も読んでおくと良いでしょう。

※自分自身は、GASについてはES2023までは対応してるのを確認しています。

Google Apps ScriptのV8 Runtime対応を検証してみた【GAS】

今回使用するサンプルファイル

Google Apps Scriptでクラス構文を使うためには、「V8ランタイム」はオンにしておく必要があります。プロジェクト設定を開き、「Chrome V8 ランタイムを有効にする」にチェックを入れましょう。以前はV8ランタイムでクラスを利用すると非常に遅いという事があったのですが、現在は解消されている模様です。

今回は固定資産の減価償却を計算するシートでclassを使ってみたいと思います。現時点ではES2019辺りまでの仕様は使えるみたいで、プライベートフィールドやプロパティなどのES2022からの仕様などはエラーとなり使えません

※現在、ファイルの位置に関係なく定義したクラスは利用出来るので、クラスのファイルよりも下で呼び出さないとエラーになるといった現象はありません。

図:V8ランタイムで利用が可能になります。

クラスの使い所

関数のみでは駄目なのか?

単純なスクリプトであるならば、Classを使うメリットは殆どありませんが、ある程度の規模のプログラムになってくると、堪える。という話をよく聞きます。関数をいくつも作って、それを呼び出してプログラムを作るというスタイルでは何故堪えるのか?という視点で考えます。

今回のテーマのように固定資産の減価償却だと、個々の固定資産が持ってる情報はすべて必要でありながら、中身が異なっています。償却期間であったり、償却法、取得原価等。これらを回すにあたって、関数だけで行う場合、スプレッドシートとの読み書きが頻繁に発生する事となるため、大きなグローバル変数で配列を用意して、そこにこれらの設定値(プロパティ)を用意して、1個ずつ格納が必要になります。

さらに減価償却にまつわる様々な関数や処理、現在の設定値の取得等をするにあたっても、それらを考慮する必要があります。

この時、設定の塊を1つのテンプレートとして用意し、そこにまつわる専用の関数をメソッドチェーンで呼び出したりする為に使うのがクラスです。Google Apps ScriptだとGASで用意された各種クラスを皆さん普段から使っていたりします(JSのnew Date()もクラスと言えます)。

new Classでインスタンスというのを作るとこのテンプレートの塊が定義されて、個々に償却期間や償却法、原価取得がそれぞれ定義、それを処理する関数はclassを渡して上げればより簡単に処理が出来るわけです。

クラスの中身はどうなってるのか?

減価償却の場合、個々の固定資産の持つ情報や様々な計算方法などを一塊にしたものを用意するわけですが、情報は多岐に渡る上に償却法が異なる等、実際にこれをスプレッドシートを使って、年毎に資産毎に読み書きをしてると非常に煩雑になってしまいます。

クラスの基本的な構造は以下の要素で成り立っています

基本中の基本

クラスを作るとなった場合にまずは一番外側を作ります。そしてそれを、new classnameにて個々にインスタンスというものを作ってから変数に格納。その変数に対して、メソッドチェーンで関数を呼び出したり、命令を与えると計算結果や現在の値が返ってくるという仕組みになっています。

コンストラクタ

必ず定義する必要のあるのが、コンストラクタ。インスタンスを作成する時に引数で受け取って、個々の要素のプロパティというものを格納しておくものです。ここに、固定資産の各種値を入れておく事が可能です。個別の資産毎に作成します。

また、ただ引数で受け取って格納するだけでなく、計算をして初期値を格納する事も可能。以下の事例では償却補償額を引数では受けずに、取得価額 × 保証率で計算して、this.hosyouに格納しています。コンストラクタ内に定義した変数はパブリックになります。var _test = 0.5みたいな書き方でプライベート風に出来ますが、コンストラクタ内でだけしか使えないです。

※コンストラクタの外側にグローバルプロパティを作る事は出来ません

この時、変数assetに格納された内容をconsole.logで出力すると以下のようになります。asset.s_nenとして出力すれば償却年数の値を取得する事が可能になります。逆に、asset.kagaku = 2000000として実行すれば、プロパティの値を変更する事が可能です。

関数を定義する

クラスで利用する関数を定義する事が可能です。クラス内に存在するthisの変数を利用し、引数を取って計算する仕組みを作ってみます。getAssetNameという関数を定義して、資産名を取り出して返すだけの関数です。

asset.getAssetName()にてこの関数が呼び出されて、プロパティの値を取得する事が可能です。

staticメソッド

変動することの無い予め格納しておいた値を返す為だけに利用するタイプで利用するもので、staticと付けて関数を定義するだけです。ただし、thisが使えない為、プロパティ等にはアクセスが出来ません。ちょっと変わった要素と言えます。

今回は3匹のモンスターを定義し、それぞれのインスタンスを生成。それらの持つ経験値の平均を取る為のstaticメソッドで計算して返すというものを作ってみました。

このように、thisでコンストラクタ内のプロパティを利用して計算する為のものというよりも、クラス自身が持ってるユーティリティな機能として実装する場合に使うといった感じです。故にクラスですが通常のライブラリのメソッドのような動きをする、そのクラス特化の関数を格納しておくものとして定義すると良いでしょう。そのため、インスタンス化せず、いきなりmonster.avgExp()として利用できます。

計算結果としては、3668が返ってきます

プライベートプロパティ風な手法

Google Apps Scriptの場合、現時点ではコンストラクタの外側に変数定義をしたり、クラス内部でだけ利用するプライベートプロパティを利用する事が出来ません。そのため、コンストラクタ内の変数には簡単にアクセス出来る反面、簡単に書き換えも出来てしまう為、安全面では難があります。

しかし、以下の即時関数 + Symbol  + クラスを利用した場合には、外部からの値を格納後には簡単に書き換えも出来ないようにする事が可能です。次項のセッターを使ったりゲッターを使わないと取得できないように制限を課すことが可能なので、この手法はGASでクラスを使う場合には必須のテクニックかもしれません。

  • クラスを即時関数で囲っておく(即時関数の変数名とクラス名は一致させておく)
  • 即時関数内ならば、グローバルで変数を定義出来る
  • ただし直接変数名を定義して値を格納するのではなく、Symbolを使って固有のIDを生成しておく。
  • クラス内のコンストラクタではthis[Symbolの変数]にて、プロパティを定義し、コンストラクタの引数がインスタンス生成時の引数になる
  • ゲッターであるItemNameを使わないと直接格納された情報にアクセスは出来ません。セッターについても同じ。
  • 変数を呼び出す時も、this[Symbolの変数]にて呼び出し、計算してreturnすれば計算結果として返すことは可能です。
  • 上記に於いてconsole.logの出力結果は以下の通りになります。console.logで変数名だけだと何も出力されず、またitem.nameとしてもコンストラクタの変数に格納した値は取れません。

この仕組を使うと、外部から途中の変数を取得したりが不可能になるので、例えばOAuth2.0認証のAccess Tokenの取得であったり、その暗号化・復号化といった処理をクラスに作り込めば隠蔽できるので、安全性も高まります。

図:理想の形で値の取得を制御出来た

ES2022のクラスの新機能を調査

2024年6月、現在のGoogle Apps Scriptがどこまで最新のJavaScriptに対応してるのか?を調べてみたところ、2023まで追従してることが判明。ということは、ES2022で追加されたプライベートプロパティにも対応してるのでは?ES2022のClass FieldsやPrivate Propertyなどのクラスの機能が使えるようになってるのでは?ということで、追加調査してみました。

ES2023まで対応してるといっても、Class構文についてはES2023対応ではないようです。

ゲッターとセッター

通常、コンストラクタに定義したプロパティの内容は、例えばitem.nameといったものや、item.getName()といった形で簡単に取得する事が可能です。逆を行えば値をセットすることも可能。なので、敢えてゲッターやセッターという仕組みを使用せずとも、目的の処理は実現できますが、前述のように入力制限を課してるクラスの場合は直接値を取得できません。

そこで getやsetという文字を付けた関数を用意する事で、それぞれ値の取得、値の書き換えを実現するようにコードを組み、ゲッターとセッターからしかアクセスさせないようにすれば、より安全なクラスを作る事が可能です。ただし、コンストラクタ内のプロパティは読み書きが出来てしまうので、一度格納したら変更できないようにする場合は、上記のコードで言えばセッターで参照させなければOKです。

アクセッサプロパティと総称されることもあります。

ゲッターにて、item.nameとしても前述の手法の場合、プロパティの値は取得できません。必ずゲッターを経由してitem.ItemNameとして参照しないと取得が出来ない。同様にセッターを経由しないと、item.name = "お塩"としても、値は書き換わりません。

クラスの継承

クラスは1度定義したら基本的にはその構造を変更する事が出来ません。しかし、例えば固定資産の減価償却のクラスを定義した後に、それらと共通するような内容+αの「社用車クラス」というものを定義したくなったら、別個に作らなければならないとなると、もったいない(社用車だって減価償却する)。

ということで下敷きになる「減価償却クラス」をベースに、社用車クラスを作って機能を追加してみたいと思います。ただし、継承⇒継承と継承しすぎてわけが分からなくなるような運用は辞めましょう

extendsで継承

extendsは、親となるクラスに対して、関数などの機能を追加する場合に利用します。そのため、新規にコンストラクタを定義したりは出来ません。新たに継承で定義したクラスでは、親のクラスを利用しつつ、追加した関数で演算させるといった事が可能になります。

上記のコードでは、assetman2という親となるクラスを定義しつつ、継承するsyayoucarをextends assetman2で継承しています。このクラスからインスタンスを生成し、asset.resale()を実行すると、きちんと取得価額×1.2の金額が計算されて返ってきます。

親のクラスでインスタンスを生成していない点とコンストラクタが無い点がポイント。親のクラスでインスタンスを生成しても、resale関数は当然実行されません。新たにコンストラクタを定義してしまうと、親のコンストラクタが消えてしまいます。

superでオーバーライド

前述のextendsの場合、新たにコンストラクタを追加出来ないので、関数等の機能の追加だけしか出来ません。親のコンストラクタに新規に追加しつつといった場合には、オーバーライドと呼ばれる継承を利用します(親のクラスを上書きするクラス)。今回のケースだとこちらのほうが目的に合っています。

親に加えて、走行距離であったり修理費用などといったプロパティを加えてみたいと思います。

  • syayoucar2クラスで、assetman3を継承しつつ、super()にて親のコンストラクタも継承する
  • 必ず先にsuper()で継承してから、追加のコンストラクタを定義しないと、エラーになる
  • syayoucar2のコンストラクタの引数は親の分+独自の分を定義しておく(runtotalとrepairecostを追加してる)
  • 親の関数を呼ぶ場合は、super.関数名()にて呼び出す事が出来る
  • 上記の関数だとasset.resaleにて1,224,000が答えとして返ってきます。

旧式のクラスの定義

V8ランタイムではない以前のGoogle Apps Scriptで動作出来るクラスのようなものとして使われていたコードは以下の通り(ES5準拠)。Class構文は使えない為、即時関数とPrototypeを利用して同じような仕組みを実現していました。

どうしても、V8ランタイムでコードを記述できないようなケースでは上記のようなスタイルでクラスを実現するようにしなければなりません。上記のコードでmon1.getHitpointで返ってくる値は、245が返ってきます

減価償却クラスのコードと解説

ソースコード

コードの解説

  • 固定資産シートの資産一覧のデータを読み取って、指定の年数分の償却額合計を計算し、償却額シートにまとめて結果を出すクラスです
  • プロンプトにて償却年数を取得して計算しています。
  • s_typeの値を元に定額法と定率法で呼び出す関数を分岐しています。
  • asset.calcretにて計算をさせて、償却額トータルを返してもらっています。
  • データは一括で二次元配列にて書き出しを行っています。
  • calcretからは償却方法に応じて同じクラス内の別の関数をthis.関数名(引数)にて渡しています

実際に構築してみた結果ですが、これを単純に関数でやってしまうと、非常に冗長なものになってしまったり、後でメンテナンスをする際に関数の手直しが非常に面倒になる点です。

呼び出し元のコードは非常にシンプルになり、呼び出し先のクラスは部品単位で細かく管理が出来る。これがクラスの利点になります。今回のような大きな機能になりそうなものは、いきなりコードで書き始めるというよりは、クラスの設計をして後から機能の追加をしていくスタイルで作成すれば、機能追加も容易になると思います。

ゲームのように大量に同じパターンを生成して、それらを配列に格納し、1ターン毎に各インスタンスにランダムなパラメータを渡すなんて芸当は、クラスが無いととっちらかって大変なことになります。

図:減価償却計算がサクっと出来ました。

メソッドチェーンを作成する

応用編として、今回のような自作のクラスでメソッドチェーンを作る手法があります。GASでもよく見かけるSpreadsheetApp.getActiveSpreadsheet().getSheetByName("test")みたいな形で命令を数珠つなぎにして送り込むものです。クラスを用意できれば、これを構築する事が可能です。

以下のコードは課税額15%を上乗せし、利益を20%乗せて、最後に答えを取得するという一連のクラスを用意してメソッドチェーンとする手法です。

  • builder変数ではクラスを初期化するコードだけを用意しておきます。
  • クラス内では、取り敢えずコンストラクタの値は0で初期化して用意しておき、つなげる関数からの引数で入力する
  • ポイントは各関数は最後値を返すのではなく、thisだけを返すようにします。
  • 最終的な値を取得するgetSalesPriceのみ、答えをreturnで返すようにしてあります。
  • chainmethodではbuilderをスタートとして数珠つなぎに2つの関数を呼び出し、最後に答えをもらっています。tomatoには上記のコードならば、4140という答えが返ってきます。
  • それぞれの段階の値を保持しておけるのがクラスの良い点です。

関連リンク

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)