🐥

【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)を活用。コード自体が仕様書としての役割を果たすように整えました。

〇参照先
▼公式ドキュメント
https://react.dev/learn/keeping-components-pure

https://react.dev/learn/passing-props-to-a-component

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

以上

Discussion