GAS開発時のファイル構成Tips
はじめに
GASで開発する際のファイル構成のTipsです。
社内のちょっとした作業を自動化するために、GASを使うことが多くあります。
個人的に使うならば自分が好きなように書けばいいのですが、会社の資産としてチームで保守しやすい用に構成しています。
保守しやすいとは
同一開発方針・構成になっていれば、保守しやすいと考えています。
そのため、下記の方針でファイルを作ります。
- テストファイルを作る
- テストのファイル名はtest.gsもしくはtest(Class名).gsにする
- classは1つのファイルにする
- 最終的に呼び出すfunctionを記載するファイルは1ファイルにする
- ファイル名は main.gs としています
- webアプリでデプロイした場合のdoPost(e)などを書く
実装例
1つのカレンダーのイベントをスプレッドシートに書き出してみます。
大まかに下記手順です。
- calenderオブジェクトを使ってイベントを取得する
- sheetオブジェクトを使ってイベントの中から必要な情報をシートに書き出す
ファイルの構成は以下になっています。
テストファイルを作る
テストファイルとして test.gs
を作成し、そこに test_
で始まるfunctionを作成して書いていきます。
unitテストのようにassertしているわけではないので、TDDとは言えないんですが、個別のfunctionを確認できるようにしておくことで、ちょっとした仕様変更時にその部分だけ確認できます。
実装例
GASの関数のテスト
function test_getAllOwnerdCalenders() {
const calendars = CalendarApp.getAllCalendars();
Logger.log(
'This user owns or is subscribed to %s calendars.',
calendars.length,
);
calendars.forEach(calendar => {
Logger.log('Calendar ID: %s, Calendar Name: %s', calendar.getId(), calendar.getName());
});
}
function test_getOneCalenderObj() {
const myCalender = CalendarApp.getCalendarById(TEST_CALENDER_ID);
Logger.log('Calendar ID: %s, Calendar Name: %s', myCalender.getId(), myCalender.getName());
}
const TEST_CALENDER_ID = "[取得したいカレンダーのID]";
Classにしたオブジェクトのテスト
function testMyCalenderGetOneDayEventDatas() {
const myCalender = new MyCalender();
const eventDatas = myCalender.getOneDayEventDatas(2025, 0, 27);
Logger.log(`Number of events: ${eventDatas.length}`);
eventDatas.forEach(eventData => {
Logger.log(`Event Name: ${eventData.eventName}, Event StartTime: ${eventData.eventStartTime}, Event EndTime: ${eventData.eventEndTime}, Event StartDate: ${eventData.eventStartDate}, Event EndDate: ${eventData.eventEndDate}, Event StartDateTime: ${eventData.eventStartDateTime}, Event EndDateTime: ${eventData.eventEndDateTime}`);
});
}
function testMyCalenderGetOneMonthEventDatas() {
const myCalender = new MyCalender();
const eventDatas = myCalender.getOneMonthEventDatas(2025, 11);
Logger.log(`Number of events: ${eventDatas.length}`);
eventDatas.forEach(eventData => {
Logger.log(`Event Name: ${eventData.eventName}, Event StartTime: ${eventData.eventStartTime}, Event EndTime: ${eventData.eventEndTime}, Event StartDate: ${eventData.eventStartDate}, Event EndDate: ${eventData.eventEndDate}, Event StartDateTime: ${eventData.eventStartDateTime}, Event EndDateTime: ${eventData.eventEndDateTime}`);
});
}
classは1つのファイルにする
GAS全体でfunction名が重ならないようにするために、classで扱います。
カレンダーオブジェクトとシートオブジェクトが1つのファイルに混じっていると視認性が悪くなるので、ファイルを分けて管理します。
実装例
myCalender.gs
class MyCalender {
constructor() {
const CALENDER_ID = "[取得したいカレンダーのID]";
this.calender = CalendarApp.getCalendarById(CALENDER_ID);
}
getOneDayEventDatas(year, monthIndex, day) {
const startTime = new Date(year, monthIndex, day);
const twentyFourHoursFromStartTime = new Date(startTime.getTime() + (24 * 60 * 60 * 1000 - 1));
const events = this.calender.getEvents(startTime, twentyFourHoursFromStartTime);
return events.map(event => new EventData(event.getTitle(), event.getStartTime(), event.getEndTime()));
}
getOneMonthEventDatas(year, monthIndex) {
const startTime = new Date(year, monthIndex, 1);
const nextMonthFirstDate = new Date(year, monthIndex + 1, 1);
const endTime = new Date(nextMonthFirstDate - 1);
const events = this.calender.getEvents(startTime, endTime);
return events.map(event => new EventData(event.getTitle(), event.getStartTime(), event.getEndTime()));
}
}
eventData.gs
class EventData {
constructor(eventName, eventStartTime, eventEndTime) {
this.eventName = eventName;
this.eventStartTime = eventStartTime;
this.eventEndTime = eventEndTime;
}
get eventStartDate() {
return this.formatDate(this.eventStartTime);
}
get eventEndDate() {
return this.formatDate(this.eventEndTime);
}
get eventStartDateTime() {
return this.formatDateTime(this.eventStartTime);
}
get eventEndDateTime() {
return this.formatDateTime(this.eventEndTime);
}
formatDate(date) {
const originDate = new Date(date);
const year = originDate.getFullYear();
const month = ('0' + (originDate.getMonth() + 1)).slice(-2);
const day = ('0' + originDate.getDate()).slice(-2);
return year + '/' + month + '/' + day;
}
formatDateTime(date) {
const originDate = new Date(date);
const year = originDate.getFullYear();
const month = ('0' + (originDate.getMonth() + 1)).slice(-2);
const day = ('0' + originDate.getDate()).slice(-2);
const hours = ('0' + originDate.getHours()).slice(-2);
const minutes = ('0' + originDate.getMinutes()).slice(-2);
const seconds = ('0' + originDate.getSeconds()).slice(-2);
return `${year}/${month}/${day} ${hours}:${minutes}:${seconds}`;
}
}
eventDataSheet.gs
class EventDataSheet {
constructor(sheetName) {
const spreadsheet = SpreadsheetApp.getActive();
this.sheet = spreadsheet.getSheetByName(sheetName);
if (!this.sheet) {
this.sheet = spreadsheet.insertSheet(sheetName);
}
}
clearSeet() {
this.sheet.clear();
}
clearDatas() {
const lastRow = this.sheet.getLastRow();
if (lastRow > 1) {
this.sheet.getRange(2, 1, lastRow - 1, this.sheet.getLastColumn()).clear();
}
}
appendHeader() {
this.sheet.appendRow(["イベント日", "イベント名", "イベント開始日時", "イベント終了日時"]);
}
appendData(eventData){
this.sheet.appendRow([eventData.eventStartDate, eventData.eventName, eventData.eventStartDateTime, eventData.eventEndDateTime]);
}
}
最終的に呼び出すfunctionを記載するファイルは1ファイルにする
実際に実行するファイルを1つにしておくことで、どこに何があるかわからない状態を防ぎます。
ファイル名は main.gs としていますが、決まっていれば何でもよいと思います。
webアプリでデプロイした場合のdoPost(e)などはここに書きます。
実装例
今回の例では、AppScriptの「実行」を使う想定で書いてしまっています。
main.gs
function get202501Event() {
// シートを取得、又は作成
const sheet = new EventDataSheet("2025/01");
// シートをクリア
sheet.clearSeet();
// ヘッダー追加
sheet.appendHeader();
// カレンダーを取得
const myCalender = new MyCalender();
// 一ヶ月のイベントを取得
const eventDatas = myCalender.getOneMonthEventDatas(2025, 0);
// シートに追加
eventDatas.forEach( eventData => {
sheet.appendData(eventData);
})
}
最後に
過去の自分が書いたGASがさっぱりわからない、という反省を元に改善してきただけなので、もっと良い構成があると思います。
ベストプラクティスがある方、ぜひ教えて下さい。
Discussion