🐕

あえてAIに「怪しいコード」を書いてもらい、設計力を鍛える

に公開

AIコーディングツール(Cursor, Copilot等)の普及で、コードの品質を意識する機会が増えました。一方で、生成されたコードをレビューした際に、「このコード、なんとなく良くないな」と思ったことありませんか?

その「なんとなく」を言語化する力、つまり「設計力」が今、ますます重要です。本記事では、AIを活用して設計力を効率的に鍛える「リファクタリング実践法」を紹介します。

「なんとなく良くない」は実際良くない

アプリ開発を行う上で、以下の状況に遭遇したことはありませんか?

  • 変数名や関数名が処理内容を表していない
  • 処理が複雑すぎて、理解に時間がかる
  • 機能を追加する時間が次第に増えていく
  • 少し変更しただけなのに予期せぬ箇所に影響が出てしまう

といった状況です。

これらは、コードの品質、具体的には変更容易性が低下している危険信号です。そして、こうした問題の根っこをたどっていくと、多くの場合、ソフトウェア設計の原則(SOLID原則、凝集度、結合度など)に違反していることが原因です。

「なんとなく」の状態のままでは、「まあ動いているからいいか」と見過ごしてしまったり、良くないと分かっていても具体的にどこから手をつければ良いのか分からず、改善が進まないといった問題が発生しがちです。その「なんとなく」の正体を突き止め、設計原則に照らして具体的に指摘・言語化できるようになることが、コードの品質を一段階引き上げる鍵となります。

リファクタリングを通じて設計力を鍛える

設計原則は知識として学べますが、実践しなければ身につきません。怪しいコードをより良く書き換える「リファクタリング」こそ、設計判断を磨く最高のトレーニングです。

しかし、適切な練習材料となる「改善のしがいがあるコード」を都合よく見つけるのは意外と難しいものです。既存のプロダクトコードをいきなり触るのはリスクが伴いますし、ちょうど良い複雑さのサンプルを探すのも手間がかかります。

そこで僕がやっているのは、AIを使って怪しいコードを生成してもらい、それを自らの手でリファクタリングする方法です。

このアプローチには、以下のようなメリットがあります。

  • 手軽に練習問題を入手できる: 既存のレガシーコードを探したり、自分でサンプルを作る手間が省けます。
  • 特定のテーマに沿ったコードを生成できる: SOLID原則違反、凝集度の低いコードなど、学びたいテーマに合わせた「悪い例」をAIに意図的に作らせることができます。
  • 安全な環境で試行錯誤できる: 生成されたコードは練習用なので、気兼ねなく様々なリファクタリング手法を試せます。プロダクションコードを壊す心配がありません。

プロンプト例:

例えばSOLID原則を学びたい場合には、以下のプロンプトが利用できます。その他にもDDDやクリーンアーキテクチャーを学びたい場合には、プロンプトを書き換えてください。

プログラミング経験はあるが、SOLID 原則やクリーンコードにはまだ不慣れなプログラマーとして振る舞ってください。
リファクタリングを通じてソフトウェア設計のベストプラクティスを学ぶために、「改善の余地」が大きいコードを意図的に生成してください。

1時間でリファクタリングが完了する規模にしてください。
上記プロンプトで生成されたコード
import * as fs from 'fs';
import * as path from 'path';

// 全ての処理を1つのクラスに詰め込んだマネージャークラス
class SalesDataManager {
  private data: any[] = [];
  private filePath: string;

  constructor() {
    // ファイルパスを直接ハードコード
    this.filePath = path.join(process.cwd(), 'sample-data.csv');
  }

  // CSVを読み込んで解析する処理
  public loadData(): void {
    const content = fs.readFileSync(this.filePath, 'utf-8');
    const lines = content.split('\n');
    const headers = lines[0].split(',');

    this.data = lines.slice(1).map(line => {
      const values = line.split(',');
      const record: any = {};
      headers.forEach((header, index) => {
        record[header] = values[index];
      });
      return record;
    });
  }

  // 地域ごとの売上集計
  public calculateRegionalSales(): void {
    const regionalSales: { [key: string]: number } = {};

    this.data.forEach(item => {
      if (regionalSales[item.region]) {
        regionalSales[item.region] += parseInt(item.sales);
      } else {
        regionalSales[item.region] = parseInt(item.sales);
      }
    });

    console.log('地域別売上:', regionalSales);
  }

  // カテゴリごとの売上集計
  public calculateCategorySales(): void {
    const categorySales: { [key: string]: number } = {};

    this.data.forEach(item => {
      if (categorySales[item.category]) {
        categorySales[item.category] += parseInt(item.sales);
      } else {
        categorySales[item.category] = parseInt(item.sales);
      }
    });

    console.log('カテゴリ別売上:', categorySales);
  }

  // 月次レポート生成
  public generateMonthlyReport(): void {
    let totalSales = 0;
    let maxSale = 0;
    let bestProduct = '';

    this.data.forEach(item => {
      const sale = parseInt(item.sales);
      totalSales += sale;
      if (sale > maxSale) {
        maxSale = sale;
        bestProduct = item.name;
      }
    });

    const averageSales = totalSales / this.data.length;

    console.log('月次レポート');
    console.log('総売上:', totalSales);
    console.log('平均売上:', averageSales);
    console.log('最高売上商品:', bestProduct);
    this.saveReportToFile('monthly_report.txt', {
      total: totalSales,
      average: averageSales,
      bestProduct
    });
  }

  // レポートをファイルに保存
  private saveReportToFile(filename: string, data: any): void {
    const content = JSON.stringify(data, null, 2);
    fs.writeFileSync(filename, content);
  }

  // データ検証(簡易的な実装)
  public validateData(): boolean {
    let isValid = true;
    this.data.forEach(item => {
      if (!item.id || !item.name || !item.sales || !item.region || !item.category || !item.date) {
        console.error('Invalid data found:', item);
        isValid = false;
      }
    });
    return isValid;
  }
}

// 使用例
const manager = new SalesDataManager();
manager.loadData();

if (manager.validateData()) {
  manager.calculateRegionalSales();
  manager.calculateCategorySales();
  manager.generateMonthlyReport();
} else {
  console.error('データの検証に失敗しました');
}

リファクタリング実践の3つのポイント

リファクタリング時には以下を意識しましょう。

問題の特定と改善方針の言語化

なぜ良くないのか明確に言語化しましょう。(例: 凝集度が低い、単一責任の原則違反、リポジトリパターンを採用していない)。その上でどうすれば良くなるかを考えましょう。「なんとなく」を具体的な言葉にするのが重要です。

優先順位付けとスコープ設定

リファクタリングの優先順位付けを行いましょう。コアドメイン(アプリの最も重要な箇所)のリファクタリングをせずに、優先度の低いものを優先しては本末転倒です。何を優先して、どこまでやるか、時間や目標を決めて区切りをつけましょう。

ユニットテストの実装

リファクタリングではアプリの振る舞いを変えてはいけません。それを保証するためにも、リファクタリングに着手する前にユニットテストを実装しましょう。テスト実装も練習したい場合は自分で書く、効率重視ならAIを活用するなど、目的に応じて使い分けましょう。

本を読んで学ぶ

book-image

設計力をさらに高めたい、まずは設計原則を学習したいという方は、本を読むことをお勧めします。本を読むことで「あーあそこはここが悪いのか」や、「こういうパターンがあるんだ」と言う気づきを効率良く学習することができます。僕がお勧めする本は良いコード/悪いコードで学ぶ設計入門です。

本のタイトル通り、良いコード、悪いコードを通じて、設計パターンを紹介してくれます。この本はわかりやすい上に、本で紹介している設計パターンをいくつか実践できるば、あなたの設計力、怪しいコードを見抜く力はかなり向上します。

まとめ

AIの時代だからこそ、コードの良し悪しを判断し改善する「設計力」がエンジニアにとって重要な資産となっていきます。AIを練習相手にしてリファクタリングを実践し、そのスキルを磨きましょう。

Discussion