どうも。つじけ(tsujikenzo)です。このシリーズでは、[GAS]オブジェクト指向によるGAS開発のススメ、をお届けします。
今日のアジェンダ
- 要件定義
- 外部設計
- 内部設計・コーディング・テスト
- クラス設計のコツ
- シートクラス
はじめに
前回(GAS中級講座5期)の、卒業LTでは 「ノンプログラマーによるGAS開発モデル」 という、内容をお届けしました。
プロの開発手法をまなび、ノンプログラマーにも取り入れられる要素はないか、という考察をしたものです。

今回のシリーズでは、その進化版で、オブジェクト指向のエッセンスを取り入れたGAS開発を、お届けします。(ノンプログラマーのわたしが、個人的に学んだことをアウトプットしますので、間違いなどはご容赦ください)
オブジェクト指向のエッセンス
流行り廃りもありますが、プログラムの開発・保守・運用を楽にする手法の1つが、オブジェクト指向プログラミングです。
わたしも、勉強をはじめたばっかりで、少しずつブログ化しています。

オブジェクト指向とは、実世界のヒト・こと・モノを「オブジェクト」という単位でとらえ、ソースコードに反映させることです。
GAS開発を楽にしてくれる手法だと信じていますので、興味のあるかたはぜひ、一緒に学びましょう。
今日、第1回目は、「要件定義」から始めていきます。
ユースケース図
要件定義をするときに便利なツールが、ユースケース図です。
今回は、「スプレッドシートで日報を作成し、マネージャーにPDFをメールする」という、業務アプリケーションを作成したいと思います。
ユースケース図を書こう
ユースケース図は、「そのシステムでユーザーがなにをできるのか」を可視化したものです。
あまりむずかしく考えずに、直感で書いてみましょう。
まず、システムを真ん中に置いてみます。
こと
次に、やりたいこと(○○する)を並べてみます。ここで、「将来的に使うかもしれない」という機能は、記入しないことをオススメします。(たとえばGoogleドライブに保存する、とかSlackに通知を飛ばすなど)
本当に必要な、コアな部分に集中しましょう。

とくに、スプレッドシートにレコードを追加するばあいなどは、「フォームから入力させたい」と考えるかもしれません。
しかし、現段階では「スプレッドシートを開いて、1行追加する」と考えましょう。
フォームから入力などは、あとで機能を追加すればいいのです。
ヒト
システムを操作するヒトが必ずいると思いますので、ヒトを記入します。
この段階では、ヒトは、それぞれの処理を実行できると考えていいでしょう。
いずれは、処理はすべて連携され、「日報を入力するだけで、メールが送信される業務アプリケーション」にしたいかもしれません。
しかし、まずは、個別に処理を実行できるという考え方が大事です。
トリガーなどは、あとで機能を追加すればいいのです。
モノ
ヒトが処理することに対して、アイコンなどをおいて、なにを操作するのかイメージを固めましょう。
とくに、とりあつかうものが、スプレッドシートなのか、PDFなのか、メールなのか、など、具体的なアイコンがあるとわかりやすいです。
このシステムと連携するものとして、日報を管理する人事部のシステムがあるかもしれません。
しかし、今後連携するシステムのモノまで考える必要はありません。別のシステムとは、APIで連携させていきます。
API連携などは、あとで機能を追加すればいいのです。(システムをAPIで連携したものを、マイクロサービスと呼びます。)
完成図
実際に使用するシートなどが決まっていたり、定義できるばあいは、ユースケース図に書き込んでしまいます。
完成したユースケース図はこちらです。
まとめ
以上で、「要件定義」をお送りしました。
ウォーターフォール開発のように、ガチガチに要件定義をして、仕様を細かく決めるのではなく、ユースケース図をざっくりと書くのがコツです。
ガチガチに書いても、あとで変更は必ず発生します。なにごとも「ざっくり」でいきましょう。
申し遅れましたが、開発にはMURALやJamboardといったホワイトボードツールがオススメです。
次回は、 「外部設計」 をお届けします。
参考資料
前回は、「要件定義」をお届けしました。

今回は、2回目で「外部設計」です。
業務アプリケーションを外側からみたら、どのように見えるのかを決めていきます。
今日のアジェンダ
- クラス図
- 業務フロー図
クラス図
前回作成したユースケース図では、業務アプリケーションで必要な、ヒト・こと・モノに登場してもらいました。
この、ヒト・こと・モノをオブジェクトとしてとらえてみましょう。
クラスを作成する
ユースケース図に登場した、ヒト・こと・モノをすべてオブジェクトとして書き出してみます。
復習ですが、クラスは、オブジェクトを生成する機能です。
具体的なオブジェクトを眺めながら、どんなクラスを作成したら、必要なオブジェクトが生成されるかを考えます。
ヒト
たとえば、ヒトでいうと、Staffクラスが1つあれば、インスタンスに、辻オブジェクトと高橋オブジェクトが生成できそうです。
スプレッドシート
スプレッドシートは、データ入力用、加工用、印刷用、という構造にすると管理がしやすいです。(詳しくはこちらのブログで)
データ加工はGASで行いますので、「データ入力用」と、「印刷用」のシートを用意しましょう。
そのまま、クラス化します。
[つじけコラム] ユーティリティクラスは作るの?
GWSの、SpreadSheetクラスの便利な関数を集めた、ユーティリティSpreadSheetクラスなどは、できれば作りません。
もし、便利な関数などを作成し、使い回ししたいばあいは、ライブラリを活用するといいでしょう。
シートは、インスタンスごとに1枚作成してもかまいませんし、1枚を共有してもかまいません。
シートの自由な運用ができるのも、クラス化のメリットです。
PDFを担当するクラスは、業務アプリケーションの規模によって、切り分けが必要です。
業務アプリケーションの規模が小さい場合は、PDFクラスでさまざまな処理を担当してよいでしょう。
PDFクラスのお仕事
- PDFを作成・保存
- PDFを探す
- PDFの結合・分割
- PDFのblob化
もし、PDFクラスが肥大化してきたばあいは、たとえば、1.と3.のクラスを分けるのも手です。
クラスを切り分けるときは、「createPDFクラス」や、「processPDFクラス」など、なにをするかクラス名で明示することが大事です。
今回は、createPDFクラスを作成しました。
Gmail
Gmailは、GWSのサービスの1つです。
Gmailでは、アタッチメントや下書き作成やスレッドなど、さまざまなオブジェクトの操作が考えられます。
なので、ちゃんと、オブジェクトに対応したクラスが提供されています。
普段、何気なくメソッドを書いていますが、GmailAppのメソッドは、静的メソッドです。
//createDraft()は静的メソッド
const now = new Date();
const recipient = "mike@example.com";
const subject = "current time";
const body = `The time is:${now.toString()}`;
GmailApp.createDraft(recipient, subject, body);
また、GmailAppクラスは、コンストラクタをもっていません。
const g = new GmailApp(); //TypeError: GmailApp is not a constructor
console.log(g);
つまり、GmailAppクラスは、インスタンスを生成して使うことを想定していません。
このような継承はできないことを理解しておきましょう。
class MyGmailApp extends GmailApp { }
さきほども言いましたが、Gmailに関する便利な関数を作成したのなら、ライブラリを活用するほうがいいです。
ユーティリティ系のクラスを作成する必要はありません。
GASのプロジェクトのアカウントが、Gmailのアカウントに紐づいているので、インスタンス生成時に Gmailアカウントを渡す必要もありません。(それができると便利なこともあるんですけどね)
と、なると、作成するクラスは、大きく分けて以下の2種類が考えられます。
- GmailSearchクラス・・・Gmailのスレッドやメッセージを検索するクラス
- GmailDraftクラス・・・Gmailの下書きを作成、送信するクラス
これらのクラスに、オブジェクトを渡すことで威力を発揮します。(詳細は第4回目)
今回は、GmailDraftクラスを作成しました。
完成図
完成したクラス図はこのような感じです。
本来なら、トリガーやonOpenもクラス化するはずですが、functionでかまわないと思いますので、補足的にクラス図に置いています。
ProtertyStoreやトリガーについても、業務アプリケーションの規模が小さいうちは、クラス化する必要はないと思います。
スプレッドシートのひな形を流用して、別のひな形を運用するばあいなどは、クラスを継承しますが、別の機会にご紹介したいと思います。
業務フロー図
業務フロー図は、実際に、ユーザーがどのような流れでデータを入力するのか、などを可視化したものです。
もちろん、この時点では、シート構成や、カスタムメニューなどは未完成だと思いますので、ダミーデータなどを用意して、それっぽく見せるのがポイントです。
業務フロー図でシェアしたい内容は以下です。
- ユーザーはどんなタイミングでFunctionを実行したいのか
- 実行したいFunctionはどこから呼び出すのか
- (実行したいFunction名がイメージできるならなお良し)
- スプレッドシート編集時、などの、トリガーでうごく処理はなにか
- 時限式トリガーでうごくFunctionはなにか
もちろん、ザックリでかまいません。
ユーザーに目視確認してもらいたいフローも、書いておきましょう。
ゆくゆくは、業務フロー図をそのまま、業務アプリケーションマニュアルとして使えます。
この段階で、修正したいクラスなどが見えるばあいもあると思います。必要に応じて、ユースケース図やクラス図を見直してみましょう。
まとめ
以上で、「外部設計」をお送りしました。
UMLを学んでいるひとからしたら、「これがクラス図?笑わせるぜ!」と思われるのは百も承知です。
しかし、クラス図はノンプログラマーにとって敷居が高く、手が出しづらいものです。
なので、今回はまず 「ヒト・こと・モノをオブジェクトとしてとらえよう」 という点だけにフォーカスしてみました。
カスタムクラス・メソッド
もし、GWSのクラス名と同じクラスやメソッドを作成するばあいは、Customという言葉を付けるようにしています。
- class CutomPropertyStore
- getCutomCreateFile()
- setCustomValues()
ご留意くださいませ。
次回は、 「内部設計・コーディング・テスト」 をお届けします。
前回のおさらい
前回は、「外部設計」をお届けしました。

今回は、3回目で「内部設計・コーディング・テスト」です。
「テスト」という工程は、ノンプログラマーにはあまり馴染みがありません。
今日は、「テスト駆動開発(っぽいもの)」という手法を取り入れながら、プログラミングしていきます。
今日のアジェンダ
- プロジェクトを準備する
- ファイル内構成
- テスト駆動開発
- コンストラクタとメソッドの実装
プロジェクトを準備する
まず、プロジェクトを用意します。実際に使用するスプレッドシートの、コンテナバインドスクリプトでいいでしょう。
クラスごとのスクリプトファイルを用意する
クラス図を見ながら、1クラスにつき1スクリプトファイルを作成します。
スクリプトファイル名は「class_○○」で統一します。
onOpen()には、01_のような番号を振ると分かりやすいでしょう。
さっそく、スクリプトファイルの中身を書いていきます。
順番はどれからでもかまいません。チームで開発するばあいは、クラスの担当を分けて、GitHubなどにPUSHしていくのもいいでしょう。
ファイル内構成
スクリプトファイル内は、このような構成になります。
クラス系ファイル
クラス系ファイルは、かならず上にクラスを書き、下にクラスをテストする関数を書きます。
//class クラス{ }
//function クラスのテストをする関数(){ }
onOpenなどの実行用関数系
同様に、onOpenなどの実行用関数系ファイルも、下にfunctionをテストする関数を書きます。
//グローバル領域
//function onOpen(){ }
//function onOpenで呼ばれる関数1(){ }
//function onOpenで呼ばれる関数2(){ }
//function スクリプトファイル内のテストをする関数(){ }
まずは、これらのコメントを、スクリプトファイルにコピペしておいてもいいでしょう。
テスト駆動開発
クラスや、実行用関数を書くときは、まず、呼び出し側のテスト関数から書き始めます。
テスト関数の名前は、かならず「testクラス名」で統一します。
たとえば、DataSheetクラスのテストをする関数なら、「testDataSheet」という具合です。
まだ、中身はなにもありませんが、このような手順で書いてみましょう。
/** Dataシートクラス */
class DataSheet {
}
/** TEST関数 */
function testDataSheet() {
}
日本語でテスト関数を書く
テスト関数は、DataSheetクラスがちゃんと動くかどうか、テストをするための関数です。
なので、DataSheetクラスがもつ、状態や振舞いを、1つ1つ確認します。
たとえば、DataSheetクラスは、なにかしらのプロパティをもつはずですので、インスタンスが生成されたら、インスタンスとその中身を確認するテスト関数を 「日本語で」 書きます。
/** Dataシートクラス */
class DataSheet {
}
/** TEST関数 */
function testDataSheet() {
//インスタンスの確認
//シートがつかめているか確認
//シート名の確認
}
メソッドも同様に、DataSheetクラスの振舞いを確認するものなので、1つ1つ確認します。
テスト関数のコツは、「メソッドでやることは1つ」という原則を守ることです。
メソッドはとても小さな処理になります。それでかまいません。
クラスがやる処理をすべて書き起こして、日本語でテスト関数を書きます。
/** Dataシートクラス */
class DataSheet {
}
/** TEST関数 */
function testDataSheet() {
//インスタンスの確認
//シートがつかめているか確認
//シート名の確認
//全てのRecordsをobjArrayで取得するメソッド
//Recordsのlengthを確認するメソッド
//Dataシートの最終行を返すメソッド
//Dataシートの最終列を返すメソッド
//受け取ったobjArrayを貼り付けるメソッド
}
ある程度、テスト関数を日本語で書き終えたら、テスト関数のコードを書きます。
テスト関数では、ログ出力をもってコードが動いている、という確認をします。
なので、すべてのテスト関数に console.log() を書きます。
マルチカーソル機能(Alt + クリック) を使って、コーディングを効率化していきましょう。コピペより早いです。
console.log()の中に、確認するプロパティやメソッドを書いていきます。
チームで開発するさいは、テスト関数を書いて、次の人にパスしてあげると、次の人も作業がしやすいですね。
ということで、すべてのクラス(onOpenなども含む)の、テスト関数を日本語で書くことからはじめましょう。
/** Dataシートクラス */
class DataSheet {
}
/** TEST関数 */
function testDataSheet() {
//インスタンスの確認
const d = new DataSheet();
console.log(d);
//シートがつかめているか確認
console.log(d.sheet);
//シート名の確認
console.log(d.sheet.getName());
//全てのRecordsをobjArrayで取得するメソッド
console.log(d.getDataSheetValues());
//Recordsのlengthを確認するメソッド
console.log(d.dataSheetRecordsLength());
//Dataシートの最終行を返すメソッド
console.log(d.getCustomLastRow());
//Dataシートの最終列を返すメソッド
console.log(d.getCustomLastColumn());
//受け取ったobjArrayを貼り付けるメソッド
console.log(d.setCustomValues());
}
コンストラクタとメソッドの実装
テスト関数ができたら、クラスの中身を書いていきます。
コンストラクタになにをもたせて、インスタンスを生成するかは、非常にむずかしいポイントです。
絶対これが正しい、という正解はありませんで、まずは、自分のイメージと合うコンストラクタを書いていいと思います。
「この方が便利!かっこいい!」と思うものがあれば、修正しながら、でいいと思います。
/** Dataシートクラス */
class DataSheet {
/**
* @constructor
*/
constructor() {
this.id = SHEET_ID; //onOpen.gsのグローバル領域に定義しています。
this.sheetName = `Data`;
this.sheet = SpreadsheetApp.openById(this.id).getSheetByName(this.sheetName);
}
}
メソッドは、短く、純粋関数(戻り値を返すだけの関数)で書きます。
set系のメソッドには、テキストメッセージを持たせ、メソッドはかならず戻り値を持つようにします。
手順としては、まず、テスト関数をそのままクラス内に貼り付けます。
/** Dataシートクラス */
class DataSheet {
/**
* @constructor
*/
constructor() {
this.id = SHEET_ID; //onOpen.gsのグローバル領域に定義しています。
this.sheetName = `Data`;
this.sheet = SpreadsheetApp.openById(this.id).getSheetByName(this.sheetName);
}
//シート名の確認
console.log(d.sheet.getName());
//全てのRecordsをobjArrayで取得するメソッド
console.log(d.getDataSheetValues());
//Recordsのlengthを確認するメソッド
console.log(d.dataSheetRecordsLength());
//Dataシートの最終行を返すメソッド
console.log(d.getCustomLastRow());
//Dataシートの最終列を返すメソッド
console.log(d.getCustomLastColumn());
//受け取ったobjArrayを貼り付けるメソッド
console.log(d.setCustomValues());
}
マルチカーソル機能(とくにCtrl+矢印キーが大活躍します)を使って、メソッドに仕上げていきます。
メソッドは、かならず戻り値を持たせますので、すべてのメソッドにreturnを持たせます。
return文には、変数を持たせるように統一します。
returnで返す変数、ということは、変数宣言するはずですので、変数宣言も用意しておきます。
変数名はとりあえずvalueでいいでしょう。
これだけ準備しておくだけでも、コーディングがものすごく早くなります。(コーディング時に悩みが少なくなります)
/** Dataシートクラス */
class DataSheet {
/**
* @constructor
*/
constructor() {
this.id = SHEET_ID; //onOpen.gsのグローバル領域に定義しています。
this.sheetName = `Data`;
this.sheet = SpreadsheetApp.openById(this.id).getSheetByName(this.sheetName);
}
/** 全てのRecordsをobjArrayで取得するメソッド
* @return {Object} value
*/
getDataSheetValues() {
const value = '';
return value;
}
/** Recordsのlengthを確認するメソッド
* @return {Object} value
*/
dataSheetRecordsLength() {
const value = '';
return value;
}
/** Dataシートの最終行を返すメソッド
* @return {Object} value
*/
getCustomLastRow() {
const value = '';
return value;
}
/** Dataシートの最終列を返すメソッド
* @return {Object} value
*/
getCustomLastColumn() {
const value = '';
return value;
}
/** 受け取ったobjArrayを貼り付けるメソッド
* @return {Object} value
*/
setCustomValues() {
const value = '';
return value;
}
}
そして、すべてコーディングが終わったら、テスト関数 testDataSheet() を実行します。
ここで発生するエラー(タイプミスや浮動小数点など)のことを、バグと呼びます。
テスト関数を実行しながら、バグを1つずつつぶしていきます。
「バグに対処するために、もうすこし早めに、コードが動くかどうかテストしたい」という声もあると思います。
しかし安心してください。バグの必要な修正箇所は、比較的見つかりやすいです。
それは、メソッドが小さいからです。メソッドとメソッドは複雑に絡み合っていません。
このような書き方を「疎結合(そけつごう)」と言います。
メソッドを書くときは、疎結合を意識していきましょう。
まとめ
以上で、「内部設計・コーディング・テスト」をお送りしました。
チームで開発をするときなど、他人のコードが、コーディングガイドラインに違反した書き方だと、ついつい口を出してしまうものです。
しかし、まずはテスト関数が動いているなら中身(書き方)は問わない、という安心感を提供することで、チームの士気を高めます。
リファクタリングもしかりで、まずはコードが動くことを確認しながらコーディングするテクニックを、取り入れてみましょう。
テスト駆動開発、おススメです。
次回は、 「クラス設計のコツ」 をお届けします。
前回のおさらい
前回は、「内部設計・コーディング・テスト」をお届けしました。

今回は、4回目で「クラス設計のコツ」です。
最近、個人的に、「こうやって書けば、すこし業務アプリケーション開発が楽になるかもしれないなぁ」と、思う書き方があります。
今日はそんな、「オブジェクト指向(っぽいもの)」という手法を取り入れながら、プログラミングしていきます。
今日のアジェンダ
- 引数にオブジェクトを取る
- インスタンスの役割
引数にオブジェクトを取る
クラス化のさいに、「処理の共通化をさせよう」という想いが強くなればばるほど、自然と引数は多くなります。
なんにでも対応できるように、引数を配列でもたせたりしてしまうこともあるでしょう。
/** GmailDraftクラス */
class GmailDraft {
/** メールの下書きを作成するためのコンストラクタ
* @constructor
* @param{Array} string
*/
constructor(personArray) {
const [name, toAddress, ccAddress, subject, body] = personArray;
this.name = name;
this.toAddress = toAddress;
this.ccAddress = ccAddress;
this.subject = subject;
this.body = body;
}
}
オブジェクト指向プログラミングの特徴は、オブジェクト同士が作用しあうという点です。 出典:[GAS][DDD]1章オブジェクト指向とはなにか
たとえば、GmailDraftが生成するインスタンスは、1件のGmailメッセージになるのが自然でしょう。
toAddressやccAddressは、インスタンスによって条件分岐したいところです。
営業部に所属するスタッフが、日報を提出する先は、営業部の管理職ですし、製造部なら、製造部の管理職に送信したいです。
これまでの手続き型プログラミングでは、この条件分岐を、if文で書いていました。
/** GmailDraftクラス */
class GmailDraft {
/** メールの下書きを作成するためのコンストラクタ
* @constructor
* @param{Array} string
*/
constructor(personArray) {
const [name, team, ccAddress, subject, body] = personArray;
this.name = name;
this.team = team;
this.toAddress = team === '営業部' ? 'eigyoMG@gmail.com' : 'seizoMG@gmail.com';
this.ccAddress = ccAddress;
this.subject = subject;
this.body = body;
}
}
しかし、if文は、コードの変化に弱く、可読性を下げる原因のひとつです。
できるだけクラスでは、if文は書きたくありません。
インスタンスの役割
Aさんが営業部に所属しているなら、その状態は、Staffクラスのインスタンスが持っているはずです。
これは、コロコロ変わるものではありません。
インスタンスの状態を、プロパティにもつか、メソッドにもつかは、ときとばあいによりますが、プロパティはあまり増やさない方がいいです。
ここではあくまで例として、「所属」をプロパティに持たせてみました。
/** Staff情報を取り扱うクラス */
class Staff {
/** コンストラクタ */
constructor(staffName) {
this.staffName = staffName;
this.team = this.getTeam();
}
/** スタッフシートのRecordsから、所属チームを返すメソッド
* @return{string} 所属名
*/
getTeam() {
const records = this.getStaffSheetRecords();
const filter = records.filter(record => record['名前'] === this.staffName)[0];
const team = filter['所属'];
return team;
}
/** 日報提出先のメールアドレスを返すメソッド
* @return{string} メールアドレス
*/
getTeamManagerAddress() {
const records = this.getStaffSheetRecords();
const filter = records.filter(record => record['名前'] === this.staffName)[0];
const mail = filter['管理者アドレス'];
return mail;
}
/** スタッフシートのRecordsを返すメソッド
* @return{Array} 連想配列形式のrecords
*/
getStaffSheetRecords() { }
}
/** TEST関数 */
function testStaff() {
//インスタンス生成
const staffName = '辻健蔵';
const s = new Staff(staffName);
//所属を確認
console.log(s.getTeam); //営業部
//日報のメールアドレスを確認するメソッド
console.log(s.getTeamManagerAddress()); //eigyoMG@gmail.com
}
なので、このように、GmailDraftクラスに、Staffクラスのインスタンスを渡すと、if文が消えます。
テスト関数のインスタンスの生成と、GmailDraftクラスのコンストラクタに注目してください。
/** GmailDraftクラス */
class GmailDraft {
/** メールの下書きを作成するためのコンストラクタ
* @constructor
* @param{Object} Staffクラスのインスタンス
*/
constructor(obj) {
this.obj = obj;
this.name = obj.staffName;
this.team = obj.team;
this.recipient = obj.getTeamManagerAddress();
}
}
function testGmailDraft() {
//Staffクラスのインスタンスを生成
const staffName = '辻健蔵';
const tsujike = new Staff(staffName);
//GmailDraftクラスのインスタンスを生成
const g = new GmailDraft(tsujike);
console.log(g.name); //辻健蔵
console.log(g.team); //営業部
console.log(g.recipient); //tsujike@gmail.com
}
もしクラス内で、if文を書かなければならないような状況になったら、コンストラクタや、インスタンス生成時の引数を見直してみましょう。
PDF、Gmail、LINE、Slackなどのクラスは、生成したいコンテンツを(できるだけオブジェクトで)引数に取るとよいでしょう。
まとめ
以上で、「クラス設計のコツ」をお送りしました。
いままで、コンストラクタに渡す引数は、文字列型や配列が多かったかもしれません。
しかしこのように、コンストラクタの引数にオブジェクトを渡すと、一気に新しい世界が待ち受けています。
次回は、 最終回で 「シートクラス」 をお届けします。
前回のおさらい
前回は、「クラス設計のコツ」をお届けしました。

今回は、最終回で「シートクラス」です。
GASによる業務アプリケーション開発で、もっとも重要度の高いクラスが、シートクラスです。
なぜなら、スプレッドシートは、データベースや、入力用のインターフェースや、実行関数を走らせるトリガーになったり、GASになくてはならない存在だからです。
今回は、すべての内容をお伝えできませんが、重要なところをピックアップしてみたいと思います。
今日のアジェンダ
- DOVパターン(データ入力用)
- DOVパターン(印刷用)
DOVパターン(データ入力用)
要件定義の回で、スプレッドシートは、データ入力用、加工用、印刷用、という構造(DOVパターン)にすると管理がしやすい、ということをお伝えしました。
データの加工は、GASが行いますので、データ入力用と印刷用の2種類が、主なスプレッドシートの運用方法だと思います。
そして、データ入力用シートは、非正規化の1枚だけが好ましいです。
データベースを正規化すると、リレーションを組んだシートが複数枚発生しますが、「正規化しなければならないほどのデータ入力用シート」は、データ構造を見直した方がよいでしょう。
データ入力用シートオブジェクト
データ入力用シートクラスから生成されたインスタンスは、データ入力用シートオブジェクトです。
/** Dataシートクラス */
class DataSheet {
/**
* @constructor
*/
constructor() {
this.id = SHEET_ID; //onOpen.gsのグローバル領域に定義しています。
this.sheetName = 'Data';
this.sheet = SpreadsheetApp.openById(this.id).getSheetByName(this.sheetName);
}
//メソッド
}
データ入力用シートオブジェクトのメソッド
このオブジェクトは、データ入力用シートに関する処理 を担当します。
主に、シートのデータを提供したり、シートを更新したりするメソッドを持つでしょう。
/** Dataシートクラス */
class DataSheet {
//~中略~
/** すべてのRecordsをobjArrayで取得するメソッド
* @return{Array} objArray
*/
getDataSheetRecords() {
const [header, ...records] = this.sheet.getDataRange().getValues();
const allObjectRecords = records.map(record => {
const obj = {};
header.map((element, index) => obj[element] = record[index]);
return obj;
});
return allObjectRecords;
}
/** 受け取ったobjArrayを貼り付けるメソッド
* @param{Array} objArray
*/
setValuesToDataSheet(objArray) {
//2次元配列に戻す
const records = objArray.map(record => Object.values(record));
//貼り付け
this.sheet.getRange(2, 1, records.length, records[0].length).setValues(records);
return 'Dataシートに貼り付け完了しました';
}
}
大切なポイントは、このクラスは、このシートに関するお仕事(振舞い)のみを担当することです。
このシートのデータを使って、GmailのBodyを作成したければ、Gmailクラスから、このクラスに「あなたのデータを下さい」という命令をするだけです。
このクラスに「createBodyForGmail()」のようなメソッドは持たせません。
Gmailに関するお仕事はGmailに任せましょう。
DOVパターン(印刷用)
印刷用のシートは、1枚のシートを、ひながたとして共有してもかまいません。
ただし、印刷用のシートごとに、少しだけ書式を変更したいばあいなどは、シートを変更するコストがかかります。
解決方法としては、シートの書式情報や、シート独自の状態をもったオブジェクトを渡すことになりますが、規模のおおきな業務アプリケーション開発のときでいいと思います。
シートが増えることは大歓迎
印刷用のシートが10枚程度であれば、10枚のシートを用意してもかまいません。
シートを無理に共通化するために、悩んだり、処理が複雑になるメリットは、あまりありません。
シート5枚で業務が運用されるなら、シートを5枚用意しましょう。
あたまは柔らかくです。
ただし、シートごとにセル位置が違う、などはソースコードも、運用もコストが跳ね上がります。
逆に言うと、セル位置が異なるシートなら、クラスを分ける といいでしょう。
セル位置や、見出し項目の数は、クラス化のひとつの判断基準になります。
日報シートクラス
作成した日報シートクラスは、このような感じです。
日報クラスは、日報シートを処理することに専念しています。
日報シートでは、Dataシートクラスのデータが必要ですが、メソッド内でインスタンスを生成して、必要なデータを取得しています。
/** Nippoシートクラス */
class NippoSheet {
/**
* @constructor
* @param{Object} Staffクラスのインスタンス
*/
constructor(obj) {
this.id = SHEET_ID; //onOpen.gsのグローバル領域に定義しています。
this.name = obj.name;
this.sheetName = `日報_${this.name}`;
this.sheet = SpreadsheetApp.openById(this.id).getSheetByName(this.sheetName);
}
/** 日報シートに貼り付けるメソッド
* @return{Array} objArray
*/
setValuesToNippoSheet() {
this.sheet.getRange('B3').setValue(this.createID_());
this.sheet.getRange('B4').setValue(this.createName_());
this.sheet.getRange('F3').setValue(this.createDate_());
this.sheet.getRange('A7').setValue(this.createMokuhyo_());
this.sheet.getRange('A10').setValue(this.createGyomunaiyo_());
this.sheet.getRange('F4').setValue(this.createTeisyutusaki_());
return '日報が完成しました';
}
/** recordをSTAR済みにしてDataシートを更新するメソッド */
setStarToDataSheetRecord() {
//すべてのrecordsを取得
const records = this.getRecordsFromDataSheet_();
//名前でフィルター掛け
const myRecords = records.filter(record => record['名前'] === this.name);
//スターでフィルター掛け
const withoutStarMyRecords = myRecords.filter(record => record['STAR'] === '');
//スターを付ける
withoutStarMyRecords.map(record => {
record['STAR'] = '★';
return record;
});
//Dataシートに貼り付け
const d = new DataSheet();
d.setValuesToDataSheet(records);
return 'DataシートにSTARをつけました';
}
/** sheetIdGidを返すメソッド
* @return{string} e.g edit#gid=729331016 の数値部分
*/
getNippoSheetIdGid() {
return this.sheet.getSheetId();
}
/** ↓↓サブメソッド↓↓ */
/** DataSheetから自分のrecordsを取得するメソッド
* @return{Array} objArray
*/
getMyRecordsFromDataSheet_() {
//DataSheetからすべてのrecordsを取得
const records = this.getRecordsFromDataSheet_();
//フィルター掛け
const myRecords = records.filter(record => record['名前'] === this.name);
return myRecords;
}
/** DataSheetからすべてのrecordsを取得するメソッド
* @return{Array} objArray
*/
getRecordsFromDataSheet_() {
const d = new DataSheet();
const records = d.getDataSheetRecords();
return records;
}
/** myRecordsからStar無しを返すメソッド
* @return{Array} objArray
*/
getWithoutStarMyRecords_() {
const records = this.getMyRecordsFromDataSheet_();
const withoutStarRecords = records.filter(record => record['STAR'] === '');
return withoutStarRecords;
}
/** IDを取得するメソッド */
createID_() {
return this.getWithoutStarMyRecords_()[0]['ID'];
}
/** 名前を取得するメソッド */
createName_() {
return this.getWithoutStarMyRecords_()[0]['名前'];
}
/** 作成日を取得するメソッド */
createDate_() {
return this.getWithoutStarMyRecords_()[0]['作成日'];
}
/** 今日の目標を取得するメソッド */
createMokuhyo_() {
return this.getWithoutStarMyRecords_()[0]['今日の目標'];
}
/** 業務内容を取得するメソッド */
createGyomunaiyo_() {
return this.getWithoutStarMyRecords_()[0]['業務内容'];
}
/** 提出先を取得するメソッド */
createTeisyutusaki_() {
return this.getWithoutStarMyRecords_()[0]['提出先'];
}
}
/** TEST関数 */
function testNippoSheet() {
//インスタンス生成
const person = new Staff('辻健蔵');
const tsujiNippo = new NippoSheet(person);
//日報シートに貼り付けるメソッド
console.log(tsujiNippo.setValuesToNippoSheet());
//処理したrecordをSTAR済みにしてDataシートを更新するメソッド
console.log(tsujiNippo.setStarToDataSheetRecord());
// sheetIdGidを返すメソッド
console.log(tsujiNippo.getNippoSheetIdGid());
/** ↓↓サブメソッド↓↓ */
//DataSheetからすべてのrecordsを取得するメソッド
console.log(tsujiNippo.getRecordsFromDataSheet_());
//自分のrecordsだけ取得するメソッド
console.log(tsujiNippo.getMyRecordsFromDataSheet_());
//myRecordsからStar無しを返すメソッド
console.log(tsujiNippo.getWithoutStarMyRecords_());
//IDを取得するメソッド
console.log(tsujiNippo.createID_());
//名前を取得するメソッド
console.log(tsujiNippo.createName_());
//作成日を取得するメソッド
console.log(tsujiNippo.createDate_());
//今日の目標を取得するメソッド
console.log(tsujiNippo.createMokuhyo_());
//業務内容を取得するメソッド
console.log(tsujiNippo.createGyomunaiyo_());
//提出先を取得するメソッド
console.log(tsujiNippo.createTeisyutusaki_());
}
クラスの中で、他のクラスを呼び出すことはあります。
しかし、クラスは、自分のクラスを操作することに専念すべきです。
もし、クラス内で、他のクラスのメソッドを呼び出すことが多かったり、他のシート名が多く出現するようなら、他のクラスにメソッドを実装させられないか、検討してみましょう。
まとめ
以上で、「シートクラス」をお送りしました。
現実世界のヒト・こと・モノを、オブジェクトとしてとらえると、自然と、GASでクラスを書くようになります。
これは、共通した処理をクラス化したものとは、見える景色が少し違うかもしれません。
オブジェクト指向プログラミングは、大変奥の深いものです。
今回ご紹介できなかった、型の話や、継承やポリモーフィズムの話もたくさんあります。
まだまだ、GASの楽な書き方を考察する日々は続きます。
学んだことを、少しずつアウトプットしていきたいと思います。
このシリーズの目次
[GAS]オブジェクト指向によるGAS開発のススメ
Comments