はじめに
日々開発を行うにあたり、リファクタリングを意識しているものの、考え方や進め方に悩むことが多々あり、一旦考えを整理したいと思いメモを残してみようと思います。
結論
リファクタリングの規模の切り口でやることを以下のように整理
- 小規模(メソッド関数レベル)
- 中規模(クラス/コンポーネントレベル)
- 大規模(アーキテクチャレベル)
また、大規模なアーキテクチャレベルのリファクタリングは、アプリケーションの規模感に応じて段階的に以下のようなフェーズがある
- プロダクト立ち上げ直後
- 小〜中規模アプリケーション(成長期)
- 中〜大規模アプリケーション(拡大期)
これらのスコープの範囲、アプリの成長段階に応じて適切なリファクタリングの手法を選択することが重要
小規模(メソッド/関数レベル)
目的
可読性を向上させる: 読み手にコードの意図が伝わりやすくする
メンテナンス性を向上させる: 処理の簡素化、重複の削除
着眼点
- 意図がわかりやすい変数名や記述
- ネストを減らす
- 処理を簡潔にする
- マジックナンバーの削除
- など
コード例
例1: 意図がわかりやすい変数名と定数化
const a
とか、もっての外。
多少変数名が長くなっても読めば意図が伝わる様にしたい。
定数も直で記述すると何を意図しているのかわからないので、
行数がたとえ増えたとしても変数でおいた方が読み手にとってはわかりやすい。
// Before
function calculate(a, b) {
return a + 0.8 + b * 1.2;
}
// After
function calculateOrderCost(productPrice, shippingCost) {
const discountRate = 0.8;
const shippingMultiplier = 1.2;
return productPrice * discountRate + shippingCost * shippingMultiplier;
}
例2: ネストを減らす
if文のネスト
恐ろしい。
結局、このブロックに入る条件ってなに?が分かりにくくなるので、
早期リターンなどを使ってネストを減らした方が追いやすいコードに。
// Before
function getUserStatus(user) {
if (user) {
if (user.isActive) {
return "Active";
} else {
return "Inactive";
}
} else {
return "Unknown";
}
}
// After
function getUserStatus(user) {
if (!user) return "Unknown";
return user.isActive ? "Active" : "Inactive";
}
他にも着眼点はたくさんありますが、一例としてこれらを挙げます。
リーダブルコード読むと引き出し増えます。
中規模(クラス/コンポーネントレベル)
目的
- 再利用可能なコンポーネントを作成
- 抽象化により重複を減らす・変更に強くする
- 責務を明確化する
着眼点
- 表示コンポーネントとロジックの分離
- 汎用的なロジックの抽出(例: カスタムフックの切り出し)
- コンポーネントの再利用性を高める
コード例
例1: 表示とロジックの分離
// Before: ロジックと表示が混在
function UserList() {
const [users, setUsers] = useState([]);
useEffect(() => {
fetch("/api/users")
.then((res) => res.json())
.then((data) => setUsers(data));
}, []);
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
上記コンポーネントを分離して責任区分を明確にする
// After: 表示とロジックを分離
// Presentational Component
function UserList({ users }) {
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
// Container Component
function UserListContainer() {
const [users, setUsers] = useState([]);
useEffect(() => {
fetch("/api/users")
.then((res) => res.json())
.then((data) => setUsers(data));
}, []);
return <UserList users={users} />;
}
例2: カスタムフックでロジックを抽出
// useUsers.js
import { useState, useEffect } from "react";
function useUsers() {
const [users, setUsers] = useState([]);
useEffect(() => {
fetch("/api/users")
.then((res) => res.json())
.then((data) => setUsers(data));
}, []);
return users;
}
export default useUsers;
// コンテナコンポーネントで使用
function UserListContainer() {
const users = useUsers();
return <UserList users={users} />;
}
上記で示した分離も、小規模なアプリのうちはまずコンテナ分離を行う方が望ましい。
最初からいきなりカスタムフックに切り出してしまうと、どの部分を抽象化すべきか?の判断に迷う・謝る可能性がある。
コンテナをまず分離しておいて、必要に応じてその中のロジックだけをカスタムフックとして切り出す段階的なリファクタリングを行うことで迷子になりにくくなると思います。
カスタムフックは再利用性が求められる段階で導入するのが適切で、必要以上に抽象化してしまう無駄な設計・過剰な設計は開発スピードと柔軟性を犠牲にしてしまうのでバランスが大事。
大規模(アーキテクチャレベル)
目的
- スケーラビリティの向上
- モジュール間の依存関係を最小化
- 長期的なメンテナンス性を確保
フェーズごとの取り組み
プロダクト立ち上げ直後
- Context APIを使用して状態を管理
- シンプルなディレクトリ構造
たとえばこんな構造
src/
components/
App.js
Header.js
Footer.js
index.js
小〜中規模アプリケーション(成長期)
- Redux や Recoil を使用した状態管理の一元化
- 機能単位でディレクトリを分ける
たとえばこんな構造
src/
features/
users/
components/
UserList.js
containers/
UserListContainer.js
store/
usersSlice.js
store/
rootReducer.js
中〜大規模アプリケーション(拡大期)
- マイクロフロントエンドアーキテクチャ を採用
- 共通モジュールを独立したパッケージとして管理
src/
features/
users/
orders/
shared/
components/
Button.js
hooks/
useAuth.js
utils/
fetch.js
まとめ
リファクタリングはスコープやアプリの規模感に応じて適切な手法や戦略を選択することが大切。
大規模なアプリまで成長したらどんなリファクタリングが必要になるか知っておくことも大切ですが、まだ規模が小さい段階から先取りしてリファクタリングの手法を取り入れるのも開発速度を低下させる原因になります。どこまでやるのが良いか都度判断して最適な手法がどれか?を即座に考えられるようなエンジニアへ成長していきたいです。