[GAS]オブジェクト指向によるGAS開発のススメ #5シートクラス

GAS

どうも。つじけ(tsujikenzo)です。このシリーズでは、2021年9月から始まりました「ノンプロ研GAS中級講座6期」について、全7回でお届けします。最終回の7回目を、5回に渡ってお送りしています。

前回のおさらい

前回は、「クラス設計のコツ」をお届けしました。

[GAS]オブジェクト指向によるGAS開発のススメ #4クラス設計のコツ
どうも。つじけ(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開発のススメ

  1. 要件定義
  2. 外部設計
  3. 内部設計・コーディング・テスト
  4. クラス設計のコツ
  5. シートクラス
タイトルとURLをコピーしました