どうも。つじけ(tsujikenzo)です。このシリーズでは、2021年9月から始まりました「ノンプロ研GAS中級講座6期」について、全7回でお届けします。最終回の7回目を、5回に渡ってお送りしています。
前回のおさらい
前回は、「クラス設計のコツ」をお届けしました。
今回は、最終回で「シートクラス」です。
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開発のススメ