📝

GAS開発時のファイル構成Tips

2025/01/26に公開

はじめに

GASで開発する際のファイル構成のTipsです。
社内のちょっとした作業を自動化するために、GASを使うことが多くあります。
個人的に使うならば自分が好きなように書けばいいのですが、会社の資産としてチームで保守しやすい用に構成しています。

保守しやすいとは

同一開発方針・構成になっていれば、保守しやすいと考えています。
そのため、下記の方針でファイルを作ります。

  • テストファイルを作る
    • テストのファイル名はtest.gsもしくはtest(Class名).gsにする
  • classは1つのファイルにする
  • 最終的に呼び出すfunctionを記載するファイルは1ファイルにする
    • ファイル名は main.gs としています
    • webアプリでデプロイした場合のdoPost(e)などを書く

実装例

1つのカレンダーのイベントをスプレッドシートに書き出してみます。
大まかに下記手順です。

  1. calenderオブジェクトを使ってイベントを取得する
  2. 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がさっぱりわからない、という反省を元に改善してきただけなので、もっと良い構成があると思います。

ベストプラクティスがある方、ぜひ教えて下さい。

DELTAテックブログ

Discussion