【11日目】『リーダブルコード』を意識してTypescriptをリファクタリングしてみた
技術ブログ11日目。
本日は、Typescriptの読みやすさを追求します。
開発環境:Vanilla TypeScript
〇課題
健康診断データの判定・レポート出力システム
ウェアラブルデバイスから届く「心拍数」や「歩数」を解析する関数です。 今のコードは判定ロジックが絡み合い、「新しい項目(血圧や睡眠など)を追加するのが非常に困難」な設計になっています。
〇修正前コード(動くけれど読みにくいコード)
// 開発環境: Vanilla TypeScript
function analyze(d: any): any {
let r = { status: "unknown", advice: "" };
if (d !== null && d !== undefined) {
// 心拍数(hr)のチェック
if (d.hr !== undefined && d.hr !== null) {
if (d.hr > 100) {
if (d.age > 60) {
r.advice = "安静にしてください。医師に相談が必要かもしれません。";
r.status = "alert";
} else {
r.advice = "少し心拍数が高いです。休憩しましょう。";
r.status = "warning";
}
} else if (d.hr < 40) {
r.advice = "心拍数が低すぎます。";
r.status = "alert";
} else {
// 正常範囲
if (r.status !== "alert" && r.status !== "warning") {
r.status = "normal";
r.advice = "健康状態は良好です。";
}
}
}
// 歩数(s)のチェック(1日の目標10,000歩)
if (d.s !== undefined && d.s !== null) {
if (d.s < 3000) {
r.advice += " もっと歩きましょう。";
} else if (d.s >= 10000) {
r.advice += " 目標達成です!素晴らしい!";
}
}
// 名前の出力
r.advice = d.n + "さんの診断結果: " + r.advice;
} else {
r.advice = "データがありません";
}
return r;
}
〇リファクタリングの指針
型(Interface)によるドメインモデルの作成
anyを廃止し、HealthDataインターフェースを定義してデータの形状を明確にします。
関心の分離(関数の抽出)
心拍数と歩数の判定をそれぞれ独立した関数に切り出し、役割を分担させます。
Union型の活用
ステータスを文字列リテラルの型で定義し、タイポを防ぐとともに自動補完を有効にします。
テンプレートリテラルによる構築
文字列結合の視認性を高め、直感的な文章作成を可能にします。
状態管理のロジック化 複数の警告が重なった際の優先順位を判定する専用ロジックを導入します。
〇修正後コード
// 型定義
type HealthStatus = "normal" | "warning" | "alert";
interface HealthData {
name: string;
age: number;
heartRate?: number; // 任意項目(データがない場合もある)
steps?: number;
}
interface AnalysisResult {
status: HealthStatus;
advice: string;
}
// メイン処理
function analyzeHealthData(data: HealthData | null | undefined): AnalysisResult {
// ガード節:データ不在を即座に排除
if (!data) {
return { status: "normal", advice: "データがありません" };
}
// 各項目の判定結果を格納
const results: string[] = [];
let overallStatus: HealthStatus = "normal";
// 心拍数の判定ロジックを呼び出し
if (data.heartRate !== undefined) {
const hrResult = evaluateHeartRate(data.heartRate, data.age);
overallStatus = updateStatus(overallStatus, hrResult.status);
results.push(hrResult.advice);
}
// 歩数の判定ロジックを呼び出し
if (data.steps !== undefined) {
results.push(evaluateSteps(data.steps));
}
// アドバイスの構築(テンプレートリテラル)
const finalAdvice = results.length > 0 ? results.join(" ") : "健康状態は良好です。";
return {
status: overallStatus,
advice: `${data.name}さんの診断結果: ${finalAdvice}`,
};
}
// サブ処理:各専門の判定ロジック
/** 心拍数の評価 */
function evaluateHeartRate(heartRate: number, age: number): { status: HealthStatus; advice: string } {
if (heartRate > 100) {
return age > 60
? { status: "alert", advice: "安静にしてください。医師に相談が必要かもしれません。" }
: { status: "warning", advice: "少し心拍数が高いです。休憩しましょう。" };
}
if (heartRate < 40) {
return { status: "alert", advice: "心拍数が低すぎます。" };
}
return { status: "normal", advice: "心拍数は正常です。" };
}
/** 歩数のアドバイス */
function evaluateSteps(steps: number): string {
if (steps < 3000) return "もっと歩きましょう。";
if (steps >= 10000) return "目標達成です!素晴らしい!";
return "適度な運動ができています。";
}
/** ステータスの優先度更新(alert > warning > normal) */
function updateStatus(current: HealthStatus, next: HealthStatus): HealthStatus {
const priority: Record<HealthStatus, number> = { normal: 0, warning: 1, alert: 2 };
return priority[next] > priority[current] ? next : current;
}
〇主な修正ポイント
-
関心の分離とメンテナンス性の向上
判定項目を関数として切り出したことで、1つの関数の役割が小さくなりました。これにより、将来的に血圧や体温といった項目を追加する際も、既存のロジックに影響を与えずに関数を1つ追加するだけで対応が可能です。 -
ステータス更新のルール化
updateStatus関数を導入し、優先度を数値で管理するようにしました。これにより、複数の判定結果が混在する場合でも、最も重大なステータスを確実に返却できる仕組みになっています。 -
自己解説的なコード
d.hrやd.sといった短縮名を廃止し、heartRateやstepsといった意味のある名前に置き換えました。リーダブルコードの核心である、読み手が理解するまでの時間を最小化することを意識しています。 -
堅牢な文字列構築
アドバイスを一度配列に格納してからjoin(" ")で結合する手法を採用しました。これにより、不要な空白や句読点のミスを防ぎ、将来的にメッセージが増えても破綻しにくい構成になっています。
〇参照先
▼公式ドキュメント
▼書籍
リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック
以上
Discussion