2,064行の巨大ファイルを12分割した話。Expressサーバーのリファクタリング実録

こんにちは。C.Cです。

今回はDay 14(3月17日)の話です。この日の作業は「リファクタリング」。機能は一切変えずに、コードの構造だけを整理しました。地味ですが、プロジェクトの健康を保つためにとても大切な作業です。

2,064行の巨大ファイル

問題のファイルはserver/index.tsでした。Expressサーバーのメインファイルで、全てのAPI処理がここに書かれていました。

最初は291行でした。 Day 1の初回コミットの時点では、まだ小さなファイルです。

しかし、機能が増えるたびにこのファイルが太っていきました。

チャット機能を追加すると+100行。 タスク管理を追加すると+150行。 KPI収集を追加すると+200行。 X連携、会議室、成果物管理、note seeds...。

気づけば2,064行までに膨らんでいたのです。

2,064行のファイルがどれくらい大きいかというと、この記事が約200行です。

この記事を10本分つなげた量のコードが、1つのファイルに入っていたわけです。

なぜ大きいファイルが問題なのか

「動いているなら大きくても困らないのでは?」と思うかもしれません。実際、2,064行あっても正しく動いていました。

でも、いくつかの問題が出始めていました。

1つ目は、目的のコードを見つけにくいこと。

「チャット機能のバグを直したい」と思ったとき、2,064行の中からチャット関連のコードを探す必要があります。スクロールしながら「これは...タスク管理だな...これはKPIだな...あ、チャットはここだ」と目で追う。時間がかかりますし、見落としも起きやすい。

2つ目は、変更の影響範囲が見えにくいこと。

全ての機能が1ファイルに入っていると、「チャット機能を直したつもりがKPI機能も壊してしまった」ということが起こり得ます。

変数名が被っていたり、処理の順序に意図しない依存があったりするためです。

3つ目は、複数人(この場合はAI含む)で同時に作業しにくいこと。

1つのファイルを同時に編集すると、変更が衝突しやすくなります。

たとえるなら、1つの巨大な引き出しに全ての書類を入れている状態です。

確かに「書類は全部この引き出しにあります」とは言えますが、経理書類を探すのに営業書類をかき分けるのは非効率です。

分割の方針 -- ドメイン別に分ける

分割の方針は「ドメイン別」です。ドメインとは、ここでは「業務領域」という意味です。

チャットに関するコードはchat.tsに。 タスクに関するコードはtasks.tsに。 KPIに関するコードはkpi.tsに。

同じ業務に関するコードを、同じファイルにまとめる。

分割後のファイル構成はこうなりました。

server/
  index.ts          -- サーバーの起動と設定(大幅に縮小)
  routes/
    chat.ts         -- チャット機能
    tasks.ts        -- タスク管理
    kpi.ts          -- KPIモニタリング
    x.ts            -- X投稿関連
    meetings.ts     -- 会議室
    artifacts.ts    -- 成果物管理
    note-seeds.ts   -- note seeds記事ネタ管理
    line.ts         -- LINE Webhook
    diaries.ts      -- 日記
  ai-clients.ts     -- AI呼び出しの共通処理

2,064行の1ファイルが、12個のファイルになりました。

それぞれ100〜200行程度で、1つのファイルが1つの業務領域を担当します。

先ほどの引き出しのたとえで言えば、「経理用」「営業用」「人事用」とラベルを貼った引き出しを用意して、それぞれの書類を振り分けた状態です。

Express Router -- ファイル分割の技術的な仕組み

Expressには、Router(ルーター)という仕組みがあります。これを使うと、API処理をファイルごとに分離できます。

元のコードでは、全てのAPIをindex.tsに直接書いていました。

// index.ts に全部書いていた
app.post('/api/chat', (req, res) => { /* チャット処理 */ });
app.get('/api/tasks', (req, res) => { /* タスク一覧 */ });
app.post('/api/tasks', (req, res) => { /* タスク作成 */ });
app.get('/api/kpi/full', (req, res) => { /* KPI取得 */ });
// ... 2,064行分

分割後は、各ファイルでRouterを作り、index.tsに登録する形になります。

// routes/chat.ts
import { Router } from 'express';
const router = Router();
router.post('/api/chat', (req, res) => { /* チャット処理 */ });
export default router;

// routes/tasks.ts
import { Router } from 'express';
const router = Router();
router.get('/api/tasks', (req, res) => { /* タスク一覧 */ });
router.post('/api/tasks', (req, res) => { /* タスク作成 */ });
export default router;

// index.ts(大幅に縮小)
import chatRoutes from './routes/chat';
import taskRoutes from './routes/tasks';
app.use(chatRoutes);
app.use(taskRoutes);

各ファイルが自分の担当範囲だけを定義し、index.tsがそれを「登録」するだけ。

index.tsは交通整理役に徹し、実際の処理は各ルートファイルに任せます。

「機能を変えない」ことの重要性

リファクタリングで最も大事なルールは、「動作を変えない」ことです。

コードの配置場所は変えます。

ファイルを分割し、関数を移動し、importを書き換えます。

でも、外から見た動きは何も変わりません。同じURLにリクエストを送ったら、同じレスポンスが返ってくる。

これが守られないと、「整理しようとして壊してしまった」ということが起きます。

引っ越しでたとえるなら、荷物を別の部屋に移すのはOKですが、引っ越し中に荷物を捨てたり中身を入れ替えたりしてはいけません。

私がこのリファクタリングを行ったとき、コミットメッセージに「機能変更なし(純粋なリファクタリング)」と明記しました。

もし後でバグが見つかったとき、「このコミットは構造変更だけだから、バグの原因ではないはず」と判断できるようにするためです。

ai-clients.ts -- 共通処理の抽出

ファイル分割のついでに、もう1つの整理をしました。AI呼び出しの共通処理をai-clients.tsに抽出したことです。

チャット、会議室、自律思考ループ、記事生成。Office-Chronosの至るところでAIを呼び出します。

しかし、Geminiの呼び出し方、Claudeの呼び出し方、フォールバックの処理は全て共通です。

それまでは各ルートファイルが独自にAI呼び出しコードを書いていました。同じようなコードが複数箇所に散らばっている状態です。1箇所直しても別の箇所を直し忘れる、というリスクがありました。

ai-clients.tsに共通処理をまとめたことで、「AI呼び出し方法を変えたい」ときはこのファイル1つを修正すれば全体に反映されるようになりました。

振り返り

リファクタリングは、新機能の追加に比べて地味な作業です。ユーザーから見て何も変わりません。でも、開発者(とAI)から見ると、コードの見通しが劇的に改善します。

2,064行のファイルを12個に分割した結果、

  • 「チャットのバグを直したい」→ routes/chat.tsだけ見ればいい
  • 「AI呼び出し方法を変えたい」→ ai-clients.tsだけ修正すればいい
  • 「新機能を追加したい」→ 新しいルートファイルを作ればいい

コードの整理は、将来の自分(と将来のAI)への投資です。

次回は、Day 15。Geminiが止まり、Claudeを呼んだら認証エラーが返ってきた -- マルチプロバイダーAIとVertex AI全面移行の話をします。