【GASの始め方】リファクタリングで生成AIを活用しよう | GASおじさんのブログ
GASの基本

【GASの始め方】リファクタリングで生成AIを活用しよう

リファクタリングと生成AIの活用 GASの基本

みなさんこんにちは!GASおじさんです。

GASでスプレッドシートを自由自在に操るためのスキル習得講座の第12回です。

前回の記事はこちら。

前回は「オブジェクト」および「メソッド」とは何かについて学びました。

今回はこの知識を活用して、応用問題の解答をリファクタリングしていきます。また、生成AIの活用の仕方を解説していきます。

再び、応用問題レベル4

問題

解説にあたって、あらためて応用問題レベル4を確認します。

以下のスプレッドシート「GASをはじめよう!応用問題レベル4」を開いてコピーを作成してください。

GASをはじめよう!応用問題レベル4(コピー用)

【問題】こちらのスプレッドシートの「集計」シートに、「A社」〜「E社」の合計売上および平均売上を集計するGASプログラムを作成してください。

解答

前々回の記事で作成した解答を確認します。

function aggregateCompanySales() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName('集計');
  const companies = getCompanies();
  const values = [];
  for(const company of companies){
    values.push([company, getTotalSales(company), getAverageSales(company)]);
  }
  sheet.getRange(2, 1, values.length, values[0].length).setValues(values);
}

function getCompanies() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheets = ss.getSheets();
  const companies = [];
  for(const sheet of sheets){
    const sheetName = sheet.getName();
    if(sheetName != '集計'){
      companies.push(sheetName);
    }
  }
  return companies;
}

function getTotalSales(sheetName) {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName(sheetName);
  const targetRow = sheet.getRange('A:A').createTextFinder('合計')
                    .matchEntireCell(true).findNext().getRow();
  const targetCol = sheet.getRange('1:1').createTextFinder('売上')
                    .matchEntireCell(true).findNext().getColumn();
  return sheet.getRange(targetRow, targetCol).getValue();
}

function getAverageSales(sheetName) {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName(sheetName);
  const targetRow = sheet.getRange('A:A').createTextFinder('平均')
                    .matchEntireCell(true).findNext().getRow();
  const targetCol = sheet.getRange('1:1').createTextFinder('売上')
                    .matchEntireCell(true).findNext().getColumn();
  return sheet.getRange(targetRow, targetCol).getValue();
}

さて、こちらを観察して何か気づくことはありませんか?

getTotalSalesgetAverageSalesの2つの関数を見比べてみてください。

この2つ、よく似ていますね。似ているというか、ほとんど同じです。

異なるのはtargetRowを定める「合計」と「平均」のところだけで、それ以外は全部同じです。

こういう同じような処理を繰り返し記述するのは、いかにも効率が悪い書き方です。

リファクタリングしよう

こういうコードを見た時に、「もっと効率的に書けそうだな」と思ったら、そのタイミングで「リファクタリング」してあげましょう。

リファクタリングとは、ひとことでいうと「コードを整理して書き直すこと」です。

リファクタリング前とリファクタリング後でコードの実行結果は変わりませんが、内部の構造を改善することで、コードの可読性や保守性を高めることにつながります。

コードは放っておくと急激に複雑化していって、気づいた頃には取り返しのつかないほど肥大化してしまっていることがあります。そうなるともうどこでバグが発生するかわからず、「触らぬ神に祟りなし」の状態になってしまいます。

プログラムは創作意欲のあるうちに一気に作り切ることも大事ですが、ふと冷静に振り返った時に「もっと綺麗にかけないか」と自問自答してみましょう。そうすることで長期的なパフォーマンス向上につながります。

さて、それでは、前回新たに手に入れた武器「オブジェクト」を活用して、コードをリファクタリングしていきましょう。

オブジェクトを使ってコーディングする

salesオブジェクトを定義する

それでは、getTotalSales関数とgetAverageSales関数という2つの関数を、getCompanySales関数という1つの関数にまとめてみます。

function getCompanySales(sheetName) {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName(sheetName);
  const targetRowTotal = sheet.getRange('A:A').createTextFinder('合計')
                          .matchEntireCell(true).findNext().getRow();
  const targetRowAverage = sheet.getRange('A:A').createTextFinder('平均')
                          .matchEntireCell(true).findNext().getRow();
  const targetCol = sheet.getRange('1:1').createTextFinder('売上')
                    .matchEntireCell(true).findNext().getColumn();
  const sales = {
    total: sheet.getRange(targetRowTotal, targetCol).getValue(),
    average: sheet.getRange(targetRowAverage, targetCol).getValue(),
  };
  return sales;
}

注目は10〜13行目。

ここでsalesオブジェクトを定義しています。

こうすることで、sales.totalとすれば合計売上を取得できて、sales.averageとすれば平均売上を取得できますね。このオブジェクトを最後にreturnするという関数になっています。

では、このgetCompanySales関数をaggregateCompanySales関数の中で呼び出して、二次元配列valuesにpushするデータを[company, sales.total, sales.average]としてあげましょう。

function aggregateCompanySales() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName('集計');
  const companies = getCompanies();
  const values = [];
  for(const company of companies){
    const sales = getCompanySales(company);
    values.push([company, sales.total, sales.average]);
  }
  sheet.getRange(2, 1, values.length, values[0].length).setValues(values);
}

function getCompanies() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheets = ss.getSheets();
  const companies = [];
  for(const sheet of sheets){
    const sheetName = sheet.getName();
    if(sheetName != '集計'){
      companies.push(sheetName);
    }
  }
  return companies;
}

function getCompanySales(sheetName) {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName(sheetName);
  const targetRowTotal = sheet.getRange('A:A').createTextFinder('合計')
                          .matchEntireCell(true).findNext().getRow();
  const targetRowAverage = sheet.getRange('A:A').createTextFinder('平均')
                          .matchEntireCell(true).findNext().getRow();
  const targetCol = sheet.getRange('1:1').createTextFinder('売上')
                    .matchEntireCell(true).findNext().getColumn();
  const sales = {
    total: sheet.getRange(targetRowTotal, targetCol).getValue(),
    average: sheet.getRange(targetRowAverage, targetCol).getValue(),
  };
  return sales;
}

これで、getTotalSalesとgetAverageSalesで重複したコードを一つにまとめることができました。

このように、同じような処理をしている箇所があったら、「ひとつにまとめられないかな」と考えてリファクタリングしてみましょう。

リファクタリング後は、部屋の掃除をした後のような爽快感を味わうことができます。

さて、これで終わっても良いですが、もうちょっと改善してみます。

companyオブジェクトを定義する

先ほど作ったgetCompanySales関数の処理は、実はgetCompanies関数の中で記述することもできます。

getCopanies関数のif文の中身を以下のように書き換えてみましょう。

function getCompanies() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheets = ss.getSheets();  
  const companies = [];
  for(const sheet of sheets){
    const sheetName = sheet.getName();
    if(sheetName != '集計'){
      const targetRowTotal = sheet.getRange('A:A').createTextFinder('合計')
                            .matchEntireCell(true).findNext().getRow();
      const targetRowAverage = sheet.getRange('A:A').createTextFinder('平均')
                              .matchEntireCell(true).findNext().getRow();
      const targetCol = sheet.getRange('1:1').createTextFinder('売上')
                        .matchEntireCell(true).findNext().getColumn();
      const company = {
        name: sheetName,
        sales: {
          total: sheet.getRange(targetRowTotal, targetCol).getValue(),
          average: sheet.getRange(targetRowAverage, targetCol).getValue(),
        },
      };
      companies.push(company);
    }
  }
  return companies;
}

8〜21行目のif文の中身を書き換えました。

特に注目は14〜20行目。ここでオブジェクトcompanyが定義されています。

companyはname(会社名)とsales(売上情報)というプロパティを持っており、

さらにsalesはtotalaverageというプロパティを持っているオブジェクトとなっています。

こうすることで、

  • company.nameで会社名を取得
  • company.sales.totalで会社の合計売上を取得
  • company.sales.averageで会社の平均売上を取得

することできます。

for文のループの中で、このcompanyオブジェクトが配列companiesにpushされていった結果、最終的に以下のようなA社〜E社の情報をもった配列companiesが生成されることになります。

companies = [ 
  { name: 'A社', sales: { total: 60000, average: 20000 } }, // 1周目のcompany
  { name: 'B社', sales: { total: 75000, average: 15000 } }, // 2周目のcompany
  { name: 'C社', sales: { total: 100000, average: 10000 } }, // 3周目のcompany
  { name: 'D社', sales: { total: 120000, average: 10000 } }, // 4周目のcompany
  { name: 'E社', sales: { total: 150000, average: 10000 } }, // 5周目のcompany
];

もともとcompaniesは['A社', 'B社', 'C社', 'D社', 'E社']という、「会社名」という情報しか持っていない配列でしたよね。

これが、オブジェクトを活用することによって、売上情報を含めたより詳細な会社情報の配列に生成しなおすことができました。

このように、配列の中にオブジェクトを格納するパターンはよく使われます。

前回の記事で、「配列は複数形の情報を、オブジェクトは単数系の情報を扱うことが多い」という話をしましたが、これはつまりこういうことです。

配列は複数形、オブジェクトは単数系

それではあらためて、全体のコードを書き換えてみましょう。

function aggregateCompanySales() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName('集計');
  const companies = getCompanies();
  const values = [];
  for(const company of companies){
    values.push([company.name, company.sales.total, company.sales.average]);
  }
  sheet.getRange(2, 1, values.length, values[0].length).setValues(values);
}

function getCompanies() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheets = ss.getSheets();  
  const companies = [];
  for(const sheet of sheets){
    const sheetName = sheet.getName();
    if(sheetName != '集計'){
      const targetRowTotal = sheet.getRange('A:A').createTextFinder('合計')
                            .matchEntireCell(true).findNext().getRow();
      const targetRowAverage = sheet.getRange('A:A').createTextFinder('平均')
                              .matchEntireCell(true).findNext().getRow();
      const targetCol = sheet.getRange('1:1').createTextFinder('売上')
                        .matchEntireCell(true).findNext().getColumn();
      const company = {
        name: sheetName,
        sales: {
          total: sheet.getRange(targetRowTotal, targetCol).getValue(),
          average: sheet.getRange(targetRowAverage, targetCol).getValue(),
        },
      };
      companies.push(company);
    }
  }
  return companies;
}

7行目をcompanyオブジェクトを使って書き換えました。

いかがでしょうか。

リファクタリングする前は、

  • aggregateCompanySales
  • getCompanies
  • getTotalSales
  • getAverageSales

という4つあった関数を、

  • aggregateCompanySales
  • getCompanies

という2つの関数にまとめることができました。

「各社の売上を集計する」という実行結果は変わりませんが、コードの無駄がなくなりパフォーマンスの改善が期待できるでしょう。

AIにコードレビューしてもらおう

これにて完成…?

さて、無事リファクタリングも終わって、散らかったコードを「綺麗なコード」にすることができました。

しかし、ちょっと待ってください。「綺麗」というのは一体何を以て判断できるのでしょうか。

たしかに、リファクタリング前は明らかに重複した内容のあるコードだったので、それに比べたら綺麗になったのかもしれませんが、本当にこれ以上改善できることはないのでしょうか?

もしこういうときに現場に優しいベテランの先輩がいれば、的確なアドバイスをもらうことができるかもしれません。

しかし実際は、先輩が怖くてとても相談できないとか、そもそもエンジニアじゃないからプログラミングできる人が周りにいないとか、自営業で1人孤独に戦ってますとか、そういう人の方が多いと思います。

ただ、現代は恵まれた時代です。AIがあります。

AIはどんな相談でも真摯に答えてくれます。しかも無料で。これを使わない手はありません。

AIにコードレビューしてもらおう

ということで、AIに今回作成したコードをレビューしてもらいましょう。
※自分の書いたコードを他の誰かにチェックしてもらうことを「コードレビュー」と言います。

生成AIの有名どころはChatGPT、Gemini、Claudeなどがありますが、今回はChatGPTを使ってみます。

コードレビューしてもらう方法は至って簡単。

プロンプトに「以下のコードをレビューしてください」と書いて、それに続けて作成したコードをコピペするだけです。

これでぶん投げてみましょう。

すると、たった数秒で超詳細なレビューを返してくれます。

以下、ChatGPTからの応答の全文を掲載します。

このコードは、Google Apps Script を使用して、スプレッドシート内の各シートから特定の会社の売上データを集計し、それを「集計」という名前のシートに出力するものです。コードは全体的にわかりやすく書かれていますが、いくつかの改善点や考慮すべき事項があります。

改善点と提案

1. 例外処理の追加:

• createTextFinder で指定された値が見つからない場合や getRow() や getColumn() が失敗した場合にエラーが発生します。これに対する例外処理を追加することで、エラー発生時の動作を制御することができます。

2. スプレッドシートへの依存の最小化:

• getCompanies 関数内ではすべてのシートを取得しているため、シート数が多い場合にはパフォーマンスに影響が出る可能性があります。特定のシートだけを対象にする場合は、シート名のフィルタリングを関数外で行うことも検討できます。

3. パフォーマンスの最適化:

• それぞれのシートで createTextFinder を複数回呼び出しており、パフォーマンスに影響を与える可能性があります。複数の値を一度に取得するように、範囲をまとめて取得する方法に変更することも検討できます。

4. コードの可読性とメンテナンス性:

• aggregateCompanySales 関数で getCompanies 関数を呼び出していますが、values への push 部分を関数化することで、メイン処理の見通しを良くすることができます。

5. 定数の利用:

• 「集計」シート名を複数箇所で使用しているので、定数として管理することで、変更時にコード全体の修正が容易になります。

改善後のサンプルコード

function aggregateCompanySales() {
  const AGGREGATION_SHEET_NAME = '集計';
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName(AGGREGATION_SHEET_NAME);
  const companies = getCompanies(ss, AGGREGATION_SHEET_NAME);
  const values = companies.map(company => [company.name, company.sales.total, company.sales.average]);
  
  sheet.getRange(2, 1, values.length, values[0].length).setValues(values);
}

function getCompanies(ss, aggregationSheetName) {
  const sheets = ss.getSheets();  
  const companies = [];
  for (const sheet of sheets) {
    const sheetName = sheet.getName();
    if (sheetName !== aggregationSheetName) {
      const company = getCompanyData(sheet);
      if (company) {
        companies.push(company);
      }
    }
  }
  return companies;
}

function getCompanyData(sheet) {
  try {
    const totalFinder = sheet.getRange('A:A').createTextFinder('合計').matchEntireCell(true).findNext();
    const averageFinder = sheet.getRange('A:A').createTextFinder('平均').matchEntireCell(true).findNext();
    const salesFinder = sheet.getRange('1:1').createTextFinder('売上').matchEntireCell(true).findNext();

    if (!totalFinder || !averageFinder || !salesFinder) return null;

    const totalRow = totalFinder.getRow();
    const averageRow = averageFinder.getRow();
    const salesCol = salesFinder.getColumn();
    
    return {
      name: sheet.getName(),
      sales: {
        total: sheet.getRange(totalRow, salesCol).getValue(),
        average: sheet.getRange(averageRow, salesCol).getValue(),
      },
    };
  } catch (error) {
    Logger.log(`Error processing sheet ${sheet.getName()}: ${error}`);
    return null;
  }
}

主な変更点:

  • 例外処理を追加し、値が見つからない場合やエラーが発生した場合にログにエラーメッセージを記録します。
  • getCompanyData 関数を追加して、個々のシートから会社データを取得する処理を分割しました。
  • aggregateCompanySales 関数内の push 処理を map 関数に置き換え、コードの簡潔性を高めました。
  • 定数 AGGREGATION_SHEET_NAME を導入し、シート名を管理しています。

これらの変更により、コードの可読性、メンテナンス性、パフォーマンスが向上します。

ChatGPT

このように、生成AIに相談すれば超具体的なアドバイスをしてくれます。

もちろん、AIの言うことがすべて正しいというわけではないので、このアドバイスをすべて反映させる必要はありません。

冒頭で「コードは全体的にわかりやすく書かれています」とも評価してくれていますし、基本的には現状で満足してもいいのかなと思います。

「綺麗なコード」に正解はなく、それを追求しすぎるとキリがないということもありますので、ある程度のところで時間を区切ることも大切です。

開発を進めていく中で、「これは確かにその通りだな」と思う部分や、その場の状況に応じてAIによる改善案を適用していくといいでしょう。

まとめ

定期的にリファクタリングする習慣を身につけよう

以上、コードのリファクタリングについて解説しました。

開発に没頭していると「動くシステムを作ること」を優先して、「綺麗なコードを書くこと」はおざなりになるということがよくあります。

もちろん、それ自体は悪いことではありません。

“Done is better than perfect.”という言葉があるように、いきなり完璧なものを目指すのではなく、まず終わらせるというのはとても大切なことです。

しかし、ある程度開発が進んだところでコードを見直すという習慣を身につけないと、バグの温床だらけのとんでもなくやっかいなシステムが出来上がることになります。

問題が大きくなる前に定期的にリファクタリングする癖をつけておきましょう。

ここで、綺麗なコードを書くために読むべき名著を紹介しておきますので、興味のある人はぜひ読んでみてください。

困ったらすぐAIに相談する習慣を身につけよう

また、困ったときはすぐにAIに相談すると、開発が捗ります。

私は常時「ChatGPT」と「Gemini」と「Claude」の3人をデスクトップアプリで起動しており、いつでもこの3人に相談できる体制を整えています。

このようにAIと共存・共生していく術を身につけることは、これからの時代には必須となってきます。

巷ではAIに職を奪われる話で賑わいがちですが、こんなに頼りになる友はいません。敵に回すのではなく味方につけましょう。

それではまた!

連載目次: GASでスプレッドシートを自由自在に操るためのスキル習得講座

  1. 【GASの始め方】まずはスプレッドシートの操作から始めてみよう
  2. 【GASの始め方】setValuesで複数のセルに値を入力しよう
  3. 【GASの始め方】getValueで値を取得してsetValueで入力しよう
  4. 【GASの始め方】getValuesで複数のセルの値を取得しよう
  5. 【GASの始め方】getValuesして別のシートにsetValuesしよう
  6. 【GASの始め方】応用問題で関数について学ぼう
  7. 【GASの始め方】繰り返し処理の「for文」を習得しよう
  8. 【GASの始め方】flat()でループさせる配列を自動生成しよう
  9. 【GASの始め方】for文とif文でデータ抽出して配列を生成しよう
  10. 【GASの始め方】TextFinderで行と列を特定しよう
  11. 【GASの始め方】オブジェクトとメソッドについて学ぼう
  12. 【GASの始め方】リファクタリングで生成AIを活用しよう

GASを勉強するならこちら!

▼オススメ書籍はこちら!

スポンサーリンク
GASおじさんをフォローする

コメント

タイトルとURLをコピーしました