ریفکتور کد با هوش مصنوعی؛ آموزش بازآرایی Legacy Code و ساخت AI Refactoring Assistant
در این آموزش یاد میگیرید Legacy Code را بدون تغییر ناخواسته رفتار با AI ریفکتور کنید و ابزاری بسازید که کد و تستها را تحلیل، برنامه مرحلهای و Patch پیشنهادی تولید و نتیجه را ارزیابی میکند.
رفکتور کد با هوش مصنوعی؛ بازآرایی ایمن Legacy Code
رفکتور یا Refactoring به معنی تغییر ساختار داخلی کد بدون تغییر رفتار قابل مشاهده آن است.
هدف رفکتور معمولاً یکی از این موارد است:
- خواناترشدن کد
- کاهش تکرار
- کوچککردن تابعهای پیچیده
- جداکردن مسئولیتها
- بهبود نامگذاری
- سادهکردن شرطها
- کاهش وابستگی میان ماژولها
- آمادهکردن کد برای توسعه قابلیت جدید
- سادهترکردن تست
- حذف مسیرهای منسوخ
هوش مصنوعی میتواند یک تابع ۳۰۰ خطی را در چند ثانیه به چند تابع کوچکتر تبدیل کند. اما سؤال اصلی این نیست که آیا کد جدید زیباتر است؛ سؤال اصلی این است:
آیا رفتار سیستم پس از رفکتور دقیقاً حفظ شده است؟
اگر مدل در حین بازآرایی یکی از این موارد را تغییر دهد، دیگر فقط Refactor انجام ندادهایم:
- ترتیب Validation
- نوع Exception
- متن خطا
- گردکردن مبلغ
- رفتار ورودی خالی
- ترتیب خروجی
- مقدار پیشفرض
- Mutation ورودی
- تعداد فراخوانی Dependency
- Contract عمومی تابع
- اثر جانبی ثبتشده
در این مقاله یک روش عملی برای Refactoring با AI پیاده میکنیم:
- رفتار فعلی و رفتار مورد انتظار را جدا میکنیم.
- Code Smellها را شناسایی میکنیم.
- Characterization Test میسازیم.
- برنامه رفکتور مرحلهای تولید میکنیم.
- هر مرحله را با Patch کوچک انجام میدهیم.
- تستها و ابزارهای قطعی را اجرا میکنیم.
- Diff نهایی را از نظر تغییر رفتار بررسی میکنیم.
- یک AI Refactoring Assistant با پایتون و API درواره میسازیم.
تفاوت Refactor، Rewrite و Feature Change
این سه مفهوم نباید در یک Pull Request با یکدیگر مخلوط شوند.
Refactor
ساختار داخلی تغییر میکند، اما رفتار عمومی حفظ میشود.
مثال:
def calculate_total(items):
subtotal = sum(
item.price * item.quantity
for item in items
)
return subtotal
به:
def calculate_subtotal(items):
return sum(
item.price * item.quantity
for item in items
)
def calculate_total(items):
return calculate_subtotal(items)
Rewrite
بخش مهمی از پیادهسازی از ابتدا نوشته میشود. احتمال تغییر رفتار و ایجاد خطای جدید بیشتر است.
Feature Change
قابلیت یا رفتار جدیدی اضافه میشود:
از این پس برای خریدهای بالاتر از مبلغ مشخص تخفیف اعمال شود.
این تغییر Refactor نیست، حتی اگر کد همزمان تمیزتر شود.
هوش مصنوعی در رفکتور چه کارهایی انجام میدهد؟
شناسایی Code Smell
- تابع طولانی
- شرطهای تو در تو
- مسئولیتهای متعدد
- تکرار منطق
- پارامترهای زیاد
- Boolean Flagهای متعدد
- Mutation پنهان
- Dependency مستقیم
- نامگذاری مبهم
- Magic Number
- Comment توضیحدهنده کد پیچیده
- Data Clump
- Primitive Obsession
پیشنهاد مرزهای استخراج تابع
مدل میتواند بخشهایی را که مسئولیت مستقلی دارند پیشنهاد کند:
- Validation
- Calculation
- Transformation
- Persistence
- Formatting
- Notification
ساخت برنامه مرحلهای
بهجای تولید یک Patch بزرگ، مراحل کوچک پیشنهاد میشود:
- افزودن تست رفتار فعلی
- استخراج Constantها
- استخراج Validation
- استخراج محاسبه
- حذف تکرار
- بهبود نامها
- اجرای کامل تستها
تولید Characterization Test
برای کد قدیمی که Specification کامل ندارد، AI میتواند به ثبت رفتار فعلی کمک کند.
پیشنهاد Patch کوچک
هر Patch باید فقط یک هدف ساختاری داشته باشد.
توضیح اثر Refactor
مدل میتواند قبل و بعد را مقایسه و تغییرهای احتمالی Contract را مشخص کند.
مهمترین ریسک: زیباترشدن کد همراه با تغییر رفتار
کد قدیمی:
if customer_type == "vip":
discount = subtotal * Decimal("0.10")
elif subtotal > Decimal("10000000"):
discount = subtotal * Decimal("0.05")
مدل ممکن است این منطق را به دو شرط مستقل تبدیل کند:
if customer_type == "vip":
discount += subtotal * Decimal("0.10")
if subtotal > Decimal("10000000"):
discount += subtotal * Decimal("0.05")
کد دوم خوانا به نظر میرسد، اما رفتار تغییر کرده است. مشتری VIP با خرید بزرگ اکنون هر دو تخفیف را دریافت میکند.
برای جلوگیری از چنین خطاهایی، قبل از رفکتور باید رفتارهای مرزی با Test ثبت شوند.
Characterization Test چیست؟
Characterization Test رفتار فعلی کد را ثبت میکند، حتی اگر هنوز ندانیم آن رفتار از نظر محصول کاملاً صحیح است یا خیر.
این تست پاسخ میدهد:
کد فعلی برای این ورودی چه خروجی یا اثر جانبی دارد؟
نکته مهم: Characterization Test با Acceptance Test یکسان نیست.
- Characterization Test رفتار فعلی را ثبت میکند.
- Acceptance Test رفتار مورد انتظار محصول را تعریف میکند.
اگر میان این دو اختلاف وجود دارد، باید ابتدا درباره رفتار صحیح تصمیم گرفته شود. Refactor نباید مخفیانه این اختلاف را حل کند.
پرامپت آماده برای تحلیل رفکتور
تو یک مهندس ارشد نرمافزار هستی.
کد زیر را برای Refactoring تحلیل کن، اما هنوز Patch نساز.
خروجی:
1. مسئولیتهای فعلی
2. API عمومی که باید حفظ شود
3. اثرهای جانبی
4. Code Smellهای قابل اثبات
5. رفتارهای مرزی
6. تستهای موجود
7. تستهای لازم پیش از Refactor
8. برنامه Refactor مرحلهای
9. معیار پایان هر مرحله
10. ریسک تغییر رفتار
قواعد:
- Feature جدید پیشنهاد نده.
- تغییر رفتار را Refactor معرفی نکن.
- بازنویسی کامل پیشنهاد نده.
- هر مرحله باید کوچک و مستقل باشد.
- ترتیب Validation و نوع Exception را حفظ کن.
- ابتدا تست و سپس تغییر ساختار پیشنهاد بده.
Specification:
[SPECIFICATION]
Source:
[SOURCE CODE]
Existing tests:
[TESTS]
پروژه عملی: رفکتور سرویس قیمتگذاری سفارش
کدی داریم که چند مسئولیت را در یک تابع انجام میدهد:
- Validation
- محاسبه Subtotal
- اعمال تخفیف
- محاسبه مالیات
- Formatting خروجی
ایجاد پروژه
mkdir ai-refactoring-assistant
cd ai-refactoring-assistant
python -m venv .venv
فعالسازی در Linux و macOS:
source .venv/bin/activate
فعالسازی در Windows:
.venv\Scripts\Activate.ps1
نصب وابستگیها:
pip install \
openai \
python-dotenv \
pydantic \
pytest \
pytest-cov
ساخت پوشهها:
mkdir app tests refactor_assistant plans output specifications
فایلهای زیر را ایجاد کنید:
app/__init__.py
app/pricing.py
tests/__init__.py
tests/test_pricing.py
refactor_assistant/__init__.py
refactor_assistant/schemas.py
refactor_assistant/collector.py
refactor_assistant/planner.py
refactor_assistant/patch_generator.py
refactor_assistant/renderer.py
plan_refactor.py
generate_refactor_patch.py
کد Legacy نمونه
فایل app/pricing.py:
from decimal import (
Decimal,
ROUND_HALF_UP,
)
def calculate_order_price(
items,
customer_type,
tax_rate,
):
if not isinstance(items, list):
raise TypeError(
"items must be a list"
)
if customer_type not in {
"regular",
"vip",
}:
raise ValueError(
"unsupported customer type"
)
if tax_rate < 0 or tax_rate > 1:
raise ValueError(
"tax rate must be between 0 and 1"
)
subtotal = Decimal("0")
for item in items:
if "price" not in item:
raise ValueError(
"item price is required"
)
if "quantity" not in item:
raise ValueError(
"item quantity is required"
)
price = Decimal(
str(item["price"])
)
quantity = int(
item["quantity"]
)
if price < 0:
raise ValueError(
"price cannot be negative"
)
if quantity < 0:
raise ValueError(
"quantity cannot be negative"
)
subtotal += price * quantity
subtotal = subtotal.quantize(
Decimal("0.01"),
rounding=ROUND_HALF_UP,
)
discount = Decimal("0")
if customer_type == "vip":
discount = (
subtotal * Decimal("0.10")
).quantize(
Decimal("0.01"),
rounding=ROUND_HALF_UP,
)
elif subtotal > Decimal("10000000"):
discount = (
subtotal * Decimal("0.05")
).quantize(
Decimal("0.01"),
rounding=ROUND_HALF_UP,
)
taxable_amount = (
subtotal - discount
).quantize(
Decimal("0.01"),
rounding=ROUND_HALF_UP,
)
tax = (
taxable_amount
* Decimal(str(tax_rate))
).quantize(
Decimal("0.01"),
rounding=ROUND_HALF_UP,
)
total = (
taxable_amount + tax
).quantize(
Decimal("0.01"),
rounding=ROUND_HALF_UP,
)
return {
"subtotal": str(subtotal),
"discount": str(discount),
"taxable_amount": str(
taxable_amount
),
"tax": str(tax),
"total": str(total),
}
کد کار میکند، اما مسئولیتهای مختلف در یک تابع قرار گرفتهاند.
Specification رفتار
فایل specifications/pricing.md:
# Order pricing specification
## Public API
تابع calculate_order_price سه پارامتر دریافت میکند:
- items
- customer_type
- tax_rate
خروجی یک Dictionary شامل رشتههای عددی با دو رقم اعشار است.
## Validation
- items باید list باشد.
- customer_type فقط regular یا vip است.
- tax_rate باید بین صفر و یک باشد.
- هر Item باید price و quantity داشته باشد.
- price و quantity نباید منفی باشند.
- مقدار quantity به int تبدیل میشود.
- Validationها باید قبل از محاسبه انجام شوند.
## Discount
- مشتری vip دقیقاً 10 درصد تخفیف میگیرد.
- مشتری regular فقط وقتی subtotal بزرگتر از 10000000 است، 5 درصد تخفیف میگیرد.
- اگر subtotal دقیقاً 10000000 باشد، تخفیف regular اعمال نمیشود.
- تخفیفها با یکدیگر جمع نمیشوند.
## Tax
- مالیات پس از تخفیف محاسبه میشود.
- tax_rate صفر و یک معتبرند.
## Money
- تمام مبالغ با ROUND_HALF_UP تا دو رقم اعشار گرد میشوند.
تست رفتار فعلی و مورد انتظار
فایل tests/test_pricing.py:
from decimal import Decimal
import pytest
from app.pricing import (
calculate_order_price,
)
def test_empty_order_returns_zero_values():
result = calculate_order_price(
[],
"regular",
Decimal("0.10"),
)
assert result == {
"subtotal": "0.00",
"discount": "0",
"taxable_amount": "0.00",
"tax": "0.00",
"total": "0.00",
}
def test_vip_discount_is_applied_before_tax():
result = calculate_order_price(
[
{
"price": "100.00",
"quantity": 1,
}
],
"vip",
Decimal("0.20"),
)
assert result == {
"subtotal": "100.00",
"discount": "10.00",
"taxable_amount": "90.00",
"tax": "18.00",
"total": "108.00",
}
def test_regular_discount_above_threshold():
result = calculate_order_price(
[
{
"price": "10000001",
"quantity": 1,
}
],
"regular",
Decimal("0"),
)
assert result["discount"] == "500000.05"
assert result["total"] == "9500000.95"
def test_regular_has_no_discount_at_threshold():
result = calculate_order_price(
[
{
"price": "10000000",
"quantity": 1,
}
],
"regular",
Decimal("0"),
)
assert result["discount"] == "0"
assert result["total"] == "10000000.00"
def test_vip_does_not_stack_discounts():
result = calculate_order_price(
[
{
"price": "20000000",
"quantity": 1,
}
],
"vip",
Decimal("0"),
)
assert result["discount"] == "2000000.00"
@pytest.mark.parametrize(
"customer_type",
[
"",
"gold",
None,
],
)
def test_rejects_unknown_customer_type(
customer_type,
):
with pytest.raises(
ValueError,
match="unsupported customer type",
):
calculate_order_price(
[],
customer_type,
Decimal("0"),
)
@pytest.mark.parametrize(
"tax_rate",
[
Decimal("-0.01"),
Decimal("1.01"),
],
)
def test_rejects_invalid_tax_rate(
tax_rate,
):
with pytest.raises(
ValueError,
match=(
"tax rate must be "
"between 0 and 1"
),
):
calculate_order_price(
[],
"regular",
tax_rate,
)
نکته مهم: خروجی تخفیف سبد خالی "0" است، نه "0.00". این رفتار فعلی شاید ایدهآل نباشد، اما تغییر آن یک تصمیم Contract است و نباید مخفیانه در Refactor انجام شود.
اجرای تست و Coverage
pytest -q
سپس:
pytest \
--cov=app \
--cov-branch \
--cov-report=term-missing
Coverage بالا بهتنهایی کافی نیست. رفتارهای مهم مرزی باید با Assertion صریح ثبت شوند.
چه تستهایی پیش از Refactor کم هستند؟
itemsغیر از List- نبود
price - نبود
quantity - قیمت منفی
- تعداد منفی
- نرخ مالیات دقیقاً صفر
- نرخ مالیات دقیقاً یک
- گردکردن
ROUND_HALF_UP - تبدیل Quantity رشتهای به
int - تغییرنکردن فهرست ورودی
- حفظ متن Exception
- ترتیب Validation در ورودی دارای چند خطا
AI Refactoring Assistant باید ابتدا این Gapها را گزارش کند.
تعریف Schema برنامه رفکتور
فایل refactor_assistant/schemas.py:
from typing import Literal
from pydantic import BaseModel, Field
class BehaviorContract(BaseModel):
name: str
description: str
source: Literal[
"specification",
"test",
"source_code",
]
must_preserve: bool
class CodeSmell(BaseModel):
title: str
evidence: str
impact: str
confidence: float = Field(
ge=0,
le=1,
)
class MissingTest(BaseModel):
title: str
behavior: str
reason: str
priority: Literal[
"high",
"medium",
"low",
]
class RefactorStep(BaseModel):
order: int
title: str
goal: str
files: list[str]
transformations: list[str]
preserved_behaviors: list[str]
tests_to_run: list[str]
completion_criteria: list[str]
risk: Literal[
"high",
"medium",
"low",
]
class RefactorPlan(BaseModel):
target: str
summary: str
public_api: list[str]
side_effects: list[str]
behavior_contracts: list[
BehaviorContract
]
code_smells: list[CodeSmell]
missing_tests: list[MissingTest]
steps: list[RefactorStep]
out_of_scope: list[str]
unresolved_questions: list[str]
جمعآوری Context
فایل refactor_assistant/collector.py:
import subprocess
import sys
from pathlib import Path
def read_file(
file_path: Path,
max_chars: int = 50_000,
) -> str:
content = file_path.read_text(
encoding="utf-8"
)
if len(content) > max_chars:
return (
content[:max_chars]
+ "\n\n[CONTENT TRUNCATED]"
)
return content
def run_tests(
timeout_seconds: int = 60,
) -> dict:
command = [
sys.executable,
"-m",
"pytest",
"-q",
]
try:
result = subprocess.run(
command,
capture_output=True,
text=True,
timeout=timeout_seconds,
check=False,
)
except subprocess.TimeoutExpired:
return {
"command": command,
"return_code": None,
"stdout": "",
"stderr": "",
"timed_out": True,
}
return {
"command": command,
"return_code": result.returncode,
"stdout": result.stdout[-20_000:],
"stderr": result.stderr[-10_000:],
"timed_out": False,
}
def collect_refactor_context() -> dict:
return {
"target_file": "app/pricing.py",
"source": read_file(
Path("app/pricing.py")
),
"specification": read_file(
Path(
"specifications/pricing.md"
)
),
"tests": read_file(
Path("tests/test_pricing.py")
),
"test_run": run_tests(),
"constraints": [
"Public function signature must not change.",
"Returned dictionary keys must not change.",
"String formatting must remain unchanged.",
"Exception types and messages must remain unchanged.",
"No new third-party dependency is allowed.",
"This task is refactoring, not feature development.",
],
}
تولید برنامه رفکتور با API درواره
فایل refactor_assistant/planner.py:
import json
import os
from dotenv import load_dotenv
from openai import OpenAI
from pydantic import ValidationError
from refactor_assistant.schemas import (
RefactorPlan,
)
load_dotenv()
api_key = os.getenv("DARVAREH_API_KEY")
model = os.getenv("DARVAREH_MODEL")
if not api_key:
raise RuntimeError(
"DARVAREH_API_KEY is not configured."
)
if not model:
raise RuntimeError(
"DARVAREH_MODEL is not configured."
)
client = OpenAI(
api_key=api_key,
base_url="https://api.darvareh.ir/v1",
)
SYSTEM_PROMPT = """
تو یک مهندس ارشد نرمافزار و متخصص Refactoring هستی.
در این مرحله فقط برنامه Refactor تولید کن و Patch نساز.
اولویت:
1. حفظ رفتار عمومی
2. ساخت تستهای لازم
3. مراحل کوچک و قابل بازگشت
4. کاهش پیچیدگی
5. بهبود خوانایی
قواعد:
- Feature جدید پیشنهاد نده.
- تغییر رفتار را Refactor معرفی نکن.
- بازنویسی کامل پیشنهاد نده.
- API عمومی، نوع Exception، متن خطا و قالب خروجی را حفظ کن.
- رفتار Source Code را با Specification اشتباه نگیر.
- اختلاف میان Source، Test و Specification را گزارش کن.
- هر مرحله فقط یک هدف اصلی داشته باشد.
- قبل از تغییر پرریسک، Test لازم را پیشنهاد کن.
- خروجی فقط JSON معتبر باشد.
"""
OUTPUT_TEMPLATE = {
"target": "app/pricing.py",
"summary": "string",
"public_api": [
"calculate_order_price"
],
"side_effects": [],
"behavior_contracts": [
{
"name": "string",
"description": "string",
"source": "specification",
"must_preserve": True,
}
],
"code_smells": [
{
"title": "Long Function",
"evidence": "string",
"impact": "string",
"confidence": 0.95,
}
],
"missing_tests": [
{
"title": "string",
"behavior": "string",
"reason": "string",
"priority": "high",
}
],
"steps": [
{
"order": 1,
"title": "string",
"goal": "string",
"files": [
"app/pricing.py"
],
"transformations": [
"string"
],
"preserved_behaviors": [
"string"
],
"tests_to_run": [
"pytest -q"
],
"completion_criteria": [
"string"
],
"risk": "low",
}
],
"out_of_scope": [],
"unresolved_questions": [],
}
def generate_refactor_plan(
context: dict,
) -> RefactorPlan:
prompt = (
"Context پروژه:\n\n"
+ json.dumps(
context,
ensure_ascii=False,
indent=2,
)
+ "\n\nقالب خروجی:\n"
+ json.dumps(
OUTPUT_TEMPLATE,
ensure_ascii=False,
indent=2,
)
)
response = client.chat.completions.create(
model=model,
temperature=0.1,
messages=[
{
"role": "system",
"content": SYSTEM_PROMPT,
},
{
"role": "user",
"content": prompt,
},
],
)
raw_output = (
response.choices[0]
.message.content
)
if not raw_output:
raise RuntimeError(
"The model returned an empty plan."
)
try:
parsed = json.loads(raw_output)
except json.JSONDecodeError as error:
raise RuntimeError(
f"Invalid JSON from model: "
f"{error}"
) from error
try:
return RefactorPlan.model_validate(
parsed
)
except ValidationError as error:
raise RuntimeError(
f"Refactor plan validation "
f"failed: {error}"
) from error
اسکریپت ساخت Plan
فایل plan_refactor.py:
from pathlib import Path
from refactor_assistant.collector import (
collect_refactor_context,
)
from refactor_assistant.planner import (
generate_refactor_plan,
)
def main():
context = collect_refactor_context()
if (
context["test_run"]["return_code"]
!= 0
):
raise RuntimeError(
"Existing tests must pass before "
"starting refactoring."
)
plan = generate_refactor_plan(
context
)
output_path = Path(
"plans/refactor_plan.json"
)
output_path.parent.mkdir(
parents=True,
exist_ok=True,
)
output_path.write_text(
plan.model_dump_json(indent=2),
encoding="utf-8",
)
print(plan.model_dump_json(indent=2))
print(f"\nPlan saved to {output_path}")
if __name__ == "__main__":
main()
فایل .env:
DARVAREH_API_KEY=YOUR_API_KEY
DARVAREH_MODEL=MODEL_ID_DARVAREH
اجرا:
python plan_refactor.py
برای دریافت کلید API در درواره ثبتنام کنید. Model ID را از صفحه مدلهای درواره انتخاب کنید.
برنامه Refactor مناسب برای مثال ما
یک Plan منطقی:
- تکمیل تستهای Validation و Rounding
- استخراج Constant مربوط به دقت پول
- استخراج تابع
round_money - استخراج Validation پارامترهای اصلی
- استخراج Parse و Validation اقلام
- استخراج محاسبه Subtotal
- استخراج محاسبه Discount
- استخراج Tax
- حفظ تابع عمومی بهعنوان Orchestrator
- اجرای Test Suite بعد از هر مرحله
نباید در یک مرحله تمام تابع بازنویسی شود.
اضافهکردن تستهای کمبود
def test_rejects_non_list_items():
with pytest.raises(
TypeError,
match="items must be a list",
):
calculate_order_price(
(),
"regular",
Decimal("0"),
)
def test_requires_item_price():
with pytest.raises(
ValueError,
match="item price is required",
):
calculate_order_price(
[
{
"quantity": 1,
}
],
"regular",
Decimal("0"),
)
def test_requires_item_quantity():
with pytest.raises(
ValueError,
match="item quantity is required",
):
calculate_order_price(
[
{
"price": "10",
}
],
"regular",
Decimal("0"),
)
def test_rounds_money_half_up():
result = calculate_order_price(
[
{
"price": "1.005",
"quantity": 1,
}
],
"regular",
Decimal("0"),
)
assert result["subtotal"] == "1.01"
بعد از اضافهکردن تستها:
pytest -q
تولید Patch برای فقط یک مرحله
نباید کل Plan را یکباره به مدل بدهیم و درخواست Patch کامل کنیم. یک Step انتخاب کنید.
فایل refactor_assistant/patch_generator.py:
import json
from pathlib import Path
from refactor_assistant.planner import (
client,
model,
)
from refactor_assistant.schemas import (
RefactorPlan,
)
PATCH_SYSTEM_PROMPT = """
تو فقط یک مرحله تأییدشده Refactoring را اجرا میکنی.
قواعد:
- خروجی فقط Unified Diff باشد.
- Markdown و توضیح خارج از Diff تولید نکن.
- رفتار عمومی را تغییر نده.
- امضای تابع عمومی را تغییر نده.
- Type و متن Exceptionها را حفظ کن.
- قالب Dictionary خروجی را حفظ کن.
- Dependency جدید اضافه نکن.
- فایل خارج از Step را تغییر نده.
- Feature جدید اضافه نکن.
- Testها را حذف یا ضعیف نکن.
- اگر Step بدون تغییر رفتار قابل انجام نیست، Patch تولید نکن.
"""
def generate_patch_for_step(
step_order: int,
) -> str:
plan = RefactorPlan.model_validate_json(
Path(
"plans/refactor_plan.json"
).read_text(encoding="utf-8")
)
selected_step = next(
(
step
for step in plan.steps
if step.order == step_order
),
None,
)
if selected_step is None:
raise ValueError(
f"Step {step_order} not found."
)
files = {}
for file_name in selected_step.files:
files[file_name] = Path(
file_name
).read_text(encoding="utf-8")
tests = Path(
"tests/test_pricing.py"
).read_text(encoding="utf-8")
prompt = {
"approved_step": (
selected_step.model_dump()
),
"behavior_contracts": [
contract.model_dump()
for contract in (
plan.behavior_contracts
)
if contract.must_preserve
],
"files": files,
"tests": tests,
}
response = client.chat.completions.create(
model=model,
temperature=0,
messages=[
{
"role": "system",
"content": (
PATCH_SYSTEM_PROMPT
),
},
{
"role": "user",
"content": json.dumps(
prompt,
ensure_ascii=False,
indent=2,
),
},
],
)
patch = (
response.choices[0]
.message.content
)
if not patch:
raise RuntimeError(
"The model returned an "
"empty patch."
)
return patch
ذخیره Patch بدون اعمال خودکار
فایل generate_refactor_patch.py:
import argparse
from pathlib import Path
from refactor_assistant.patch_generator import (
generate_patch_for_step,
)
def main():
parser = argparse.ArgumentParser()
parser.add_argument(
"--step",
type=int,
required=True,
)
arguments = parser.parse_args()
patch = generate_patch_for_step(
arguments.step
)
output_path = Path(
f"output/refactor-step-"
f"{arguments.step}.patch"
)
output_path.parent.mkdir(
parents=True,
exist_ok=True,
)
output_path.write_text(
patch,
encoding="utf-8",
)
print(patch)
print(f"\nPatch saved to {output_path}")
if __name__ == "__main__":
main()
اجرا:
python generate_refactor_patch.py --step 2
Patch را بخوانید و سپس اعتبار آن را بررسی کنید:
git apply --check output/refactor-step-2.patch
این دستور فقط بررسی میکند Patch قابل اعمال است و آن را اعمال نمیکند.
نمونه نتیجه رفکتور
یک نسخه بازآراییشده میتواند چنین باشد:
from decimal import (
Decimal,
ROUND_HALF_UP,
)
MONEY_PRECISION = Decimal("0.01")
VIP_DISCOUNT_RATE = Decimal("0.10")
REGULAR_DISCOUNT_RATE = Decimal("0.05")
REGULAR_DISCOUNT_THRESHOLD = Decimal(
"10000000"
)
def round_money(
value: Decimal,
) -> Decimal:
return value.quantize(
MONEY_PRECISION,
rounding=ROUND_HALF_UP,
)
def validate_request(
items,
customer_type,
tax_rate,
):
if not isinstance(items, list):
raise TypeError(
"items must be a list"
)
if customer_type not in {
"regular",
"vip",
}:
raise ValueError(
"unsupported customer type"
)
if tax_rate < 0 or tax_rate > 1:
raise ValueError(
"tax rate must be between 0 and 1"
)
def calculate_subtotal(
items,
) -> Decimal:
subtotal = Decimal("0")
for item in items:
if "price" not in item:
raise ValueError(
"item price is required"
)
if "quantity" not in item:
raise ValueError(
"item quantity is required"
)
price = Decimal(
str(item["price"])
)
quantity = int(
item["quantity"]
)
if price < 0:
raise ValueError(
"price cannot be negative"
)
if quantity < 0:
raise ValueError(
"quantity cannot be negative"
)
subtotal += price * quantity
return round_money(subtotal)
def calculate_discount(
subtotal: Decimal,
customer_type: str,
) -> Decimal:
if customer_type == "vip":
return round_money(
subtotal * VIP_DISCOUNT_RATE
)
if (
subtotal
> REGULAR_DISCOUNT_THRESHOLD
):
return round_money(
subtotal
* REGULAR_DISCOUNT_RATE
)
return Decimal("0")
def calculate_order_price(
items,
customer_type,
tax_rate,
):
validate_request(
items,
customer_type,
tax_rate,
)
subtotal = calculate_subtotal(
items
)
discount = calculate_discount(
subtotal,
customer_type,
)
taxable_amount = round_money(
subtotal - discount
)
tax = round_money(
taxable_amount
* Decimal(str(tax_rate))
)
total = round_money(
taxable_amount + tax
)
return {
"subtotal": str(subtotal),
"discount": str(discount),
"taxable_amount": str(
taxable_amount
),
"tax": str(tax),
"total": str(total),
}
رفتار عجیب "discount": "0" عمداً حفظ شده است. اگر محصول تصمیم بگیرد همه مبالغ دو رقم اعشار داشته باشند، آن تغییر باید در یک Feature یا Contract Change جداگانه انجام شود.
آیا استخراج تابع همیشه مفید است؟
خیر. شکستن بیش از حد کد میتواند خوانایی را کاهش دهد.
تابع استخراجشده باید حداقل یکی از این ویژگیها را داشته باشد:
- مسئولیت مستقل
- نامی که مفهوم را بهتر میکند
- منطق تکراری
- امکان تست مستقل معنادار
- کاهش محسوس پیچیدگی
- مرز مناسب Dependency
تابع زیر ارزش زیادی ندارد:
def subtract(a, b):
return a - b
مگر اینکه مفهوم کسبوکار مشخصی مانند calculate_taxable_amount ایجاد کند.
رفکتور شرطهای پیچیده
قبل:
if (
user
and user.is_active
and order.total > 0
and not order.cancelled
and (
user.is_vip
or order.total > threshold
)
):
...
بهجای درخواست «سادهکردن شرط»، رفتارهای منطقی را ثبت کنید:
- کاربر وجود ندارد.
- کاربر غیرفعال است.
- مبلغ صفر است.
- سفارش لغوشده است.
- کاربر VIP است.
- کاربر عادی با مبلغ بالاتر از حد است.
- کاربر عادی در مرز حد است.
سپس میتوان Predicateهای نامگذاریشده استخراج کرد.
رفکتور کد دارای Side Effect
اگر تابع علاوه بر محاسبه این کارها را انجام دهد:
- ثبت دیتابیس
- ارسال پیام
- نوشتن فایل
- تغییر Cache
- انتشار Event
ترتیب عملیات بخشی از رفتار است.
مدل باید این اطلاعات را ثبت کند:
{
"side_effects": [
{
"operation": "save order",
"order": 1
},
{
"operation": "publish order_created event",
"order": 2
}
]
}
رفکتور نباید ترتیب Side Effectها را بدون تصمیم صریح تغییر دهد.
رفکتور کد بدون تست
ترتیب مناسب:
- مسیرهای اصلی اجرا را شناسایی کنید.
- API عمومی را ثبت کنید.
- ورودی و خروجیهای واقعی را نمونهبرداری کنید.
- Characterization Test بسازید.
- Side Effectها را Mock یا Capture کنید.
- تستها را اجرا کنید.
- Refactor کوچک انجام دهید.
- دوباره تست کنید.
نباید مستقیماً از مدل بخواهید یک ماژول بدون تست را بازنویسی کند.
Golden Master Testing
برای تابعی با خروجی پیچیده میتوان مجموعهای از ورودیها را اجرا و خروجی فعلی را ذخیره کرد.
import json
from app.legacy_report import (
generate_report,
)
def test_report_matches_golden_master():
input_data = {
"customer_id": 42,
"period": "monthly",
}
actual = generate_report(
input_data
)
expected = json.loads(
Path(
"tests/fixtures/"
"report-golden.json"
).read_text(
encoding="utf-8"
)
)
assert actual == expected
Golden Master رفتار فعلی را ثبت میکند، اما اگر خروجی شامل زمان، شناسه تصادفی یا ترتیب غیرقطعی باشد باید ابتدا نرمال شود.
رفکتور مرحلهای با Git
برای هر مرحله:
pytest -q
سپس Diff را بررسی کنید:
git diff --stat
git diff
اگر مرحله بیش از حد بزرگ شده است، آن را کوچکتر کنید.
Commitهای پیشنهادی:
test: characterize pricing validation behavior
refactor: extract money rounding helper
refactor: extract pricing validation
refactor: extract subtotal calculation
refactor: extract discount calculation
هر Commit باید مستقل و تستپذیر باشد.
بررسی تغییر رفتار با Differential Testing
میتوان نسخه قدیمی و جدید را با ورودیهای یکسان اجرا و خروجی را مقایسه کرد.
def compare_implementations(
old_function,
new_function,
cases,
):
differences = []
for case in cases:
try:
old_result = old_function(
**case
)
old_error = None
except Exception as error:
old_result = None
old_error = (
type(error).__name__,
str(error),
)
try:
new_result = new_function(
**case
)
new_error = None
except Exception as error:
new_result = None
new_error = (
type(error).__name__,
str(error),
)
if (
old_result != new_result
or old_error != new_error
):
differences.append(
{
"case": case,
"old_result": old_result,
"new_result": new_result,
"old_error": old_error,
"new_error": new_error,
}
)
return differences
در Refactor خالص، Differences باید خالی باشند؛ مگر اختلافی که صریحاً تأیید شده باشد.
Property-based Testing برای حفظ رفتار
برای تولید ورودیهای متنوع میتوان از Hypothesis استفاده کرد:
pip install hypothesis
نمونه مقایسه نسخه قدیمی و جدید:
from decimal import Decimal
from hypothesis import (
given,
strategies as st,
)
@given(
price=st.decimals(
min_value="0",
max_value="100000000",
places=2,
allow_nan=False,
allow_infinity=False,
),
quantity=st.integers(
min_value=0,
max_value=100,
),
customer_type=st.sampled_from(
["regular", "vip"]
),
tax_rate=st.decimals(
min_value="0",
max_value="1",
places=2,
allow_nan=False,
allow_infinity=False,
),
)
def test_refactor_preserves_behavior(
price,
quantity,
customer_type,
tax_rate,
):
items = [
{
"price": str(price),
"quantity": quantity,
}
]
old_result = old_calculate(
items,
customer_type,
Decimal(tax_rate),
)
new_result = new_calculate(
items,
customer_type,
Decimal(tax_rate),
)
assert new_result == old_result
نسخه قدیمی و جدید باید همزمان و با نامهای جداگانه قابل اجرا باشند.
Refactor در برابر مهاجرت نسخه
مهاجرت Python، Framework یا Library معمولاً فقط Refactor نیست؛ زیرا Dependency جدید ممکن است رفتار متفاوتی داشته باشد.
برای Migration Plan این اطلاعات را آماده کنید:
- نسخه فعلی و مقصد
- Deprecationها
- Breaking Changeهای مستند
- APIهای استفادهشده
- Test Suite
- Lockfile
- Runtime
- محدودیت استقرار
- مسیر Rollback
مراحل مناسب:
- Test Baseline
- Inventory وابستگیها
- شناسایی APIهای Deprecated
- مهاجرت یک بخش
- اجرای تست
- مقایسه خروجی
- Benchmark در صورت نیاز
- مرحله بعد
مدل نباید فقط Syntax را تبدیل کند؛ تفاوت Semantics نیز باید بررسی شود.
استفاده از AI برای تبدیل کد بین زبانها
تبدیل Python به TypeScript یا PHP به Python یک Rewrite محسوب میشود، نه Refactor.
برای کاهش خطا:
- Contract مستقل تعریف کنید.
- Test Fixtureهای مشترک بسازید.
- ورودی و خروجی مرجع داشته باشید.
- نوع خطاها را نگاشت کنید.
- رفتار عدد، تاریخ و Null را مقایسه کنید.
- Differential Testing اجرا کنید.
- مهاجرت را ماژولبهماژول انجام دهید.
تشخیص Refactor خارج از محدوده
مواردی که باید متوقف شوند:
- تغییر API عمومی
- تغییر Schema داده
- تغییر نوع خطا
- اضافهکردن Dependency
- تغییر رفتار Business Rule
- حذف مسیر قدیمی بدون تصمیم
- تغییر فرمت خروجی
- تغییر ترتیب Side Effect
- تغییر همزمان چند ماژول نامرتبط
این موارد ممکن است ارزشمند باشند، اما باید Task و Review جداگانه داشته باشند.
اجرای Patch در محیط جدا
جریان پیشنهادی:
Source + Specification + Tests
↓
Refactor Plan
↓
Human Approval
↓
One-step Patch Draft
↓
Temporary Git Worktree
↓
Static Checks + Full Tests
↓
Differential and Coverage Check
↓
Human Review
در نسخه اولیه، ابزار فقط Patch را ذخیره کند و آن را خودکار روی Branch اصلی اعمال نکند.
معیارهای کیفیت Refactor
Behavior Preservation
مهمترین معیار؛ خروجی و اثر جانبی باید حفظ شوند.
Complexity
آیا پیچیدگی واقعاً کاهش یافته است؟
Cohesion
آیا هر تابع یا کلاس مسئولیت منسجمتری دارد؟
Coupling
آیا وابستگی میان اجزا کمتر یا روشنتر شده است؟
Testability
آیا رفتارهای مهم سادهتر تست میشوند؟
Diff Size
آیا تغییر مرحلهای و قابل Review است؟
Public API Stability
آیا Contract عمومی بدون تصمیم صریح تغییر نکرده است؟
Performance Baseline
اگر مسیر پرتکرار است، قبل و بعد Benchmark بگیرید.
ارزیابی AI Refactoring Assistant
Dataset پیشنهادی:
- تابع طولانی
- شرط تو در تو
- منطق تکراری
- کد دارای Side Effect
- API عمومی حساس
- کد بدون تست
- Code Smell ظاهری اما قابل قبول
- Refactor پیشنهادی با تغییر رفتار پنهان
- Migration نسخه
- تبدیل زبان
معیارها:
| معیار | توضیح |
|---|---|
| Behavior Preservation | درصد Patchهای بدون تغییر رفتار |
| Test Pass Rate | موفقیت تستهای قبلی |
| Differential Match | برابری خروجی قدیم و جدید |
| Patch Validity | قابل اعمال بودن Patch |
| Minimality | محدودبودن Diff |
| Scope Compliance | عدم تغییر خارج از Step |
| Smell Reduction | کاهش مشکل هدف |
| Human Acceptance | درصد Patchهای پذیرفتهشده |
| Review Time | زمان بررسی Patch |
| Regression Rate | خطاهای ایجادشده پس از Refactor |
مدیریت هزینه
تحلیل در سطح Symbol
فقط تابع، کلاس و تست مرتبط ارسال شوند.
Plan یکبار، Patch مرحلهای
Plan برای کل کار تولید و هر Patch فقط برای یک Step ساخته شود.
Cache کردن Plan
کلید Cache:
source_hash +
test_hash +
spec_hash +
model_id +
prompt_version
استفاده از ابزارهای قطعی
Formatter، Linter، Type Checker و Tests را به مدل نسپارید.
حذف فایلهای Generated
Lockfile و Build Output معمولاً Context مفیدی برای Refactor ندارند.
انتخاب مدل مناسب
برای Refactoring، مدل باید در این زمینهها مناسب باشد:
- درک عمیق کد
- پیروی از Constraint
- حفظ API
- تحلیل Test
- تولید Patch کوچک
- خروجی JSON معتبر
- Context Window مناسب
- شناخت Framework و زبان
برای استخراج نام یا تابع ساده ممکن است مدل سریعتر کافی باشد. برای Legacy Code چندفایلی، مدل قویتر احتمالاً نتیجه بهتری میدهد.
مدلها و هزینه بهروز را در صفحه مدلهای درواره بررسی کنید.
اشتباهات رایج
درخواست «این کد را تمیز کن»
هدف، Constraint و رفتارهای لازم برای حفظ مشخص نیستند.
Refactor بدون Test Baseline
ابتدا تستهای فعلی باید موفق باشند.
مخلوطکردن Refactor و Feature
تغییر رفتار را در Task جدا انجام دهید.
Patch بزرگ
هر مرحله باید یک هدف و امکان بازگشت داشته باشد.
اعتماد به ظاهر کد
خوانایی بهتر، حفظ رفتار را تضمین نمیکند.
حذف رفتار عجیب بدون تصمیم
رفتار عجیب ممکن است بخشی از Contract مصرفکننده باشد.
تغییر تست برای هماهنگی با Patch
Specification تعیین میکند کد یا تست کدامیک باید تغییر کند.
اجرای مستقیم Patch مدل
Patch ابتدا باید بررسی و در محیط جدا آزمایش شود.
نادیدهگرفتن Side Effect
فقط برابر بودن مقدار Return کافی نیست.
برنامه پیادهسازی در تیم
مرحله اول: تحلیل Code Smell
AI فقط مشکل و شواهد را گزارش کند.
مرحله دوم: Characterization Test
رفتارهای مهم و مرزی ثبت شوند.
مرحله سوم: Plan مرحلهای
هر Step مستقل و کوچک باشد.
مرحله چهارم: Patch پیشنهادی
Patch اعمال خودکار نشود.
مرحله پنجم: اجرای ایزوله
Test، Type Check، Linter و Differential Test اجرا شوند.
مرحله ششم: ارزیابی
Patchهای پذیرفته و ردشده برای بهبود Prompt ثبت شوند.
چکلیست رفکتور با AI
- هدف Refactor مشخص است.
- تغییر Feature داخل Task نیست.
- تستهای فعلی پیش از شروع موفقاند.
- Specification موجود است.
- API عمومی ثبت شده است.
- Side Effectها شناخته شدهاند.
- رفتارهای مرزی تست دارند.
- Characterization Test نوشته شده است.
- Plan مرحلهای است.
- هر Patch فقط یک هدف دارد.
- Dependency جدید اضافه نشده است.
- Exceptionها بدون تصمیم تغییر نکردهاند.
- قالب خروجی حفظ شده است.
- Differential Test اجرا شده است.
- کل Test Suite موفق است.
- Diff توسط توسعهدهنده بررسی شده است.
- API Key فقط در Backend نگهداری میشود.
پرسشهای متداول
آیا هوش مصنوعی میتواند کد را Refactor کند؟
بله. AI میتواند Code Smellها را شناسایی، برنامه Refactor و Patch پیشنهادی تولید کند. حفظ رفتار باید با تست و مقایسه خروجی تأیید شود.
تفاوت Refactor و بازنویسی چیست؟
در Refactor رفتار عمومی حفظ میشود. در Rewrite بخش مهمی از پیادهسازی دوباره نوشته میشود و احتمال تغییر رفتار بیشتر است.
آیا میتوان Legacy Code بدون تست را با AI بازآرایی کرد؟
ابتدا باید Characterization Test ساخته شود. بازنویسی مستقیم کد قدیمی بدون ثبت رفتار فعلی ریسک زیادی دارد.
Characterization Test چیست؟
تستی است که رفتار فعلی کد را ثبت میکند. این تست الزاماً اثبات نمیکند رفتار فعلی از نظر محصول صحیح است.
چگونه مطمئن شویم رفتار تغییر نکرده است؟
Test Suite، Differential Testing، Property-based Testing و بررسی Side Effectها را اجرا کنید.
آیا AI میتواند کد را بین دو زبان تبدیل کند؟
بله، اما این کار معمولاً Rewrite یا Migration است، نه Refactor. Contract و تستهای مرجع برای مقایسه دو نسخه ضروریاند.
آیا Patch تولیدشده را خودکار اعمال کنیم؟
در شروع خیر. Patch باید ذخیره، بررسی و در محیط جدا اجرا شود. خودکارسازی فقط پس از ارزیابی و با محدودیتهای روشن مناسب است.
بهترین مدل برای Refactoring چیست؟
به زبان، اندازه Context، کیفیت تست و پیچیدگی کد بستگی دارد. مدلهای موجود و قیمت آنها را در صفحه مدلهای درواره بررسی کنید.
API درواره چگونه به ابزار Refactor متصل میشود؟
در Backend، base_url را برابر https://api.darvareh.ir/v1 قرار دهید و Source، Test، Specification و Step تأییدشده را برای مدل ارسال کنید.
جمعبندی
رفکتور با هوش مصنوعی میتواند سرعت بهبود کدهای قدیمی را افزایش دهد، اما فقط زمانی که حفظ رفتار در مرکز فرایند قرار داشته باشد.
روش درست این نیست که کل فایل را به مدل بدهیم و بگوییم «تمیزش کن». ابتدا باید API عمومی، Side Effectها، رفتارهای مرزی و اختلاف میان Source و Specification ثبت شوند. سپس Characterization Testها تکمیل و یک Plan مرحلهای ساخته شود.
در پروژه این مقاله یک AI Refactoring Assistant با Python، Pytest، Pydantic و API درواره ساختیم. ابزار ابتدا Context و تستها را تحلیل میکند، Code Smellها و کمبود تست را گزارش میدهد و یک برنامه کوچک و قابل بازگشت میسازد. سپس فقط برای یک Step تأییدشده، Patch پیشنهادی تولید میکند.
برای شروع، در درواره ثبتنام و API Key دریافت کنید. مدل مناسب را نیز از صفحه مدلهای درواره انتخاب و ابزار را ابتدا روی یک تابع کوچک، دارای تست و بدون Side Effect پیچیده آزمایش کنید.
مقالات مرتبط
- ساخت دستیار برنامهنویسی اختصاصی برای شرکت
- بهترین مدل هوش مصنوعی برای برنامهنویسی
- بهترین ابزارهای برنامهنویسی با هوش مصنوعی؛ بخش اول
- بهترین ابزارهای برنامهنویسی با هوش مصنوعی؛ بخش دوم
- راهنمای Vibe Coding با هوش مصنوعی
- راهنمای AGENTS.md برای عاملهای برنامهنویسی
- آموزش ارزیابی مدلهای هوش مصنوعی و Evals
- راهنمای ساخت API هوش مصنوعی آماده Production
برای مطالعه شرایط استفاده و محدودیتهای مسئولیت، صفحه «سلب مسئولیت» را مشاهده کنید.