【48日目】『プリンシプル オブ プログラミング』を意識してReact/Next.jsをリファクタリングしてみた
技術ブログ48日目。
本日は、React/Next.jsの読みやすさを追求します。
〇課題
商品在庫ダッシュボードの改善
在庫リストを表示し、カテゴリごとの税率や割引を計算する画面です。現状は1つの関数にすべての手順が混ざり、どこを直すとどこに影響が出るか分からない「泥団子」のような状態です。
〇修正前コード(動くけれど読みにくいコード)
// src/app/admin/inventory/page.tsx
"use client";
import { useState, useEffect } from "react";
export default function InventoryDashboard() {
const [list, setList] = useState<any[]>([]);
useEffect(() => {
// モックデータ取得
setList([
{ id: 1, name: "技術書", cat: "book", price: 3000, stock: 5 },
{ id: 2, name: "プロテイン", price: 5000, cat: "food", stock: 0 },
{ id: 3, name: "モニター", price: 35000, cat: "electronics", stock: 12 },
]);
}, []);
return (
<div className="p-10 max-w-4xl mx-auto">
<h1 className="text-2xl font-bold mb-6">在庫管理ダッシュボード</h1>
<div className="space-y-4">
{list.map((i) => {
// 直交性の欠如:税金、割引、表示ロジックが密結合している
let taxRate = 0.1;
if (i.cat === "food") taxRate = 0.08; // 軽減税率
let discount = 0;
if (i.price >= 10000) discount = 500; // 1万円以上で500円引き
// 自己文書化されていない計算式
const finalPrice = Math.floor((i.price - discount) * (1 + taxRate));
const isAlert = i.stock === 0;
return (
<div key={i.id} className="flex justify-between p-4 border rounded shadow-sm bg-white">
<div>
<h2 className="font-bold">{i.name}</h2>
<p className="text-sm text-gray-500">カテゴリ: {i.cat}</p>
</div>
<div className="text-right">
<p className="text-lg font-bold">
支払額: {finalPrice.toLocaleString()}円
<span className="text-xs font-normal text-gray-400 ml-1">(税抜: {i.price}円)</span>
</p>
{/* ステップダウン・ルールに反している:
高レベルな表示の中に、低レベルな条件分岐が混ざっている
*/}
<div className="mt-2">
{isAlert ? (
<span className="text-red-500 font-bold">【在庫切れ】</span>
) : (
<span className="text-green-600">在庫あり({i.stock})</span>
)}
</div>
</div>
</div>
);
})}
</div>
</div>
);
}
〇リファクタリングの指針
名著『プリンシプル オブ プログラミング』に基づき、以下の点を意識しました。
ステップダウン・ルール (Step-Down Rule)
コードを「新聞記事」のように構成します。まず高レベルな「概要」を読み、必要に応じて下の「詳細(計算ロジック等)」へ進めるように整えます。
自己文書化 (Self-Documentation)
コメントに頼らず、変数名や関数名(calculateFinalPrice など)そのもので意図を伝えます。
直交性 (Orthogonality)
「税金計算」と「割引計算」を独立させます。一箇所を変更しても他方に影響が出ない独立性を確保します。
〇修正後コード
"use client";
import { useState, useEffect, useMemo } from "react";
// 1. 定数・型定義(自己文書化:マジックナンバーを名前で説明)
const TAX_RATES = {
STANDARD: 0.1,
REDUCED: 0.08, // 軽減税率
} as const;
const DISCOUNT_RULES = {
THRESHOLD: 10000,
AMOUNT: 500,
} as const;
type Category = "book" | "food" | "electronics";
interface Product {
id: number;
name: string;
category: Category;
price: number;
stock: number;
}
// 2. 独立したドメインロジック(直交性:各計算を独立させ、組み合わせる)
const getTaxRate = (category: Category): number => {
return category === "food" ? TAX_RATES.REDUCED : TAX_RATES.STANDARD;
};
const getDiscountAmount = (price: number): number => {
return price >= DISCOUNT_RULES.THRESHOLD ? DISCOUNT_RULES.AMOUNT : 0;
};
const calculateFinalPrice = (price: number, category: Category): number => {
const discount = getDiscountAmount(price);
const taxRate = getTaxRate(category);
return Math.floor((price - discount) * (1 + taxRate));
};
// 3. Custom Hook(SRP:データの管理と高レベルな加工)
function useInventory() {
const [products, setProducts] = useState<Product[]>([]);
useEffect(() => {
// 実際はAPI等から取得
setProducts([
{ id: 1, name: "技術書", category: "book", price: 3000, stock: 5 },
{ id: 2, name: "プロテイン", category: "food", price: 5000, stock: 0 },
{ id: 3, name: "モニター", category: "electronics", price: 35000, stock: 12 },
]);
}, []);
return { products };
}
// 4. メインコンポーネント(ステップダウン・ルール:高レベルな概要から記述)
export default function InventoryDashboardPage() {
const { products } = useInventory();
return (
<div className="p-10 max-w-4xl mx-auto space-y-6 bg-gray-50 min-h-screen">
<header>
<h1 className="text-3xl font-bold text-gray-900">在庫管理ダッシュボード</h1>
</header>
<div className="grid gap-4">
{products.map((product) => (
<InventoryCard key={product.id} product={product} />
))}
</div>
</div>
);
}
// 5. サブコンポーネント(詳細な表示ロジックの隠蔽)
function InventoryCard({ product }: { product: Product }) {
const finalPrice = calculateFinalPrice(product.price, product.category);
return (
<div className="flex justify-between items-center p-6 border border-gray-200 rounded-xl shadow-sm bg-white hover:shadow-md transition">
<div className="space-y-1">
<h2 className="text-xl font-bold text-gray-800">{product.name}</h2>
<p className="text-sm text-gray-500 px-2 py-0.5 bg-gray-100 rounded-full inline-block">
{product.category}
</p>
</div>
<div className="text-right space-y-2">
<div>
<p className="text-2xl font-black text-blue-700">
{finalPrice.toLocaleString()}円
</p>
<p className="text-xs text-gray-400">
税抜単価: {product.price.toLocaleString()}円
</p>
</div>
<StockBadge stock={product.stock} />
</div>
</div>
);
}
function StockBadge({ stock }: { stock: number }) {
const isOutOfStock = stock === 0;
if (isOutOfStock) {
return <span className="text-sm font-bold text-red-500 bg-red-50 px-3 py-1 rounded-lg">【在庫切れ】</span>;
}
return (
<span className="text-sm font-medium text-green-600 bg-green-50 px-3 py-1 rounded-lg">
在庫あり({stock})
</span>
);
}
〇主な修正ポイント
ステップダウン・ルールの徹底: DashboardPage -> InventoryCard -> StockBadge という階層に整理しました。上から順に読むだけで、全体の構成から細部のロジックへと自然に理解が深まります。
直交性の確保: 「税率判定」と「割引計算」を完全に独立させました。例えば将来「割引ルール」が変わっても「税率計算」を壊す心配がなく、安心して修正が可能です。
自己文書化: 略語(cat, i)を廃止し、定数オブジェクト(TAX_RATES)を活用。コード自体が仕様書としての役割を果たすように整えました。
〇参照先
▼公式ドキュメント
▼書籍
プリンシプル オブ プログラミング 3年目までに身につけたい 一生役立つ101の原理原則
以上
Discussion