AI駆動開発でモダンにアプリケーションを開発しよう #Developer&Designer Advent Calendar 2024

Developer&Designer Advent Calendar 2024

本記事はDeveloper&Designer Advent Calendar 2024の12月18日の記事です。

はじめに


こんにちは。パーソルキャリアサービス支援部の高屋です。
バックエンドエンジニアとして、主にwebサービスのソフトウェアアーキテクチャの設計やAPIの設計、実装などを担当しています。

いきなりですが、みなさんも生成AIにふれる機会が増えてきているのではないでしょうか。
2022年に公開されたChatGPTから生成AIは広く世の中に普及しており、弊社でも社内版生成AI「ChatPCA(通称:チャッピー)」を使った業務改善が広まっています。
私もバックエンドの開発においてこれらの生成AIを活用し始めており、パフォーマンスが劇的に改善したと感じています。そればかりか、いわゆる「モダンな開発」へのハードルもぐっと下がったと思っています。

本記事ではこのAIを使った開発、「AI駆動開発」を普段どのように行っているか、AIを利用してどうモダナイズされた開発を実現しているかを紹介したいと思います。

AI駆動開発とは


AI駆動開発に関する書籍なども少しづつ出版されていますが、定義としては「生成AIを使った設計、実装」を示している認識です。
実際、多くの書籍で触れられていたり、我々も想像しやすいのが「GitHub Copilot」を使ったコード生成やサジェスト機能です。Copilot(副操縦士)の名の通り、そのコードのコメント、前後関係、エディタで開いている他タブのコード、そしてGitHub上に無数に貯蔵されているコード群から適切なコードを生成・サジェストしてくれます。
しかし、この機能だけがAI駆動開発ではありません。GitHub Copilotを契約していなければAI駆動開発ができないわけではないのです。それでは他にどんな場面でどのように活用しているかを紹介していきます。

ネーミング

ネーミングの重要性をいまさら説くわけではありませんが

a = b * c

と記述するより

selling_price = cost_price * tax_rate


と記述したほうが一目瞭然です。コードを前後にさかのぼって読解せずともこの1行で
「あぁ、原価に消費税をかけて売価にしているんだな」
と理解できます。これはつまり、チームの生産性や保守性が向上し、今後の不具合の発生率もぐっと抑えられるという一石二鳥、いや三や四鳥くらいの効果がある簡単ながら馬鹿にできない実装手法です。
冒頭で「モダンな開発」という表現をしましたが、何もクラウドサービスを使うだけがモダンではありません。小さいことですが、こういう部分も十分にモダンな開発の一部だと考えています。


さて、上記は変数名ですが、クラス名、メソッド名、エンドポイント名、テーブル名、カラム名…などネーミングの場面は設計、実装時に実によく出てきます。しかし、自慢することではないですが私は英語が苦手です。なのでAI駆動開発をする前まではネーミングに非常に時間がかかっていました。
ネーミングに生成AIを活用すると、単に英単語で表現してくれるだけではなく、適切な言い回しを表現したサンプルを提示してくれます。


たとえば皆さんなら「重要度」をどのように表現するでしょうか。


「重要?えーとimportantだっけ。となると重要度は…うーん inportant_rateとか??」


実際僕が聞いたプロンプトは以下の通りです。

プロンプト

「カラム名を5つ提案してください。格納する内容は、質問ごとに設定するその質問の重要度です。カラム名には日本語訳も付与してください。」

回答

質問ごとに設定する重要度を格納するカラム名として、以下のような候補があります:

importance_level(重要度レベル)
priority(優先度)
significance(重要性)
weight(重み)
relevance(関連性)

これらのカラム名は、重要度の内容に応じて選ぶと良いかと思います。

と返してくれました。ものの数秒で実に適切な表現が得られるわけです。試しに自分が考えた important_rateはどうか聞いてみましょう。

プロンプト

「inportant_rateではおかしいでしょうか?」

回答

important_rate という名前でも、伝わらないわけではありませんが、英語としてはやや不自然に感じます。以下の理由から、他の表現を検討することをお勧めします。

問題点

important の形容詞使用
important は形容詞なので、「~率(rate)」という名詞と組み合わせると文法的に不自然です。形容詞を名詞化した形(例: importance)を使うほうが自然です。

rate の意味の違い
rate は「割合」や「速度」を意味することが多く、重要度を表現するニュアンスに適していません。例えば「5段階評価で重要度を表す」場合は level や score などの方が適切です。


しっかりダメ出しされました。生成AIはやんわり指摘してくれるのが逆にダメージです。
いやー恥ずかしい。

ユニットテスト

例えばマイクロサービスで開発をしようとか、CI/CD組んでスモールスタートで始めようとか、モダンな開発手法はいくつもありますが、そのどれとも切っては切れない関係がこのユニットテストです。特にwebサービスの開発はローンチしたらおしまいというわけではなく、その後も運用と機能追加が待っています。機能が増えれば増えるほど指数関数的に不具合やデグレの危険性が増してしまうのはいただけません。

しかし、ユニットテストがないプロジェクトやサービスはたくさんあります。書かなかった、もしくは書けなかった理由は

  • 難しくてよくわからない
  • テストを書く時間やコストが取れない
  • メンバーにテストの書き方を教えるヒマがない
  • 1回作ったら消してしまう検証用なので必要がない
  • すでに運用しているが、テストを書けるような状態ではない


といったあたりでしょうか。
たしかに検証用のケースでわざわざ書くまでもないですし、技術的負債がたまっているサービスは厳しいかもしれません。しかしそれ以外の理由でテストを書かないのであれば、まさにAI駆動開発が役に立ってくると思います。


実際に活用方法を紹介しましょう。私は普段、PythonのフレームワークFastAPIを利用して開発を行っています。FastAPIではAPIリクエストの型定義をして、厳密かつ柔軟にバリデーションを行えますが、そのバリデーションのテストパターンを作るのはパラメータが増えれば増えるほど面倒です。

from typing import Union
from pydantic import BaseModel, Field, model_validator


class DiaryCreateRequest(BaseModel):
    diary_code: str = Field(..., min_length=13, max_length=13, alias="diaryCode")
    author_id: int = Field(..., alias="authorId")
    title: Union[str, None] = Field(...)
    body: Union[str, None] = Field(...)

    @model_validator(mode="after")
    def check_fields(self):
        if not self.title and not self.body:
            raise ValueError("Either title or body must be defined")
        return self

    class Config:
        extra = "forbid"


たとえば日記登録APIがあったとして、こんなモデルを作ってみました。このリクエストのバリデーションテストケースはええと…と考える間に生成AIで作成できちゃいます。


プロンプト

次のpydantic2.10のモデルがあります。これは、FastAPIで使用する辞書登録APIのリクエストモデルです。pytestを書くため、以下のモデルのバリデーションを網羅したテストパターンを作成してください。
エラーの際、エラーコードは400(Bad request)とします。
フォーマットは、
@pytest.mark.parametrize(
(
"input_data",
"expected_status",
"expected_error",
),
get_validate_test_cases(),
)
で取得できる形でお願いします。
(以下上記のpythonコードを貼り付け)

結果

def get_validate_test_cases():
    return [
        # Valid input: Minimum required fields
        ({
            "diaryCode": "1234567890123",
            "authorId": 1,
            "title": "Sample Title"
        }, 200, None),

        # Valid input: Body provided instead of title
        ({
            "diaryCode": "1234567890123",
            "authorId": 1,
            "body": "Sample Body"
        }, 200, None),

        # Valid input: Both title and body provided
        ({
            "diaryCode": "1234567890123",
            "authorId": 1,
            "title": "Sample Title",
            "body": "Sample Body"
        }, 200, None),

        # Invalid input: diary_code too short
        ({
            "diaryCode": "123",
            "authorId": 1,
            "title": "Sample Title"
        }, 400, "ensure this value has at least 13 characters"),

        # Invalid input: diary_code too long
        ({
            "diaryCode": "12345678901234567890",
            "authorId": 1,
            "title": "Sample Title"
        }, 400, "ensure this value has at most 13 characters"),

        # Invalid input: author_id missing
        ({
            "diaryCode": "1234567890123",
            "title": "Sample Title"
        }, 400, "field required"),

        # Invalid input: title and body both missing
        ({
            "diaryCode": "1234567890123",
            "authorId": 1
        }, 400, "Either title or body must be defined"),

        # Invalid input: extra field provided
        ({
            "diaryCode": "1234567890123",
            "authorId": 1,
            "title": "Sample Title",
            "extraField": "Not Allowed"
        }, 400, "extra fields not permitted"),

        # Invalid input: diary_code missing
        ({
            "authorId": 1,
            "title": "Sample Title"
        }, 400, "field required"),

        # Invalid input: author_id is not an integer
        ({
            "diaryCode": "1234567890123",
            "authorId": "string",
            "title": "Sample Title"
        }, 400, "value is not a valid integer"),

        # Invalid input: Empty input
        ({}, 400, "field required"),
    ]


このように、標準のバリデートパターンだけではなく、オリジナルで実装したカスタムバリデーションパターンまで網羅したデータが出力されます。

実はこのあと、このデータを使ったテストコードまで生成してくれました。そもそもテストの書き方がわからなくてもこれでとっかかりはできますし、メンバー全員にテストコードの書き方を教育する時間がないような状況でもプロンプトパターンを作って共有すればある程度の品質を担保したテストコードが実装されていくというわけです。

これで「テストを書くのは工数がかかる」「予算が、スケジュールがない」という問題点はかなり解消されると思いますし、「テストはよくわからない」という部分もAIとペアプログラミングをしているようなものなので、例となるコードを生成してもらうことでかなり理解が進むと思います。


ただ、1点気をつけなければならないのが、この例に限りませんが出力されたコードがすべて正しいというわけではない点です。
例えば今回のケースだとエラーメッセージは全然違います。pydantic2.10のメッセージに近いですが、別物です。出力されるバリデートパターンも100%網羅していないケースもたまにあります。あくまで「アシスタント」という位置づけで向き合い、コードに取り込む際は最終的に使う方がきちんとレビューをすることが大事です。

モダンなコードサジェスト

例えば売上レポートの生成処理を書くとしましょう。pdfとcsvとhtmlで出力するような処理です。
自分で書いてもいいですし、GitHub Copilotでコードサジェストしてもらってもなんとなくこんな処理になるかと思います。

class ReportGenerator:
    def generate_report(self, data, format_type):
        if format_type == "pdf":
            return self._generate_pdf(data)
        elif format_type == "csv":
            return self._generate_csv(data)
        elif format_type == "html":
            return self._generate_html(data)
        else:
            raise ValueError("未知のフォーマット")

    def _generate_pdf(self, data):
        return f"PDFレポート: {data}"

    def _generate_csv(self, data):
        return f"CSVレポート: {data}"

    def _generate_html(self, data):
        return f"HTMLレポート: {data}"

# 使用例
data = "売上データ"
generator = ReportGenerator()
print(generator.generate_report(data, "pdf"))
print(generator.generate_report(data, "csv"))
print(generator.generate_report(data, "html"))


pythonはバージョン3.9まではswitchがない(3.10以降match文ができましたね)ので、ifの連続になっていますが、switchがある言語は置き換えることで多少見やすくはなるでしょうか。


しかし、このコードは決してモダンとは言えません。
新しいフォーマット、例えばエクセルやパワーポイントで生成するような事になった場合、generate_report関数はどんどん肥大化していきます。さらに全ケースgenerate_reportのif(またはswitch)判定を通る=1つの変更が既存のコード全体に影響を及ぼしている状態になり、デグレの危険性が未来永劫増え続ける「閉鎖的な」コードだからです。

ではどのように疎結合で安全なモダンなコードにするのでしょう。解決方法を生成AIに提案してもらいます。


プロンプト

以下のようなレポート生成ロジックがあります。しかしこのままでは、フォーマットを追加するたびにif構文が冗長になるし、デグレの危険性が増します。
SOLID原則にのっとったコードを提案してもらえますか?
(以下上記のサンプルコードを貼り付け)

結果

from abc import ABC, abstractmethod

# フォーマットごとの生成ロジックをStrategyとして定義
class ReportFormat(ABC):
    @abstractmethod
    def generate(self, data):
        pass

class PDFReport(ReportFormat):
    def generate(self, data):
        return f"PDFレポート: {data}"

class CSVReport(ReportFormat):
    def generate(self, data):
        return f"CSVレポート: {data}"

class HTMLReport(ReportFormat):
    def generate(self, data):
        return f"HTMLレポート: {data}"

# レポート生成クラスはフォーマットごとの依存を排除
class ReportGenerator:
    def __init__(self):
        self.formats = {}

    def register_format(self, format_type, format_handler):
        self.formats[format_type] = format_handler

    def generate_report(self, data, format_type):
        if format_type not in self.formats:
            raise ValueError(f"未知のフォーマット: {format_type}")
        return self.formats[format_type].generate(data)

# 使用例
data = "売上データ"
generator = ReportGenerator()

# フォーマットを登録
generator.register_format("pdf", PDFReport())
generator.register_format("csv", CSVReport())
generator.register_format("html", HTMLReport())

# レポート生成
print(generator.generate_report(data, "pdf"))   # PDFレポート: 売上データ
print(generator.generate_report(data, "csv"))   # CSVレポート: 売上データ
print(generator.generate_report(data, "html"))  # HTMLレポート: 売上データ


さらにこのようなレビューももらいました。

このリファクタリングのポイント
1. 単一責任原則(SRP)
・ ReportGenerator はレポートの生成ロジックではなく、フォーマットの管理に専念。
・ 各フォーマットの生成ロジックは ReportFormat を継承したクラスに分離。

2.オープン/クローズド原則(OCP)
・ 新しいフォーマットを追加する際は、新しい ReportFormat を実装し、register_format で登録するだけで対応可能。既存のコードは変更不要。

3. 拡張性と保守性の向上
 ・フォーマットの数が増えても ReportGenerator に手を加える必要がないため、if構文の増加やデグレのリスクが回避できる。

このアプローチにより、拡張性の高い設計が実現できます。


と、非常に理想的な申し分ないコードです。
SOLID原則とストラテジーデザインパターンという、コードを書き始めて最初の方で学んだ原理原則ではありますが、それらが有効活用されている疎結合な拡張性高いモダンなコードであると言えます。
ちょっとひと手間加えるだけで適切なリファクタリングを提案してもらえ、誰でもモダンな実装が行えるのはシステムを作る上でかなり効果的だと思います。

最後に


今回はAI駆動開発でモダンにアプリケーションを開発しようと題して、いくつかのAI駆動開発例を紹介しました。非常に便利で強力なのがお伝えできたと思っています。
しかし、現状は先ほどユニットテストでも触れましたが、AIが提案してくれるものすべてが100%正しいとは言えない状況です。あくまでアシスタント、またはペアプログラミングの相手としてAIと対話しながら活用していくのが正しい向き合い方なのではないかと考えています。
そして、いままで予算や納期、学習機会といったハードルで(やらなきゃいけないのはわかっていたが)やりたくともできなかったモダンな開発への手がかりとしていただければ幸いです。



alt


高屋克啓 Katsuhiro Takaya


新規サービス開発統括部 サービス支援部 エンジニアリンググループ リードエンジニア

Webアプリのバックエンドエンジニアとして、新規サービス開発を担当しています。技術的負債を最小限に抑えた拡張性の高いシステムを設計・開発することに注力しています。

※2024年12月現在の情報です。