🚉

【35日目】『プリンシプル オブ プログラミング』を意識してTypescriptをリファクタリングしてみた

に公開

技術ブログ35日目。
本日は、Typescriptの読みやすさを追求します。

〇課題 IoTデバイス・テレメトリ解析システム
IoTデバイスから送られてくる生データ(ケルビン単位の温度、湿度、バッテリー残量)を解析し、異常がある場合にメンテナンス用の警告を作成する関数です。 現在のコードは、物理単位の計算(低レベル)と、ビジネス上の判定ルール(高レベル)が混ざり合っており、保守性が低い状態です。

〇修正前コード(動くけれど読みにくいコード)

// 開発環境: Vanilla TypeScript
function checkDevs(d: any[]): any {
  let r = [];
  
  if (d && d.length > 0) {
    for (let i = 0; i < d.length; i++) {
      let s = d[i];
      
      // 温度(ケルビンから摂氏へ変換して判定)
      let c = s.t - 273.15;
      
      // 湿度の判定
      if (s.h > 80 || c > 40 || c < 0) {
        let msg = "";
        if (c > 40) msg = "高温異常";
        else if (c < 0) msg = "凍結注意";
        else if (s.h > 80) msg = "多湿異常";
        
        r.push({
          id: s.id,
          alert: msg,
          val: c.toFixed(1) + "C"
        });
      }
      
      // バッテリーチェック
      if (s.b !== undefined && s.b < 15) {
        r.push({
          id: s.id,
          alert: "バッテリー低下",
          val: s.b + "%"
        });
      }
    }
  }
  
  return r;
}

〇リファクタリングの指針
名著『プリンシプル オブ プログラミング』に基づき、以下の点を意識しました。
SLAP (Single Level of Abstraction Principle)
「ケルビンから摂氏への変換(低レベルな計算)」と「メンテナンス要否の判定(高レベルな判断)」を分離します。関数の抽象度を一段に揃えることで、コードの意図を明確にします。

自己文書化 (Self-Documenting)
s.t や s.h といった略称、273.15 といったマジックナンバーを排除します。名前を見ただけで、コメントがなくても動作が理解できる状態を目指します。

関心の分離 (Separation of Concerns)
「データの変換」「異常の判定」「レポートの生成」をそれぞれ別の関数に分離し、将来の仕様変更に強い構成にします。

〇修正後コード

//  1. 型定義:ドメインの自己文書化 
interface TelemetryReading {
  readonly id: string;
  readonly temperatureKelvin: number;
  readonly humidityPercentage: number;
  readonly batteryLevel?: number;
}

interface MaintenanceAlert {
  readonly id: string;
  readonly message: string;
  readonly displayValue: string;
}

//  2. 定数:マジックナンバーの排除 
const KELVIN_TO_CELSIUS_OFFSET = 273.15;
const THRESHOLDS = {
  HIGH_TEMP_CELSIUS: 40,
  FREEZING_TEMP_CELSIUS: 0,
  HIGH_HUMIDITY_PERCENTAGE: 80,
  LOW_BATTERY_PERCENTAGE: 15,
} as const;

//  3. メイン処理:SLAP(抽象化レベルの統一)の適用 
function analyzeTelemetry(readings: TelemetryReading[] | null | undefined): MaintenanceAlert[] {
  if (!readings || readings.length === 0) return [];

  return readings.flatMap((reading) => {
    const alerts: MaintenanceAlert[] = [];
    
    // 低レベルな計算(変換)
    const temperatureCelsius = convertKelvinToCelsius(reading.temperatureKelvin);
    
    // 高レベルな判定とアラート生成の実行
    const statusAlert = checkEnvironmentalStatus(reading.id, temperatureCelsius, reading.humidityPercentage);
    if (statusAlert) alerts.push(statusAlert);

    const batteryAlert = checkBatteryStatus(reading.id, reading.batteryLevel);
    if (batteryAlert) alerts.push(batteryAlert);

    return alerts;
  });
}

//  4. サブ処理:関心の分離(各関数のタスクを1つに絞る) 

/** 物理単位の変換(低レベル抽象) */
function convertKelvinToCelsius(kelvin: number): number {
  return kelvin - KELVIN_TO_CELSIUS_OFFSET;
}

/** 環境状態の判定(高レベル抽象) */
function checkEnvironmentalStatus(id: string, tempC: number, humidity: number): MaintenanceAlert | null {
  if (tempC > THRESHOLDS.HIGH_TEMP_CELSIUS) {
    return { id, message: "高温異常", displayValue: `${tempC.toFixed(1)}°C` };
  }
  if (tempC < THRESHOLDS.FREEZING_TEMP_CELSIUS) {
    return { id, message: "凍結注意", displayValue: `${tempC.toFixed(1)}°C` };
  }
  if (humidity > THRESHOLDS.HIGH_HUMIDITY_PERCENTAGE) {
    return { id, message: "多湿異常", displayValue: `${humidity}%` };
  }
  return null;
}

/** バッテリー状態の判定(高レベル抽象) */
function checkBatteryStatus(id: string, batteryLevel?: number): MaintenanceAlert | null {
  if (batteryLevel !== undefined && batteryLevel < THRESHOLDS.LOW_BATTERY_PERCENTAGE) {
    return { id, message: "バッテリー低下", displayValue: `${batteryLevel}%` };
  }
  return null;
}

〇主な修正ポイント

  1. SLAP原則:一段ずつ階段を登る設計
    修正前は checkDevs 関数の中に、273.15 を引くという「数学的な計算」と、警告文を作るという「ビジネス判断」が混ざっていたため、メインの analyzeTelemetry 関数は、他の関数を呼び出す「目次」のような役割に修正しました。

  2. 自己文書化:名前がコードの意図を語る
    s.t や c といった名前を、temperatureKelvin や temperatureCelsius に変更しました。これにより、変数名を見るだけで「どの単位の数値か」がわかります。

  3. 関心の分離:
    温度、湿度、バッテリーという異なる「関心事」を個別の判定関数に分離しました。これにより、将来的に「特定のセンサーの判定基準だけを変更したい」といったニーズに、他のロジックを壊すことなく柔軟に応えられるようになります。

  4. 型による防御:信頼性の高いシステムへ
    any を廃止し、interface と readonly を導入しました。センサーデータが処理の途中で予期せず書き換えられるリスクを型レベルで排除しています。

〇参照先
▼公式ドキュメント
https://www.typescriptlang.org/docs/handbook/2/objects.html#readonly-properties

https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions

https://www.typescriptlang.org/docs/handbook/2/everyday-types.html

▼書籍
プリンシプル オブ プログラミング 3年目までに身につけたい 一生役立つ101の原理原則

以上

Discussion