Tidy First 第10章 明示的なパラメーター

に公開

リーダブルコードがジュニアエンジニア向けならば、Tidy Firstはミドルエンジニア以上向けであると思う。
この記事はそのTidy Firstの第10章の内容をわかりやすい文章で私なりにまとめました

明示的なパラメーターで読みやすいコードを書く

はじめに

既存のコードを読んでいると、「このデータはどこから来ているんだろう?」と疑問に思うことがありませんか?関数の奥深くで突然変数が登場したり、グローバルな状態に依存していたりすると、コードの理解が困難になり、バグの温床にもなりがちです。

このような問題は、特にチーム開発や長期間のプロジェクトにおいて深刻になります。コードレビューの際に「この変数はどこで定義されているの?」「この関数は何に依存しているの?」といった質問が頻発し、開発効率が大幅に低下することもあります。

今回は、そんな問題を根本的に解決する「明示的なパラメーター」について、実際のコード例と詳細な解説を交えながら説明していきます。

問題:暗黙的なデータの使用が引き起こす課題

よくある問題のパターン

実際の開発現場で遭遇する典型的な問題を見てみましょう。以下のような状況に心当たりはありませんか?

1. 関数内で突然データが登場する
関数を読んでいると、引数として渡されていないデータが突然使われている。そのデータがどこから来ているのか、関数のシグネチャからは全く分からない。

2. グローバル変数への暗黙的な依存
関数が内部でグローバル変数や環境変数を参照しているが、それが外部から見えない。関数を別の場所で再利用しようとすると、思わぬエラーが発生する。

3. 複雑なオブジェクトの受け渡し
大きなオブジェクトを丸ごと渡して、その中の一部だけを使用している。どのプロパティが実際に必要なのかが不明確で、予期しない副作用のリスクもある。

これらの問題が開発に与える影響

可読性の低下
コードを読む人は、データの流れを追うために複数のファイルを行き来する必要があります。特に新しいチームメンバーにとっては、コードの理解に多大な時間を要することになります。

テストの困難さ
関数が外部の状態に依存していると、単体テストを書く際にその状態を再現する必要があります。これにより、テストのセットアップが複雑になり、テストの実行も不安定になりがちです。

バグの増加
データの流れが不明確だと、予期しない箇所でデータが変更され、デバッグが困難なバグが発生する可能性が高まります。

解決策:ルーチンの分割と明示的なパラメーター

基本的な考え方

明示的なパラメーターのアプローチは、以下の原則に基づいています:

1. データの流れを可視化する
関数が必要とするデータを、その関数のシグネチャから明確に読み取れるようにします。

2. 依存関係を明確にする
関数が何に依存しているかを、外部から容易に把握できるようにします。

3. 副作用を制限する
関数が外部の状態を予期せず変更することを防ぎます。

実装手順

問題を解決するために、以下の手順でコードを段階的に改善していきます:

ステップ1: 暗黙的な依存関係を特定する
まず、関数が暗黙的に使用しているデータを洗い出します。

ステップ2: ルーチンを分割する
元の関数を、パラメーター準備部分と実際の処理部分に分割します。

ステップ3: パラメーターを明示的に渡す
必要なデータを明示的に引数として渡すように修正します。

実践例1:マップパラメーターの改善

問題のあるコードの詳細分析

まず、よくある問題のあるパターンを見てみましょう:

// 問題のあるコード例
const userData = { 
  name: "田中太郎", 
  age: 30, 
  email: "tanaka@example.com",
  department: "開発部",
  salary: 500000,
  permissions: ["read", "write"]
};

processUser(userData);

function processUser(userData) {
  // この関数内で突然 name と age が使われている
  console.log(`ユーザー名: ${userData.name}`);
  
  // さらに深い階層でemailが使われる
  sendWelcomeEmail(userData);
  
  // permissionsも別の場所で使われる
  checkPermissions(userData);
  
  // 最悪の場合、userDataが変更される可能性も
  userData.lastLoginDate = new Date();
}

function sendWelcomeEmail(userData) {
  // ここでemailが必要だが、関数のシグネチャからは分からない
  emailService.send(userData.email, "Welcome!");
}

function checkPermissions(userData) {
  // permissionsが必要だが、これも外部からは不明
  return userData.permissions.includes("write");
}

このコードの問題点:

  1. 不透明な依存関係: processUser関数がuserDataのどのプロパティを使用するかが不明
  2. テストの困難さ: 各関数をテストするために、常に完全なuserDataオブジェクトを準備する必要がある
  3. 副作用のリスク: userDataオブジェクトが関数内で変更される可能性がある
  4. 再利用性の低さ: 他の文脈で関数を使う際に、不要なデータまで準備する必要がある

改善されたコードの詳細解説

同じ機能を明示的なパラメーターで実装してみましょう:

// 改善されたコード例
const userData = { 
  name: "田中太郎", 
  age: 30, 
  email: "tanaka@example.com",
  department: "開発部",
  salary: 500000,
  permissions: ["read", "write"]
};

// メイン処理:必要なデータを明示的に取り出す
processUser(userData);

function processUser(userData) {
  // 上部で必要なパラメーターを明示的に収集
  const name = userData.name;
  const age = userData.age;
  const email = userData.email;
  const permissions = userData.permissions;
  
  // 実際の処理に明示的にパラメーターを渡す
  processUserCore(name, age, email, permissions);
}

function processUserCore(name, age, email, permissions) {
  // 何が必要かが一目瞭然
  console.log(`ユーザー名: ${name}`);
  
  // 各関数に必要なデータだけを渡す
  sendWelcomeEmail(email);
  const hasWritePermission = checkPermissions(permissions);
  
  console.log(`書き込み権限: ${hasWritePermission ? 'あり' : 'なし'}`);
}

function sendWelcomeEmail(email) {
  // emailだけが必要であることが明確
  // この関数は他の文脈でも簡単に再利用できる
  emailService.send(email, "Welcome!");
}

function checkPermissions(permissions) {
  // permissionsだけが必要であることが明確
  // 副作用の心配がない(読み取り専用の操作)
  return permissions.includes("write");
}

改善されたコードのメリット:

  1. 明確な依存関係: 各関数のシグネチャを見るだけで、何が必要かが分かる
  2. 簡単なテスト: 各関数を個別にテストする際に、最小限のデータだけを準備すればよい
  3. 副作用の防止: 元のオブジェクトが変更される心配がない
  4. 高い再利用性: 各関数を他の文脈で簡単に使い回せる

さらなる改善:構造化によるパラメーター管理

引数が多くなりすぎる場合は、関連するパラメーターをグループ化することで管理しやすくできます:

// パラメーターをグループ化した例
function processUser(userData) {
  // 関連するデータをグループ化
  const personalInfo = {
    name: userData.name,
    age: userData.age
  };
  
  const contactInfo = {
    email: userData.email
  };
  
  const securityInfo = {
    permissions: userData.permissions
  };
  
  // グループ化されたデータを明示的に渡す
  processUserAdvanced(personalInfo, contactInfo, securityInfo);
}

function processUserAdvanced(personalInfo, contactInfo, securityInfo) {
  // 各グループの役割が明確
  displayPersonalInfo(personalInfo);
  sendWelcomeEmail(contactInfo.email);
  checkPermissions(securityInfo.permissions);
}

function displayPersonalInfo({name, age}) {
  // 分割代入により、必要なプロパティを明示的に取得
  console.log(`ユーザー名: ${name}, 年齢: ${age}`);
}

この方法により、パラメーターの数を管理しつつ、データの役割を明確に分離できます。

実践例2:環境変数の明示的な受け渡し

問題のあるパターンの詳細分析

環境変数が深い階層で突然使われるパターンは、特にサーバーサイドアプリケーションでよく見られます:

// 問題のあるコード例
function startApplication() {
  console.log("アプリケーションを開始します");
  initializeServices();
}

function initializeServices() {
  console.log("サービスを初期化中...");
  setupDatabase();
  setupExternalAPI();
}

function setupDatabase() {
  // 突然環境変数が登場!どこで定義されているかわからない
  const dbUrl = process.env.DATABASE_URL;
  const dbPassword = process.env.DB_PASSWORD;
  
  if (!dbUrl) {
    throw new Error("DATABASE_URL is not set");
  }
  
  // データベース接続の処理...
  console.log(`データベースに接続: ${dbUrl}`);
}

function setupExternalAPI() {
  // ここでも突然環境変数が登場
  const apiKey = process.env.API_KEY;
  const apiBaseUrl = process.env.API_BASE_URL;
  
  if (!apiKey) {
    throw new Error("API_KEY is not set");
  }
  
  // API設定の処理...
  console.log(`外部API設定完了: ${apiBaseUrl}`);
}

このコードの問題点:

  1. 隠れた依存関係: 各関数が環境変数に依存していることが外部から見えない
  2. テストの困難さ: 環境変数を設定しないとテストできない
  3. エラーハンドリングの分散: 環境変数のチェックが各所に散らばっている
  4. 設定の把握困難: アプリケーション全体でどの環境変数が必要かが分からない

改善されたコードの詳細解説

同じ機能を明示的なパラメーターで実装し直してみましょう:

// 改善されたコード例
function startApplication() {
  console.log("アプリケーションを開始します");
  
  // 上位レベルで必要な設定をすべて収集・検証
  const config = loadAndValidateConfiguration();
  
  // 設定を明示的に渡してサービスを初期化
  initializeServices(config);
}

function loadAndValidateConfiguration() {
  // 必要な環境変数を一箇所で管理
  const requiredEnvVars = {
    databaseUrl: process.env.DATABASE_URL,
    dbPassword: process.env.DB_PASSWORD,
    apiKey: process.env.API_KEY,
    apiBaseUrl: process.env.API_BASE_URL
  };
  
  // 一箇所でまとめて検証
  const missingVars = [];
  for (const [key, value] of Object.entries(requiredEnvVars)) {
    if (!value) {
      missingVars.push(key);
    }
  }
  
  if (missingVars.length > 0) {
    throw new Error(`以下の環境変数が設定されていません: ${missingVars.join(', ')}`);
  }
  
  // 設定オブジェクトとして返す
  return {
    database: {
      url: requiredEnvVars.databaseUrl,
      password: requiredEnvVars.dbPassword
    },
    api: {
      key: requiredEnvVars.apiKey,
      baseUrl: requiredEnvVars.apiBaseUrl
    }
  };
}

function initializeServices(config) {
  console.log("サービスを初期化中...");
  
  // 各サービスに必要な設定だけを明示的に渡す
  setupDatabase(config.database);
  setupExternalAPI(config.api);
}

function setupDatabase(databaseConfig) {
  // 必要な設定が明示的に渡されている
  const { url, password } = databaseConfig;
  
  // データベース接続の処理...
  console.log(`データベースに接続: ${url}`);
  
  // この関数は設定オブジェクトを受け取るだけなので、
  // 環境変数に依存せずにテストできる
}

function setupExternalAPI(apiConfig) {
  // 必要な設定が明示的に渡されている
  const { key, baseUrl } = apiConfig;
  
  // API設定の処理...
  console.log(`外部API設定完了: ${baseUrl}`);
  
  // この関数も環境変数に依存しないため、テストが簡単
}

改善されたコードのメリット:

  1. 設定の一元管理: すべての環境変数が一箇所で管理され、必要な設定が一目瞭然
  2. 早期エラー検出: アプリケーション開始時に設定の問題をすべて検出
  3. テストの容易性: 各関数に設定オブジェクトを渡すだけでテストできる
  4. 明確な依存関係: 各関数が何の設定を必要とするかが明確

テスト例で違いを実感する

改善前後のコードがテストにどのような影響を与えるかを見てみましょう:

// 問題のあるコードのテスト(困難)
describe('setupDatabase (改善前)', () => {
  it('データベースの設定ができること', () => {
    // 環境変数を設定する必要がある
    process.env.DATABASE_URL = 'test-db-url';
    process.env.DB_PASSWORD = 'test-password';
    
    // テスト実行
    setupDatabase();
    
    // 環境変数のクリーンアップが必要
    delete process.env.DATABASE_URL;
    delete process.env.DB_PASSWORD;
  });
});

// 改善されたコードのテスト(簡単)
describe('setupDatabase (改善後)', () => {
  it('データベースの設定ができること', () => {
    // 必要なデータだけを準備
    const databaseConfig = {
      url: 'test-db-url',
      password: 'test-password'
    };
    
    // テスト実行(環境変数に依存しない)
    setupDatabase(databaseConfig);
    
    // クリーンアップ不要
  });
  
  it('異なる設定でもテストできる', () => {
    const anotherConfig = {
      url: 'another-db-url',
      password: 'another-password'
    };
    
    setupDatabase(anotherConfig);
  });
});

このように、明示的なパラメーターにすることで、テストが格段に書きやすくなります。

明示的なパラメーターのメリット(詳細解説)

1. 可読性の向上(具体例付き)

改善前の関数:

// 関数のシグネチャからは何が必要かわからない
function calculateTax(order) {
  // ...内部で order.amount, order.customerType, order.region を使用
}

改善後の関数:

// 必要なデータが一目瞭然
function calculateTax(amount, customerType, region) {
  // 必要な情報が明確で、コードレビューも簡単
}

関数のシグネチャを見るだけで、税額計算に必要な情報(金額、顧客タイプ、地域)が明確になります。新しいチームメンバーでも、関数の責任範囲を即座に理解できます。

2. テストの容易性(比較例)

改善前のテスト準備:

// 複雑なオブジェクト全体を準備する必要
const testOrder = {
  id: 1,
  amount: 1000,
  customerType: 'premium',
  region: 'tokyo',
  items: [/* 大量のテストデータ */],
  shipping: { /* 配送情報 */ },
  payment: { /* 支払い情報 */ }
  // ...さらに多くのプロパティ
};

改善後のテスト準備:

// 必要最小限のデータだけを準備
const amount = 1000;
const customerType = 'premium';
const region = 'tokyo';

テストの準備時間が大幅に短縮され、テストの意図も明確になります。

3. デバッグの効率化(実践例)

改善前のデバッグ:
エラーが発生した場合、複数のファイルにまたがってデータの流れを追跡する必要があります。

// エラー発生箇所
function processPayment(order) {
  // order.paymentMethod が undefined でエラー
  const result = paymentGateway.process(order.paymentMethod);
}

このエラーを解決するために、order オブジェクトがどこで作成され、どこで paymentMethod が設定されるべきなのかを調べる必要があります。

改善後のデバッグ:

// エラー発生箇所
function processPayment(paymentMethod) {
  // paymentMethod が undefined でエラー
  const result = paymentGateway.process(paymentMethod);
}

この場合、processPaymentを呼び出している箇所を確認するだけで、問題の原因を特定できます。

4. 再利用性の向上(具体例)

改善前:

// 特定のユーザーオブジェクトに依存
function generateUserReport(user) {
  return `Name: ${user.name}, Department: ${user.department}`;
}

// 使用場面が限定される
const report1 = generateUserReport(user); // userオブジェクト全体が必要

改善後:

// 汎用的に使える
function generateUserReport(name, department) {
  return `Name: ${name}, Department: ${department}`;
}

// 様々な場面で使用可能
const report1 = generateUserReport(user.name, user.department);
const report2 = generateUserReport("田中", "営業部"); // 直接値でも使用可能
const report3 = generateUserReport(employee.fullName, employee.dept); // 異なる構造でも使用可能

実装時の注意点とベストプラクティス

パラメーターが多すぎる場合の対処法

引数が5個を超える場合は、以下の戦略を検討してください:

1. 関連するパラメーターのグループ化

// Before: 引数が多すぎる
function createUser(firstName, lastName, email, phone, street, city, postalCode, country) {
  // ...
}

// After: 論理的にグループ化
function createUser(personalInfo, contactInfo, address) {
  const { firstName, lastName } = personalInfo;
  const { email, phone } = contactInfo;
  const { street, city, postalCode, country } = address;
  // ...
}

2. 設定オブジェクトパターンの使用

// オプション引数が多い場合
function apiRequest({
  url,
  method = 'GET',
  headers = {},
  timeout = 5000,
  retries = 3
}) {
  // デフォルト値を設定しつつ、必要な引数を明示
}

パフォーマンスへの配慮

明示的なパラメーターが性能に与える影響について:

一般的な影響:

  • 引数の受け渡しコストは通常無視できるレベル
  • オブジェクトの分解コストも現代のJavaScriptエンジンでは最適化されている
  • 可読性とメンテナンス性の向上による開発効率の向上が、軽微な性能低下を大幅に上回る

高頻度実行関数での注意点:

// 高頻度で呼ばれる場合は、パラメーター分解を最小限に
function highFrequencyFunction(config) {
  // 必要な部分だけを一度に取り出す
  const { x, y, z } = config;
  
  // 複数回の分解を避ける
  for (let i = 0; i < 1000000; i++) {
    // config.x, config.y を毎回アクセスするより効率的
    process(x, y, z);
  }
}

段階的な改善アプローチ

大規模なコードベースで明示的なパラメーターを導入する際の戦略:

Phase 1: 新しいコードから始める

  • 新機能やリファクタリング対象のコードから適用
  • チーム内でのコンセンサスを形成

Phase 2: 問題の多い箇所を優先的に改善

  • バグが頻発する関数
  • テストが困難な関数
  • 可読性の低い関数

Phase 3: 段階的な全体適用

  • リスクの低い関数から順次適用
  • 十分なテストカバレッジを確保してから実施

まとめ

明示的なパラメーターは、単なるコーディングテクニックではなく、ソフトウェア開発における重要な設計原則です。この手法を適用することで得られる利益は多岐にわたります:

短期的な利益:

  • コードレビューの効率化
  • バグの早期発見
  • テスト作成時間の短縮

長期的な利益:

  • システムの保守性向上
  • 新メンバーのオンボーディング時間短縮
  • 技術的負債の蓄積防止

チーム全体への影響:

  • コード品質の標準化
  • 開発効率の向上
  • プロジェクトの成功確率向上

明日からのコード作成やレビューにおいて、「このデータはどこから来ているのか?」「この関数は何に依存しているのか?」という視点を意識してみてください。一見すると手間が増えるように感じるかもしれませんが、その投資はチーム全体の生産性向上という形で必ず返ってきます。

まずは小さな関数から始めて、徐々にこのアプローチに慣れていくことをお勧めします。きっと、より保守しやすく、理解しやすいコードを書けるようになるはずです。

Discussion