😭

ボイラープレート自動生成ツールを使わなくなった話

2024/08/26に公開

こんにちは、sugar-catです。

AI Shiftでは生成AIを活用して業務改善を促進するサービス(AI Worker)の開発を行っています。
https://zenn.dev/aishift/articles/ce9783a0d7acd0

我々のチームでは、プロジェクトが発足した当初に作成したボイラープレート自動生成ツールを作りましたが、最近その役目を終えて削除しました。
この記事では、ボイラープレート自動生成ツールを使わなくなった理由とその背景について紹介します。
※この記事の内容は以下のスライドの内容を書き起こし一部加筆したものです。

https://speakerdeck.com/sugarcat7/hoirahuretozi-dong-sheng-cheng-turuwoshi-wanakunatutahua

はじめに

構成と背景

AI Workerでは、ざっくりと以下のような構成を取っており、API Server内でボイラープレート自動生成ツールを使っていました。
alt text

特に弊チームではHono + Zod OpenAPIを採用していたため、Zod SchemaをSSOT(Single Source of Truth)として扱い、フロントエンド・バックエンドを並行してスピーディーに開発することができていました。
alt text

このAPI Server自体はクリーンアーキテクチャ+DDDで構成されており、レイヤー間の依存を減らすためにコードを分割していました。
alt text

ご存知の通りこのアプローチは。長期的に見れば複雑化したコードの保守性や拡張性を高めるために有効ですが、開発初期の特に、まずはとにかく動かすことが重要な段階では、開発速度を落とす要因となっていました。

このような背景があり、ボイラープレート自動生成ツールの導入を検討しました。

https://zenn.dev/aishift/articles/ce9783a0d7acd0#hygen(code-generator)

ボイラープレート自動生成ツールの導入

導入の背景は構成と背景で述べた通りですが、より具体的な狙いは以下のようなものでした。

開発工数の削減

  • Stubの開発を高速化し、フロントエンドの開発速度に影響を与えたくない
  • レイヤー分けをした際のコードの記述量を減らしたい
  • 認知負荷の削減
    • 誰が書いても同じような実装にする
      • メソッドの命名、I/Fの定義、エラーハンドリングなど

これらの狙いを実現するために、Hygenを導入しました。
https://www.hygen.io/

Hygenについて

Hygenは、テンプレートエンジンを使ってボイラープレートを自動生成するツールです。
具体的には、下記のようなejsコードからファイルを自動生成することが可能です。

sample.ejs
to: internal/usecase/<%= h.inflection.underscore(entity.toLowerCase()) %>.ts

----

export type I<%= h.inflection.Classify(entity) %>Interactor = {
  create(param: Create<%= h.inflection.classify(entity) %>): Promise<Result<<%= h.inflection.classify(entity) %>>>;
  get(param: Get<%= h.inflection.classify(entity) %>): Promise<Result<<%= h.inflection.classify(entity) %>>>;
  list(param: List<<%= h.inflection.classify(entity) %>>): Promise<Result<OList<<%= h.inflection.classify(entity) %>>>>;
  update(param: Update<%= h.inflection.classify(entity) %>): Promise<Result<<%= h.inflection.classify(entity) %>>>;
  delete(param: Delete<%= h.inflection.classify(entity) %>): Promise<Result<<%= h.inflection.classify(entity) %>>>;
}

export class <%= h.inflection.Classify(entity) %>Interactor implements I<%= h.inflection.Classify(entity) %>Interactor {
  private transactionManager: ITransactionManager;
  private <%= h.inflection.camelize(entity.toLowerCase(), true) %>Repository: I<%= h.inflection.classify(entity) %>Repository;

  constructor(transactionManager: ITransactionManager, <%= h.inflection.camelize(entity.toLowerCase(), true) %>Repository: I<%= h.inflection.classify(entity) %>Repository) {
    this.transactionManager = transactionManager;
    this.<%= h.inflection.camelize(entity.toLowerCase(), true) %>Repository = <%= h.inflection.camelize(entity.toLowerCase(), true) %>Repository;
  }

  async create(input: Create<<%= h.inflection.classify(entity) %>>): Promise<Result<<%= h.inflection.classify(entity) %>>> {
    return this.transactionManager.withTransaction(input.tenantId, async (ctx) => {
      // TODO: Add logic to create a new <%= h.inflection.classify(entity) %>
      const repoResult = await this.<%= h.inflection.camelize(entity.toLowerCase(), true) %>Repository.create(ctx, new <%= h.inflection.classify(entity) %>(...));
      return repoResult;
    });
  }

  //...
}

上記のようなejsファイルを元に、コマンドを実行することでファイルを生成できます。
alt text

  • 実行結果の一部
task.ts
export type ITaskInteractor = {
  create(param: CreateTask): Promise<Result<Task>>;
  get(param: GetTask): Promise<Result<Task>>;
  list(param: ListTasks): Promise<Result<OList<Task>>>;
  update(param: UpdateTask): Promise<Result<Task>>;
  delete(param: DeleteTask): Promise<Result<Task>>;
}

export class TaskInteractor implements ITaskInteractor {
  private transactionManager: ITransactionManager;
  private taskRepository: ITaskRepository;

  constructor(transactionManager: ITransactionManager, taskRepository: ITaskRepository) {
    this.transactionManager = transactionManager;
    this.taskRepository = taskRepository;
  }

  async create(input: CreateTask): Promise<Result<Task>> {
    return this.transactionManager.withTransaction(input.tenantId, async (ctx) => {
      // TODO: Add logic to create a new Task
      const repoResult = await this.taskRepository.create(ctx, new Task(...));
      return repoResult;
    });
  }

  // ...
}

CLIをカスタマイズすることでインタラクティブに生成結果を変更するように実装することも可能です。

https://www.hygen.io/docs/generators

導入後に見えた課題

開発初期から半年後~現在にかけてボイラープレート自動生成ツールを使用して感じた課題は以下の通りです。
alt text

開発初期は単純なCRUD0->1のAPI開発が多かったため、ボイラープレートの導入によって圧倒的に開発速度が向上しました。
しかし、開発が進むにつれて設計の見直しバグ修正が増え、部分的な修正が必要になりました。
この種の自動生成ツールでは部分的な更新が難しく、一度ファイルを削除して再生成する必要がある場合が多いです。無理やり実現する方法もありましたがツール自体の認知負荷や運用・保守性の観点から手動で修正することが多くなりました。

結果としてツール自体使われなくなり、削除することになりました。s
alt text

どうして削除したのか?

チーム内でツールの利用者がいなくなったことが主な理由ですが、より原因を分解すると以下のようになります。
alt text

  • ツールの存在を知らなかった
  • 存在は知っていたが、使い方が分からなかった
  • 使ってみたが使い勝手が悪く、やがて使わなくなった

特に新規のチームということもあり、利用手順書の準備が後回しになったり、頻繁な人の入れ替わりごとに十分に対応できていなかったことが大きな要因でした。

どうしたら良かったのか

使われなくなったとしても、削除せずに部分的に利用したり、他チームへの展開を図るなどして、使い方を広げることができれば良かったと思います。
今ならこういった動き方をするだろうと反省しています。

alt text

周知とフィードバックの収集

まずは使ってもらわなければ、その良し悪しを判断することもできません。
そのため、誰でも使い始めやすい環境を整備し、まずはツールを使ってもらうことが大切です。
その上で利用者のフィードバックを収集し、ツールの改善を続けることが重要だと感じています。また、導入の効果を計測できる形にすることによって他のチームや組織への展開もしやすくなると思います。

属人性の排除

中長期的に、ツールを使用し続けるためにはメンテナンスが必要不可欠です。
組織内で協力者を増やし生産性向上ための啓蒙活動を行うことができれば良かったと思います。とにかく一人で作業しないことが重要と感じています。

まとめ

最終的には利用シーンの変化や運用フローの形骸化により、ツールを削除するという判断に至りました。
しかしツール自体は非常に使いやすく、もしチーム内で導入を検討している場合にはこの記事が参考になれば幸いです。

AI Shift Tech Blog

Discussion