SuperAPI AI Directory

cc-refactor-safety

Coding & development

View repository

Updated 10-16 05:06

About this skill

Refactoring Safety Guidelines

Trigger Scenarios

  • The user says "refactor", "extract", "merge", "optimize structure", "simplify code"
  • Involves modifications to table columns, data structures, or configuration items
  • Merging conditional branches (if/else/switch)

Refactoring Process

1. Read the original code (mandatory)

Do not refactor from memory or guesswork; you must read the original code in full:

# 读取完整文件
Read: file_path="src/views/Order.vue"
If it is a conditional branch, read the code of all branches

Grep: pattern="if.*健康云" -A 50 -B 5 Grep: pattern="else" -A 50 -B 5


**常见错误**:
- ❌ 只读一个分支,推测其他分支
- ❌ 凭记忆重构表格列
- ❌ 假设两个条件分支结构相似

---

### 2. 制作对比清单

#### 表格重构示例

**原始代码(健康云租户)**:

Column 1: Order Number Column 2: Order Date Column 3: Material Name Column 4: Material Quantity Column 5: Unit Price Column 6: Amount


**原始代码(非健康云租户)**:

Column 1: Order Number Column 2: Creation Time Column 3: Material Name Column 4: Unit Price Column 5: Amount


**对比清单**:

| 健康云 | 非健康云 | 状态 |
|--------|---------|------|
| 订单编号 | 订单编号 | ✅ 一致 |
| 订单日期 | 创建时间 | ⚠️ 名称不同 |
| 材料名称 | 材料名称 | ✅ 一致 |
| 材料数量 | - | ❌ 非健康云无此列 |
| 单价 | 单价 | ✅ 一致 |
| 金额 | 金额 | ✅ 一致 |

#### 数据结构重构示例

**原始接口**:
```typescript
interface Order {
  id: string
  date: string
  items: Item[]
  total: number
}

Interface after refactoring:

interface Order {
  id: string
  createdAt: string  // 重命名:date → createdAt
  items: Item[]
  total: number
}

Comparison Checklist:

  • ✅ Same number of fields (4)
  • ⚠️ Field renamed: date → createdAt
  • ✅ Same field order
  • ✅ Same field types

3. Verification checkpoints

After refactoring is complete, you must check item by item:

Table Refactoring Check

  • Same number of columns (6 columns originally → 6 columns after refactoring)
  • Same column order (the 1st column is Order Number, the 2nd column is Date...)
  • Same column names (or a clear mapping relationship)
  • No columns omitted
  • No extra columns

Conditional Branch Check

  • All if/else/switch branches have been handled
  • The logic of each branch is consistent with the original code
  • No assumption that one branch is similar to another branch

Data Structure Check

  • Same number of fields
  • Same field types
  • Same field order (if order matters)
  • No fields omitted

4. Output Format

After refactoring is complete, output a comparison checklist:

## 重构对比

### 原始结构(健康云租户)
- 列1: 订单编号
- 列2: 订单日期
- 列3: 材料名称
- 列4: 材料数量
- 列5: 单价
- 列6: 金额

### 原始结构(非健康云租户)
- 列1: 订单编号
- 列2: 创建时间
- 列3: 材料名称
- 列4: 单价
- 列5: 金额

### 重构后结构
- 列1: 订单编号
- 列2: 日期(健康云显示"订单日期",非健康云显示"创建时间")
- 列3: 材料名称
- 列4: 材料数量(仅健康云显示)
- 列5: 单价
- 列6: 金额

### 变更说明
- 合并:订单日期 + 创建时间 → 日期(条件显示)
- 条件显示:材料数量仅在健康云租户显示
- 删除:无
- 新增:无

Common Pitfalls

1. Assumption-driven refactoring

Incorrect Example:

// 只读了健康云租户的代码
if (isHealthCloud) {
  columns = ['订单编号', '订单日期', '材料名称', '材料数量', '单价', '金额']
}

// ❌ 假设非健康云租户只是列名不同
else {
  columns = ['订单编号', '创建时间', '材料名称', '材料数量', '单价', '金额']
}

Correct Approach:

// 读取两个分支的原始代码
if (isHealthCloud) {
  columns = ['订单编号', '订单日期', '材料名称', '材料数量', '单价', '金额']
} else {
  // 非健康云租户没有"材料数量"列
  columns = ['订单编号', '创建时间', '材料名称', '单价', '金额']
}

2. Memory-based refactoring

Incorrect Example:

// ❌ 凭记忆重构,没有读取原始代码
const columns = ['订单编号', '材料名称', '单价', '金额', '订单日期']

Correct Approach:

// ✅ 读取原始代码,确认列顺序
Read: file_path="src/views/Order.vue"
// 原始代码:['订单编号', '订单日期', '材料名称', '材料数量', '单价', '金额']
const columns = ['订单编号', '订单日期', '材料名称', '材料数量', '单价', '金额']

3. Skipping verification

Incorrect Example:

// 重构完成,直接提交
// ❌ 没有验证列数、列顺序、列名

Correct Approach:

## 验证清单
- [x] 列数一致:原始 6 列 → 重构后 6 列
- [x] 列顺序一致:订单编号、订单日期、材料名称、材料数量、单价、金额
- [x] 列名一致:完全一致
- [x] 没有遗漏列
- [x] 没有多余列

5. Extracting submodules causes circular dependencies

Scenario: When splitting submodules from a large service/component, the submodule needs to call back to methods of the parent module

⚠️ Pre-check process (must be executed before splitting)

First extract shared methods, then split subservices:

拆分前:
1. 扫描父服务,识别被多个子服务调用的工具方法
2. 将这些方法提取到独立 Helper/Utils 类
3. 然后再拆分子服务(此时子服务依赖 Helper,不依赖父服务)

❌ 错误顺序:拆分子服务 → 发现循环依赖 → 修复
✅ 正确顺序:提取共享方法 → 拆分子服务 → 无循环依赖

⚠️ Repeated Pattern Warning

If a circular dependency already appeared when extracting the first subservice, then before extracting all subsequent subservices you must first check for the same problem:

真实案例:
- ReportService → 提取 ReportInvoiceService → 循环依赖(resolveTenantIds)
- ReportService → 提取 ReportPaymentService → 同样的循环依赖!
根因:resolveTenantIds() 留在 ReportService,所有子服务都需要它
修复:应在第一次发现时就提取 TenantHelper,避免后续重复犯错

Incorrect Example:

// ❌ ReportService 拆分出 ReportInvoiceService
// 但 ReportInvoiceService 又需要调用 ReportService.resolveTenantIds()
@Service
@RequiredArgsConstructor
public class ReportInvoiceService {
    private final ReportService reportService; // 循环依赖!
}

Correct Approach (by priority):

// ✅ 方案1:提取公共方法到独立工具类(推荐)
@Component
public class TenantHelper {
    public List<Long> resolveTenantIds(Long tenantId) { ... }
}

// ✅ 方案2:@Lazy 字段注入(应急)
@Service
public class ReportInvoiceService {
    @Autowired @Lazy
    private ReportService reportService;
}

// ✅ 方案3:函数式回调
public void process(Function<Long, List<Long>> tenantResolver) { ... }

Checklist:

  • Pre-check: Are there utility methods in the parent service that are called by multiple subservices? If yes → first extract them into an independent class
  • Repeat Check: Does this split have the same callback dependency as a previous split?
  • Draw the dependency graph: after splitting, is there a cycle A → B → A?
  • Is the dependency direction unidirectional: parent → child (child → parent is forbidden)
  • Spring Boot 3.x forbids constructor circular dependencies by default

Not limited to Spring:

  • Go: package-level circular references (compile error) → extract a common package
  • Vue: component circular references → async components or extract common logic into composables
  • TypeScript: module circular references → extract a common module

6. Initialization conflicts when extracting state management

Scenario: When extracting state from a component into a custom Hook/composable/Service, the encapsulated open method adds initialization logic, overwriting the state preset by the caller

Real case:

// 重构前:调用方直接控制状态,时序正确
setMarkPaidText(selectedOrderNumbers)  // 1. 设置订单号
setMarkPaidModalVisible(true)          // 2. 打开弹窗 ✅

// 重构后:提取到 useOrderModals hook
const openMarkPaidModal = () => {
  setMarkPaidText('')           // ❌ 清空了调用方刚设置的值!
  setMarkPaidModalVisible(true)
}

// 调用方:
setMarkPaidText(selectedOrderNumbers)  // 1. 设置订单号
openMarkPaidModal()                    // 2. 内部又清空了 → 弹窗空白

Correct approach:

// ✅ 方案1:open 方法接受参数
const openMarkPaidModal = (initialText?: string) => {
  if (initialText !== undefined) setMarkPaidText(initialText)
  setMarkPaidModalVisible(true)
}

// ✅ 方案2:初始化放在 close 而非 open
const closeMarkPaidModal = () => {
  setMarkPaidText('')  // 关闭时清空是安全的
  setMarkPaidModalVisible(false)
}

Checklist:

  • When extracting the open/show method, check whether the caller set state before open
  • Will the initialization logic in the open method overwrite the value preset by the caller
  • Initialization/clearing logic should be placed in close rather than open
  • If open needs initialization, it should be passed in via parameters rather than hardcoded internally

Applicable scope:

  • React:useState → useXxxModal hook
  • Vue:ref → useXxxDialog composable
  • Java: Service methods adding default value logic

7. Loss of layout structure during UI component refactoring

Scenario: When refactoring container components such as Modal/Dialog/Drawer, simplifying the complex title/header/footer causes button layout and functionality to be lost

Real case:

// 重构前:title 包含条件渲染的按钮
<Modal
  title={
    <div className="flex justify-between">
      <span>发票详情</span>
      {!editMode && <Button icon={<EditOutlined />}>编辑发票</Button>}
      {editMode && (
        <Space>
          <Button icon={<CloseOutlined />}>取消</Button>
          <Button type="primary" icon={<SaveOutlined />}>保存</Button>
        </Space>
      )}
    </div>
  }
  footer={[
    <Button key="download" type="primary" icon={<DownloadOutlined />}>
      下载原始PDF
    </Button>,
    <Button key="close">关闭</Button>,
  ]}
/>

// ❌ 重构后:title 简化为纯文本,footer 按钮被替换
<Modal
  title={`发票详情 - ${invoice?.invoiceNumber}`}  // 丢失编辑/取消/保存按钮
  footer={[
    <button key="edit">编辑</button>,   // 降级为原生 button + 丢失"下载PDF"
    <button key="close">关闭</button>,
  ]}
/>

Checklist:

  • Is the JSX structure of title/header/footer consistent with the original code
  • Conditionally rendered buttons ({condition && <Button>}) are all retained
  • Is the number of buttons consistent (N before refactoring → N after refactoring)
  • Are component library components retained (Ant Design <Button> cannot be downgraded to <button>)
  • Are function buttons omitted (such as "Download", "Export", etc.)

Applicable scope:

  • React: title/footer of Modal, Dialog, Drawer
  • Vue: header/footer slots of el-dialog, el-drawer
  • Any UI container component containing conditionally rendered buttons

8. Complex rendering logic is simplified and replaced

Scenario: When refactoring table columns or components, replacing complex render functions (JSON parsing, conditional formatting, diff comparison) with simple text display, causing functional degradation

Real case:

// 重构前:~80 行的复杂 render,从 beforeValue/afterValue JSON 解析渲染 diff
{
  title: '变更详情',
  key: 'changeDetails',
  render: (_, record) => {
    const oldValues = JSON.parse(record.beforeValue)
    const newValues = JSON.parse(record.afterValue)
    // ... 字段对比、颜色标记、条件渲染(新增/删除/更新)
    return <div>{changedKeys.map(key => (
      <div>{translateFieldName(key)}: {oldValue} → {newValue}</div>
    ))}</div>
  }
}

// ❌ 重构后:简化为读取一个数据库中为空的字段
{
  title: '变更摘要',
  dataIndex: 'changeSummary',  // 数据库中此字段为空!
  render: (val) => val || '-'  // 全部显示 -
}

Checklist:

  • Is the original render function over 20 lines or does it contain JSON parsing? → It cannot be simplified to simple text
  • Does the field used after refactoring (changeSummary) have data in the database?
  • Is the rendering effect equivalent (color marking, diff comparison, conditional display)
  • Is the data source consistent (beforeValue/afterValue vs changeSummary)

Applicable scope:

  • Refactoring of render functions for table columns
  • Refactoring of custom rendering for list items
  • Any rendering logic containing JSON parsing or conditional formatting

4. Refactoring based on field name guessing (extremely subtle)

Scenario: There are multiple similar fields in the type definition, and the wrong one was chosen during refactoring

Incorrect example:

// 类型定义
interface InvoiceHistory {
  changedAt?: string  // 可选字段
  createdAt: string   // 必填字段
  changeType?: string // 错误字段
  operationType: string // 正确字段
}

// ❌ 根据类型定义推测,选择了可选字段
dataIndex: 'changedAt'  // 可能为 undefined → Invalid Date
dataIndex: 'changeType' // 字段不存在 → 显示为空

// ❌ 枚举映射不完整
typeMap: {
  CREATE: { text: '创建', color: 'green' },
  UPDATE: { text: '更新', color: 'blue' },
  DELETE: { text: '删除', color: 'red' },
  // 遗漏了 UPLOAD、OCR_START、OCR_SUCCESS 等 7 个类型
}

Correct approach:

// ✅ 查看原始代码的实际使用
git show HEAD~1:src/components/InvoiceHistoryTab.tsx

// 原始代码使用 createdAt(必填)和 operationType(正确)
dataIndex: 'createdAt'
dataIndex: 'operationType'

// 原始代码有 10 个枚举类型
typeMap: {
  UPLOAD: { text: '上传发票', color: 'blue' },
  OCR_START: { text: '开始OCR', color: 'cyan' },
  OCR_SUCCESS: { text: 'OCR成功', color: 'green' },
  OCR_FAILED: { text: 'OCR失败', color: 'red' },
  OCR_RETRY: { text: '重试OCR', color: 'orange' },
  MANUAL_EDIT: { text: '手动编辑', color: 'orange' },
  LINK_ORDER: { text: '关联订单', color: 'purple' },
  UNLINK_ORDER: { text: '取消关联', color: 'magenta' },
  FIELD_CONFIRM: { text: '确认字段', color: 'green' },
  DELETE: { text: '删除发票', color: 'red' },
}

// ✅ 防御性编程
render: (val: string) => {
  if (!val) return '-'
  try {
    return new Date(val).toLocaleString('zh-CN')
  } catch {
    return val
  }
}

Checklist:

  • Check the actual field names in the original code (don't just look at the type definitions)
  • Prefer required fields and avoid optional fields
  • Enum mappings must be complete (check them one by one against the original code)
  • Runtime testing (TypeScript cannot detect field name errors)
  • Defensive programming (if (!val) return '-')

Why TypeScript cannot detect it:

// ❌ TypeScript 不会报错(changedAt 在类型定义中存在)
dataIndex: 'changedAt'  // 类型检查通过
render: (val: string) => new Date(val).toLocaleString()
// 运行时:val 为 undefined → Invalid Date

Pitfall #9: Shotgun Fix

Scenario: The same problem appears repeatedly in multiple files, and when fixing it, the same patch code is added file by file instead of extracting shared logic

Root cause

When a cross-cutting concern (such as image URL completion, amount formatting, permission validation) is scattered across multiple Services, the fixer tends to "fix wherever the error is reported," ultimately writing a nearly identical method in each of 5+ files.

Incorrect example

// ❌ 错误: 7 个 Service 各写一份 resolveImageUrl
// ProductService.java
private String resolveImageUrl(String url) {
    if (url == null || url.startsWith("http")) return url;
    return minioService.getPresignedUrl(url, 60 * 24 * 7);
}

// MiniProductService.java — 又写一份
private String resolveImageUrl(String url) { /* 同样逻辑 */ }

// CartService.java — 又写一份
private String resolveImageUrl(String url) { /* 同样逻辑 */ }

Correct approach

// ✅ 正确: 先全局扫描,再抽取共享工具,一次性补齐
// 第 1 步: 全局扫描受影响的位置
// Grep: pattern="getImageUrl|mainImage|imageUrl" type="java"

// 第 2 步: 抽取共享工具类
@Component
public class ImageUrlResolver {
    private final MinioService minioService;

    public String resolve(String imageUrl) {
        if (imageUrl == null || imageUrl.isBlank()) return imageUrl;
        if (imageUrl.startsWith("http://") || imageUrl.startsWith("https://")) {
            return imageUrl;
        }
        return minioService.getPresignedUrl(imageUrl, 60 * 24 * 7);
    }
}

// 第 3 步: 所有 Service 统一注入并使用

Identifying signals

Signal Description
The same private method appears in 3+ classes It should be extracted into a shared utility
After fixing one interface, users report the same problem on another interface A global scan should be done first, then fix everything at once
The fix diff contains the same change pattern in 5+ files Common logic should be extracted

Checklist

  • Was a global scan (grep) done before fixing to confirm all affected locations
  • Does the same fix pattern appear in 3+ files (if so, a shared method should be extracted)
  • Are all affected paths fixed at once (rather than waiting for users to report them one by one)
  • Do the newly added shared utility methods have clear responsibility boundaries and naming

Pitfall #10: Misalignment in deduplication/filtering of positionally corresponding collections

Scenario: Multiple lists correspond one-to-one by index position in the final output (such as [公司, 单号] / [姓名, 年龄] / [字段名, 字段值] / [问题, 答案]), and during refactoring only one of them is distinct() / filter() / sorted(), while the others are left unchanged → the length changes, indices become misaligned, and the output data is completely messed up.

Root cause

In the code, two List appear independent, but semantically they have a zip relationship. The developer thinks "deduplication makes the result cleaner," but in fact it breaks the implicit contract that "the i-th company corresponds to the i-th order number." This kind of error is especially subtle:

  • Unit tests often do not cover it (the case of multiple packages for the same company)
  • Compilation/type checking will not report an error (the two lists have the same type)
  • When the test data coincidentally has no duplicates, it is impossible to notice at all
  • It only blows up when duplicate real business data appears (顺丰:1, 圆通:2, 顺丰:3 → company 顺丰;圆通, order number 1;2;3)

Incorrect example (from a real review)

// ❌ 错误: companies 去重,numbers 不去重,破坏索引对应
List<String> companies = packages.stream()
    .map(ShippingPackage::getCompany)
    .filter(StringUtils::hasText)
    .distinct()
    .collect(Collectors.toList());
List<String> numbers = packages.stream()
    .map(ShippingPackage::getTrackingNo)
    .collect(Collectors.toList());

writeRow(sheet, "快递公司", String.join(";", companies));
writeRow(sheet, "快递单号", String.join(";", numbers));
// packages = [顺丰:SF1, 圆通:YT2, 顺丰:SF3]
// companies = "顺丰;圆通" (2 段)
// numbers   = "SF1;SF2;SF3" (3 段)
// 微信侧导入时按位置匹配 → 圆通 ↔ SF2,顺丰 ↔ SF3,物流错乱

Correct approach

Option A: Aligned operations (minimal change)

Either apply distinct/filter to both lists together, or leave both unchanged:

// ✅ 都不去重,保留所有包裹按顺序对应
List<String> companies = packages.stream()
    .map(ShippingPackage::getCompany)
    .collect(Collectors.toList());
List<String> numbers = packages.stream()
    .map(ShippingPackage::getTrackingNo)
    .collect(Collectors.toList());

if (companies.stream().distinct().count() == 1) {
    writeRow(sheet, "快递公司", companies.get(0));
} else {
    writeRow(sheet, "快递公司", String.join(";", companies));
}
writeRow(sheet, "快递单号", String.join(";", numbers));

Option B: Switch to a paired structure (recommended, eliminates the problem at its root)

Change "two parallel lists" into "one list of Pair/Record," and the index correspondence is guaranteed by the language:

public record ShippingPackage(String company, String trackingNo) {}

List<ShippingPackage> packages = order.getPackages();

String companyValue = packages.size() == 1
    ? packages.get(0).company()
    : packages.stream().map(ShippingPackage::company).collect(joining(";"));
String numberValue = packages.stream()
    .map(ShippingPackage::trackingNo)
    .collect(joining(";"));

Any subsequent operations (filter, sorted, partition) act on the entire record, and there will be no situation where a single field is changed while other fields remain unchanged.

When to use which

Scenario Recommended solution
The existing code is just a temporary patch-up, and changes are costly Solution A: Aligned operations
These two fields appear together in multiple places, or more fields may be added later Solution B: Paired structure
More than 3 fields are still matched by position Solution B must be used (any more and it becomes unreadable)

Checklist

  • Whether there are 2 or more lists that are matched by index position in the final output/concatenation
  • During refactoring, whether distinct/filter/sorted was applied to only one of them (breaking alignment)
  • Whether switching to List<Pair> / List<Record> has been considered to eliminate parallel lists at the root
  • Whether the test data covers the "duplicate values" scenario (multiple packages for the same company, multiple records with the same name)

Multilingual examples

For complete implementation examples in Java (Stream + Record) / Go (slice + struct) / TypeScript (Array + interface), see references/multi-lang-examples.md。


Rule traceability

> 📋 本回复遵循:`refactor-safety` - [章节]