ソースコードを整理して開発を高速化する

ソースコードを整理して開発を高速化する

開発速度向上のため、機能を小さく分解し、意味のある名前をつけることを心がけることによってコードの可読性が上がり、動作保証されたコードの塊を作れます。結果として、テスト時間やソースコードを読む時間が減り開発が高速化します
2025.10.09

こんにちわ、リテールアプリ共創部のマッハチームの西田です

今回は筆者が開発速度の向上のために、日々自分が心がけていることをご紹介いたします

特に特殊なことではなく、昔から心がけてる基本的なことを紹介させていただきます。

現在では、70%から80%くらいはAIでコーディングしていますが、今でも変わらずに心がけています

心がけてること

  • 極力単機能になるまで分解する
  • 単純な機能を組み合わせ複雑な機能を作成する

個人的に、コードはキーボードを使ってぽちぽち書いてる時間よりも、読んでる時間の方が長くなりがちです。自分が書いたコードの確認、他の人の書いたコードのレビュー、不具合発生時の調査、改修の時のあたりをつける… etc。AIを使うようになって、この傾向はさらに強くなってるように感じます

また、コードを書く時間よりも、動作保証できるようになるまで、自動テストを作成したり、手動でUIを操作したり、curlを使ってAPIを動作確認してる時間の方が長くなることもしばしばあります

そのため、開発効率を上げるために、以下の点を重視して開発しています

  • 小さい意味のある機能に分離する
  • 動作保証されてるコードの塊を作る

小さい意味のある機能に分離する

人の短期的な記憶、ワーキングメモリーで記憶しておけるのは 3 〜 5個程度と言われています。そのため、覚えておかないといけないコンテキストが増えれば増えるほど、ワーキングメモリから溢れ、読み直しが発生したり、間違った情報で補完してしまったりします

ソースコードも複雑な処理がフラットに書かれていると、その後の処理を読み解く時に、コンテキストとして、それまで読んだコードを覚えておく必要が出てきます

例を挙げて説明します。例えば、以下のような処理を行うAPIがあるとします

  • 別サービスのAPIを使ってデータを取得する、ただしすでにデータベースにデータがあれば、キャッシュからデータを取得する。キャッシュの有効期限無いならそのままキャッシュを返却し、有効期限切れ、もしくは、キャッシュが存在しない場合は、APIからデータを取得しキャッシュとして保存する
  • ユーザーのステータスが退会済みであればエラーにする
  • ユーザーの購入情報を取得する(キャッシュ処理は同様)
  • 上記データを加工してAPIのレスポンスとして返す

これを一つの処理として書くと以下のような感じでしょうか

			
			async function getUserHandler(userId: string) {
  // キャッシュを確認
  const cached = await db.get(`user:${userId}`);
  
  let userData;
  
  if (cached) {
    if (cached.expiresIn < Date.now()) {
      await db.delete(`user:${userId}`);
      const response = await fetch(`https://api.example.com/users/${userId}`);
      userData = await response.json();
      
      await db.set(`user:${userId}`, { data: userData, expiresIn: Date.now() + 3600000 });
    } else {
      userData = cached.data;
    }	  
  } else {
    // 外部APIを呼び出し
    const response = await fetch(`https://api.example.com/users/${userId}`);
    userData = await response.json();
    
    // キャッシュに保存
    await db.set(`user:${userId}`, { data: userData, expiresIn: Date.now() + 3600000 });
  }
  
  if (userData.status == 'inactive') {
    throw new InvalidStatusError();
  }
 
  // 購買データを取得 
  const response = await fetch(`https://api.example.com/purchases/${userId}`);
  const purchaseData = await response.json();
  
  // データを加工してレスポンス用に整形
  return {
    id: userData.id,
    fullName: `${userData.firstName} ${userData.lastName}`,
    totalPurchases: purchaseData.total,
  };
}

		

処理が複雑になり、読み解くのに時間がかかりそうです

処理をサブルーチンに分けて、意味ある名前につけてみます

			
			async function fetchUserIfNeeded(userId: string) {
  // キャッシュを確認
  const cached = await db.get(`user:${userId}`);
  
  if (cached) {
    if (cached.expiresIn < Date.now()) {
      // 有効期限切れ
      await db.delete(`user:${userId}`);
      const response = await fetch(`https://api.example.com/users/${userId}`);
      const userData = await response.json();
      await db.set(`user:${userId}`, { data: userData, expiresIn: Date.now() + 3600000 });
      
      return userData;
    } else {
      // 有効期限内
      return cached.data;
    }	  
  } else {
    // キャッシュが存在しない
    const response = await fetch(`https://api.example.com/users/${userId}`);
    const userData = await response.json();
    
    // キャッシュに保存
    await db.set(`user:${userId}`, { data: userData, expiresIn: Date.now() + 3600000 });
    return userData;
  }	
}

async function fetchPurchases(userId: string) {
  const response = await fetch(`https://api.example.com/purchases/${userId}`);
  return await response.json();
}

function validateUserStatus(user) {
  if (user.status == 'inactive') {
    throw new InvalidStatusError();
  }  
}

async function getUserHandler(userId: string) {  
  const userData = await fetchUserIfNeeded(userId);
  
  validateUserStatus(userData);	
  
  const purchases = await fetchPurchases(userId);
  
  // データを加工してレスポンス用に整形
  return {
    id: userData.id,
    fullName: `${userData.firstName} ${userData.lastName}`,
    totalPurchases: purchases.total,
  };
}

		

こうすることで、 それぞれのサブルーチンはサブルーチンごとで短い処理を読んで機能を把握しやすくなります、メインのルーチン(この例で言う getUserHandler) は、全体の流れを俯瞰できるようになります

また、関数の名前を、その内容の処理がわかるよう命名しておくと、人が読んでも理解しやすく、AIのコンテキストを減らせる可能性も出てきます

動作保証されてるコードの塊を作る

前回のサンプルにあるキャッシュの機構は共通化できそうです。

この共通化したキャッシュを呼び出す機能を呼び出す側は、以下の関心(期待)を持って呼び出す考えられます

  • キャッシュがあればキャッシュからデータを返却する
  • キャッシュがなければ fetch し、DBにキャッシュする
  • 有効期限が切れればキャッシュを更新する

これらの関心(期待)は、他の fetch する処理でも同じような関心がありそうです

共通化の例としてこんな感じでしょうか

			
			interface WithCacheOptions<T> {
  db: any;
  key: string;
  fetcher: () => Promise<T>;
  ttl?: number;
}

export async function withCache<T>(options: WithCacheOptions<T>): Promise<T> {
  const { db, key, fetcher, ttl = 3600000 } = options;

  // キャッシュチェック
  const cached = await db.get(key);
  
  if (cached && cached.expiresAt > Date.now()) {
    return cached.data as T;
  }

  // 期限切れなら削除
  if (cached) {
    await db.delete(key);
  }

  // 新規取得
  const data = await fetcher();
  await db.set(key, {
    data,
    expiresAt: Date.now() + ttl,
  });

  return data;
}

		

呼び出し側もこの関数を使うようにコードを変更します。

			
			async function fetchUserIfNeeded(userId: string): Promise<User> {
  return withCache({
    db,
    key: `user:${userId}`,
    fetcher: async () => {
      const response = await fetch(`https://api.example.com/users/${userId}`);
      return response.json();
    },
    ttl: 3600000, // 1時間
  });
}

async function fetchPurchases(userId: string) {
  const response = await fetch(`https://api.example.com/purchases/${userId}`);
  return await response.json();
}

function validateUserStatus(user) {
  if (user.status == 'inactive') {
    throw new InvalidStatusError();
  }  
}

async function getUserHandler(userId: string) {  
  const userData = await fetchUserIfNeeded(userId);
  
	validateUserStatus(userData);	
  
  const purchases = await fetchPurchases(userId);
  
  // データを加工してレスポンス用に整形
  return {
    id: userData.id,
    fullName: `${userData.firstName} ${userData.lastName}`,
    totalPurchases: purchaseData.total,
  };
}

		

元々のソースから必要な情報を読み取りやすくなったのでは無いでしょうか

また、キャッシュの処理は、それ単体で自動テストを作成できます。呼び出し側の関心をそのままテストケースにできます。

  • キャッシュがあればキャッシュからデータを返却する
  • キャッシュがなければ fetch し、DBにキャッシュする
  • 有効期限が切れればキャッシュを更新する

そうしておくと、fetchした結果をキャッシュしておきたい他のケースでこの関数が再利用できます。キャッシュの有効期限の処理等は、この関数の単体テストで保証され、新しくテストを追加する必要がなくなります

こういった、動作保証されたコードの塊をいくつも作ることで、テストに使う時間を減らすことができ、結果、開発の高速化につながっていきます

最後に

今回は開発の高速化をテーマに、筆者が日頃心掛けてることを紹介させていただきました。開発効率がN倍といった派手な効果はないですが、こういったことの積み重ねで開発を高速化しています。この記事が誰かの役に立てば幸いです

この記事をシェアする

FacebookHatena blogX

関連記事

ソースコードを整理して開発を高速化する | DevelopersIO