[GAS]オブジェクト指向によるGAS開発のススメ

GAS

どうも。つじけ(tsujikenzo)です。このシリーズでは、[GAS]オブジェクト指向によるGAS開発のススメ、をお届けします。

今日のアジェンダ

  1. 要件定義
  2. 外部設計
  3. 内部設計・コーディング・テスト
  4. クラス設計のコツ
  5. シートクラス

はじめに

前回(GAS中級講座5期)の、卒業LTでは 「ノンプログラマーによるGAS開発モデル」 という、内容をお届けしました。

プロの開発手法をまなび、ノンプログラマーにも取り入れられる要素はないか、という考察をしたものです。

[ノンプロ研]GAS中級講座5期卒業LT ノンプログラマーによるGAS開発モデルとは
どうも。つじけ(tsujikenzo)です。このシリーズでは、2021年5月から始まりました「ノンプロ研GAS中級講座5期」について、全8回でお届けします。今日は8回目で最終回です。最終回は、なんと3回でお送りいたします。前回のおさらい前回

今回のシリーズでは、その進化版で、オブジェクト指向のエッセンスを取り入れたGAS開発を、お届けします。(ノンプログラマーのわたしが、個人的に学んだことをアウトプットしますので、間違いなどはご容赦ください)

オブジェクト指向のエッセンス

流行り廃りもありますが、プログラムの開発・保守・運用を楽にする手法の1つが、オブジェクト指向プログラミングです。

わたしも、勉強をはじめたばっかりで、少しずつブログ化しています。

[GAS][DDD]1章オブジェクト指向とはなにか 1.はじめに
どうも。つじけ(tsujikenzo)です。このシリーズでは「Google Apps Scriptとドメイン駆動設計」についてお届けします。全部で何章のシリーズになるかわかりません。少しずつ更新します。第1章は、「オブジェクト指向とはなにか

オブジェクト指向とは、実世界のヒト・こと・モノを「オブジェクト」という単位でとらえ、ソースコードに反映させることです。

GAS開発を楽にしてくれる手法だと信じていますので、興味のあるかたはぜひ、一緒に学びましょう。

今日、第1回目は、「要件定義」から始めていきます。

ユースケース図

要件定義をするときに便利なツールが、ユースケース図です。

今回は、「スプレッドシートで日報を作成し、マネージャーにPDFをメールする」という、業務アプリケーションを作成したいと思います。

ユースケース図を書こう

ユースケース図は、「そのシステムでユーザーがなにをできるのか」を可視化したものです。

あまりむずかしく考えずに、直感で書いてみましょう。

まず、システムを真ん中に置いてみます。 

こと

次に、やりたいこと(○○する)を並べてみます。ここで、「将来的に使うかもしれない」という機能は、記入しないことをオススメします。(たとえばGoogleドライブに保存する、とかSlackに通知を飛ばすなど)

本当に必要な、コアな部分に集中しましょう。

とくに、スプレッドシートにレコードを追加するばあいなどは、「フォームから入力させたい」と考えるかもしれません。

しかし、現段階では「スプレッドシートを開いて、1行追加する」と考えましょう。

フォームから入力などは、あとで機能を追加すればいいのです。

ヒト

システムを操作するヒトが必ずいると思いますので、ヒトを記入します。

この段階では、ヒトは、それぞれの処理を実行できると考えていいでしょう。 

いずれは、処理はすべて連携され、「日報を入力するだけで、メールが送信される業務アプリケーション」にしたいかもしれません。

しかし、まずは、個別に処理を実行できるという考え方が大事です。

トリガーなどは、あとで機能を追加すればいいのです。

モノ

ヒトが処理することに対して、アイコンなどをおいて、なにを操作するのかイメージを固めましょう。

とくに、とりあつかうものが、スプレッドシートなのか、PDFなのか、メールなのか、など、具体的なアイコンがあるとわかりやすいです。 

このシステムと連携するものとして、日報を管理する人事部のシステムがあるかもしれません。

しかし、今後連携するシステムのモノまで考える必要はありません。別のシステムとは、APIで連携させていきます。

API連携などは、あとで機能を追加すればいいのです。(システムをAPIで連携したものを、マイクロサービスと呼びます。)

完成図

実際に使用するシートなどが決まっていたり、定義できるばあいは、ユースケース図に書き込んでしまいます。

完成したユースケース図はこちらです。 

まとめ

以上で、「要件定義」をお送りしました。

ウォーターフォール開発のように、ガチガチに要件定義をして、仕様を細かく決めるのではなく、ユースケース図をざっくりと書くのがコツです。

ガチガチに書いても、あとで変更は必ず発生します。なにごとも「ざっくり」でいきましょう。

申し遅れましたが、開発にはMURALJamboardといったホワイトボードツールがオススメです。

次回は、 「外部設計」 をお届けします。

参考資料

前回は、「要件定義」をお届けしました。

[GAS]オブジェクト指向によるGAS開発のススメ #1要件定義
どうも。つじけ(tsujikenzo)です。このシリーズでは、2021年9月から始まりました「ノンプロ研GAS中級講座6期」について、全7回でお届けします。今日は7回目で最終回です。最終回は、なんと5回に渡ってお送りいたします。前回のおさら

今回は、2回目で「外部設計」です。

業務アプリケーションを外側からみたら、どのように見えるのかを決めていきます。

今日のアジェンダ

  • クラス図
  • 業務フロー図

クラス図

前回作成したユースケース図では、業務アプリケーションで必要な、ヒト・こと・モノに登場してもらいました。 

この、ヒト・こと・モノをオブジェクトとしてとらえてみましょう。

クラスを作成する

ユースケース図に登場した、ヒト・こと・モノをすべてオブジェクトとして書き出してみます。 

復習ですが、クラスは、オブジェクトを生成する機能です。

具体的なオブジェクトを眺めながら、どんなクラスを作成したら、必要なオブジェクトが生成されるかを考えます。

ヒト

たとえば、ヒトでいうと、Staffクラスが1つあれば、インスタンスに、辻オブジェクトと高橋オブジェクトが生成できそうです。 

スプレッドシート

スプレッドシートは、データ入力用、加工用、印刷用、という構造にすると管理がしやすいです。(詳しくはこちらのブログで)

データ加工はGASで行いますので、「データ入力用」と、「印刷用」のシートを用意しましょう。 

そのまま、クラス化します。 

[つじけコラム] ユーティリティクラスは作るの?

GWSの、SpreadSheetクラスの便利な関数を集めた、ユーティリティSpreadSheetクラスなどは、できれば作りません。

もし、便利な関数などを作成し、使い回ししたいばあいは、ライブラリを活用するといいでしょう。

シートは、インスタンスごとに1枚作成してもかまいませんし、1枚を共有してもかまいません。

シートの自由な運用ができるのも、クラス化のメリットです。

PDF

PDFを担当するクラスは、業務アプリケーションの規模によって、切り分けが必要です。

業務アプリケーションの規模が小さい場合は、PDFクラスでさまざまな処理を担当してよいでしょう。

PDFクラスのお仕事

  1. PDFを作成・保存
  2. PDFを探す
  3. PDFの結合・分割
  4. 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()

ご留意くださいませ。

次回は、 「内部設計・コーディング・テスト」 をお届けします。

前回のおさらい

前回は、「外部設計」をお届けしました。

[GAS]オブジェクト指向によるGAS開発のススメ #2外部設計
どうも。つじけ(tsujikenzo)です。このシリーズでは、2021年9月から始まりました「ノンプロ研GAS中級講座6期」について、全7回でお届けします。最終回の7回目を、5回に渡ってお送りしています。前回のおさらい前回は、「要件定義」を

今回は、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つずつつぶしていきます。 

「バグに対処するために、もうすこし早めに、コードが動くかどうかテストしたい」という声もあると思います。

しかし安心してください。バグの必要な修正箇所は、比較的見つかりやすいです。

それは、メソッドが小さいからです。メソッドとメソッドは複雑に絡み合っていません。

このような書き方を「疎結合(そけつごう)」と言います。

メソッドを書くときは、疎結合を意識していきましょう。

まとめ

以上で、「内部設計・コーディング・テスト」をお送りしました。

チームで開発をするときなど、他人のコードが、コーディングガイドラインに違反した書き方だと、ついつい口を出してしまうものです。

しかし、まずはテスト関数が動いているなら中身(書き方)は問わない、という安心感を提供することで、チームの士気を高めます。

リファクタリングもしかりで、まずはコードが動くことを確認しながらコーディングするテクニックを、取り入れてみましょう。

テスト駆動開発、おススメです。

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

前回のおさらい

前回は、「内部設計・コーディング・テスト」をお届けしました。

[GAS]オブジェクト指向によるGAS開発のススメ #3内部設計・コーディング・テスト
どうも。つじけ(tsujikenzo)です。このシリーズでは、2021年9月から始まりました「ノンプロ研GAS中級講座6期」について、全7回でお届けします。最終回の7回目を、5回に渡ってお送りしています。それって、全12回じゃないのでしょう

今回は、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開発のススメ #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. シートクラス

Comments

Copied title and URL