Refactor ด้วย AI โดยไม่พังของเดิม

การ refactor คือการปรับโครงสร้างโค้ดให้ดีขึ้นโดยที่พฤติกรรมภายนอกต้องเหมือนเดิมทุกประการ ฟังดูง่าย แต่เมื่อให้ AI ช่วย มักเกิดปัญหาว่ามันแอบ "ปรับปรุง" พฤติกรรมไปด้วย เช่น เปลี่ยนข้อความ error แก้เงื่อนไขที่มันคิดว่าผิด หรือลบโค้ดที่ดูเหมือนไม่ได้ใช้ ซึ่งอาจเป็นสิ่งที่ระบบอื่นพึ่งพาอยู่ บทความนี้จะแสดงวิธี refactor ด้วย AI อย่างปลอดภัย เริ่มจากการเขียน characterization test เพื่อจับพฤติกรรมปัจจุบัน แบ่งงานเป็นขั้นเล็ก และกำหนดกติกาห้ามเปลี่ยนพฤติกรรมให้ชัด พร้อมตัวอย่างจริงของการแยก controller อ้วน ๆ ใน Yii2 ออกเป็น service
ทำไม refactor ด้วย AI ถึงพังง่าย
AI เก่งเรื่องเขียนโค้ดให้ดูสะอาด แต่ไม่รู้ว่าพฤติกรรมไหนของระบบที่ "แปลกแต่ตั้งใจ" ตัวอย่างที่เจอบ่อย:
- เปลี่ยน
==เป็น===ทั้งที่ค่าที่เข้ามาเป็น string จากฟอร์ม ทำให้เงื่อนไขที่เคยเป็นจริงกลายเป็นเท็จ - รวม query สอง query เป็นหนึ่ง แต่ลำดับผลลัพธ์เปลี่ยน
- เปลี่ยนการจัดการค่าว่าง เช่น จากเดิมคืน array ว่างกลายเป็นคืน
null - ลบ method ที่ค้นในโปรเจกต์แล้วไม่เจอผู้เรียก แต่จริง ๆ ถูกเรียกผ่าน route หรือ config แบบ string
- refactor หลายเรื่องในรอบเดียวจน diff ยาวเกินกว่าจะตรวจได้
วิธีแก้ไม่ใช่ห้ามใช้ AI แต่คือสร้างตาข่ายรองรับก่อนเริ่ม และจำกัดขอบเขตของแต่ละขั้น
ขั้นที่ 1: Characterization Test ก่อนแตะโค้ด
Characterization test คือเทสต์ที่บันทึกว่า "ตอนนี้โค้ดทำอะไรอยู่" ไม่ใช่ "โค้ดควรทำอะไร" แม้พฤติกรรมบางอย่างจะดูผิด เราก็เขียนเทสต์ยืนยันพฤติกรรมนั้นไว้ก่อน เพราะเป้าหมายของ refactor คือไม่เปลี่ยนอะไร ถ้าเจอบั๊กให้จดไว้แล้วแก้แยกเป็นงานอีกชิ้นหลัง refactor เสร็จ
ตัวอย่าง controller ที่จะ refactor เป็น action สร้างคำสั่งซื้อ ที่ทำทุกอย่างรวมกันในที่เดียว:
<?php
namespace app\controllers;
use app\models\Order;
use app\models\Product;
use Yii;
use yii\web\Controller;
use yii\web\BadRequestHttpException;
class OrderController extends Controller
{
public function actionCreate()
{
$items = Yii::$app->request->post('items', []);
if (empty($items)) {
throw new BadRequestHttpException('ไม่มีสินค้า');
}
$total = 0;
foreach ($items as $item) {
$product = Product::findOne((int) $item['product_id']);
if ($product === null || $product->stock < (int) $item['qty']) {
throw new BadRequestHttpException('สินค้าไม่พอ');
}
$total += $product->price * (int) $item['qty'];
}
if ($total >= 1000) {
$total = $total * 0.95;
}
$order = new Order();
$order->user_id = Yii::$app->user->id;
$order->total = round($total, 2);
$order->save(false);
// ... ตัดสต็อก ส่งอีเมล ฯลฯ
return $this->redirect(['view', 'id' => $order->id]);
}
}
ส่วนที่มีตรรกะทางธุรกิจคือการคำนวณยอดรวมและส่วนลด ซึ่งเป็นสิ่งที่ต้องจับพฤติกรรมไว้ prompt สำหรับให้ AI ช่วยเขียน characterization test:
นี่คือ OrderController::actionCreate ของ Yii2 (แนบโค้ด)
ฉันจะ refactor ส่วนคำนวณยอดรวมออกไปเป็น service
ช่วยเขียน characterization test ด้วย Codeception unit test
- ทดสอบพฤติกรรม "ตามที่โค้ดเป็นอยู่ตอนนี้" ห้ามแก้ไขสิ่งที่คิดว่าเป็นบั๊ก
- ครอบคลุม: ยอดต่ำกว่า 1000, เท่ากับ 1000 พอดี, มากกว่า 1000,
ยอดที่มีทศนิยม, สินค้าไม่มีในระบบ, สต็อกไม่พอ, รายการว่าง
- ถ้าพบพฤติกรรมที่น่าสงสัย ให้ระบุแยกไว้ท้ายคำตอบ ไม่ต้องแก้
เมื่อได้เทสต์มาแล้ว ให้รันกับโค้ดเดิมและต้องผ่านทั้งหมด ถ้ามีเทสต์ที่ไม่ผ่าน แปลว่าเทสต์เข้าใจพฤติกรรมผิด ให้แก้ที่เทสต์ ไม่ใช่แก้โค้ด จากนั้น commit เทสต์ชุดนี้ก่อนเริ่ม refactor
ขั้นที่ 2: แบ่งเป็นขั้นเล็ก ทีละการเปลี่ยนแปลง
แทนที่จะสั่ง "refactor controller นี้ให้ดี" ให้วางแผนเป็นขั้นเล็ก ๆ ที่แต่ละขั้นรันเทสต์ได้และ commit ได้:
- แยกการคำนวณยอดรวมออกเป็น method ใน service ใหม่ โดย controller เรียกใช้ service แทน
- แยกการตรวจสต็อกออกเป็น method ใน service เดียวกัน
- ย้ายการบันทึกคำสั่งซื้อและตัดสต็อกเข้า service พร้อมครอบด้วย transaction
- ให้ controller เหลือแค่รับ request เรียก service และ redirect
แต่ละขั้นให้รันเทสต์ ถ้าผ่านก็ commit ถ้าไม่ผ่านก็ย้อนกลับ ข้อดีคือถ้าพังก็พังในวงแคบ และ diff แต่ละรอบสั้นพอให้อ่านได้จริง
ขั้นที่ 3: บอก AI ชัด ๆ ว่าห้ามเปลี่ยนพฤติกรรม
prompt ที่ใช้สั่ง refactor ต้องมีข้อห้ามที่ชัดเจน เปรียบเทียบสองแบบ:
ไม่ดี:
ช่วย refactor OrderController ให้ clean และ best practice
ดี:
Refactor ขั้นที่ 1: ย้ายการคำนวณยอดรวม (loop + ส่วนลด 5% เมื่อยอด >= 1000)
ไปไว้ใน app\services\OrderPricingService::calculateTotal(array $items): float
กติกา:
- พฤติกรรมต้องเหมือนเดิมทุกประการ รวมถึงข้อความ exception และการปัดเศษ
- ห้ามแก้ไฟล์อื่นนอกจาก OrderController.php และไฟล์ service ใหม่
- ห้ามเปลี่ยน signature ของ action และห้ามจัด format ส่วนที่ไม่เกี่ยวข้อง
- ห้ามแก้เทสต์ ถ้าคิดว่าจำเป็นต้องแก้ ให้หยุดและอธิบายเหตุผล
- ถ้าเห็นบั๊ก ให้รายงานแยก ห้ามแก้ในรอบนี้
ข้อ "ห้ามแก้เทสต์" สำคัญมาก เพราะเมื่อเทสต์ไม่ผ่าน AI บางครั้งเลือกทางลัดคือแก้ค่าที่คาดหวังในเทสต์ให้ตรงกับผลลัพธ์ใหม่ ซึ่งทำให้ตาข่ายรองรับหายไปทันที
ตัวอย่างผลลัพธ์: จาก controller อ้วนสู่ service
หลังทำครบทุกขั้น service จะมีหน้าตาประมาณนี้:
<?php
namespace app\services;
use app\models\Product;
use yii\web\BadRequestHttpException;
class OrderPricingService
{
public const DISCOUNT_THRESHOLD = 1000;
public const DISCOUNT_RATE = 0.95;
/**
* คำนวณยอดรวมพร้อมส่วนลด พฤติกรรมคงเดิมจาก OrderController::actionCreate
* @param array<int, array{product_id: int|string, qty: int|string}> $items
* @throws BadRequestHttpException เมื่อรายการว่าง ไม่พบสินค้า หรือสต็อกไม่พอ
*/
public function calculateTotal(array $items): float
{
if (empty($items)) {
throw new BadRequestHttpException('ไม่มีสินค้า');
}
$total = 0;
foreach ($items as $item) {
$qty = (int) $item['qty'];
$product = Product::findOne((int) $item['product_id']);
if ($product === null || $product->stock < $qty) {
throw new BadRequestHttpException('สินค้าไม่พอ');
}
$total += $product->price * $qty;
}
if ($total >= self::DISCOUNT_THRESHOLD) {
$total = $total * self::DISCOUNT_RATE;
}
return round($total, 2);
}
}
และ controller จะบางลงเหลือแค่หน้าที่ของมัน:
public function actionCreate(OrderService $orders)
{
$items = Yii::$app->request->post('items', []);
$order = $orders->place(Yii::$app->user->id, $items);
return $this->redirect(['view', 'id' => $order->id]);
}
Yii2 รุ่นใหม่ ๆ รองรับการ inject service เข้า action ผ่าน DI container ได้ ถ้าเวอร์ชันที่ใช้อยู่ยังไม่รองรับ ก็สร้าง service ผ่าน Yii::createObject(OrderService::class) ภายใน action แทนได้
สังเกตว่าเราย้ายการคำนวณมาโดยไม่เปลี่ยนตรรกะเลย แม้จะมีจุดที่น่าปรับ เช่น การใช้ float กับเงิน ซึ่งควรเปลี่ยนเป็นการคำนวณแบบทศนิยมแม่นยำหรือเก็บเป็นสตางค์ แต่นั่นคือ "การเปลี่ยนพฤติกรรม" ต้องทำเป็นงานแยก มีเทสต์ใหม่ และสื่อสารกับทีม
Checklist ตรวจงาน refactor จาก AI
- characterization test ถูก commit ไว้ก่อนเริ่ม และผ่านกับโค้ดเดิม
- เทสต์ทั้งหมดยังผ่าน และไม่มีไฟล์เทสต์อยู่ใน diff ของขั้น refactor
- ข้อความ exception, HTTP status, รูปแบบ response และการปัดเศษเหมือนเดิม
- ไม่มี public method หรือ route ใดหายไปโดยไม่ได้ตรวจผู้เรียก (รวมถึงการเรียกผ่าน string)
- diff แต่ละ commit สั้นพอให้อ่านได้ในไม่กี่นาที
- บั๊กที่พบระหว่างทางถูกจดไว้เป็น issue แยก ไม่ได้ถูกแก้ปนมา
สรุป
- refactor คือเปลี่ยนโครงสร้างโดยพฤติกรรมเดิมทุกประการ AI มักละเมิดข้อนี้ถ้าเราไม่บอกชัด
- เขียน characterization test จับพฤติกรรมปัจจุบันก่อนเสมอ แม้พฤติกรรมนั้นจะดูแปลก
- แบ่งงานเป็นขั้นเล็ก รันเทสต์และ commit ทุกขั้น เพื่อให้ diff อ่านได้และย้อนกลับง่าย
- ใน prompt ให้ระบุไฟล์ที่แก้ได้ ห้ามแก้เทสต์ และให้รายงานบั๊กแยกแทนการแก้
- การปรับพฤติกรรมหรือแก้บั๊กที่เจอระหว่าง refactor ให้ทำเป็นงานแยกต่างหาก
ลองทำ
เลือก action หนึ่งในโปรเจกต์ Yii2 ของคุณที่ยาวเกิน 50 บรรทัด ให้ AI ช่วยเขียน characterization test อย่างน้อย 5 กรณี รันกับโค้ดเดิมให้ผ่านแล้ว commit จากนั้นให้ AI ย้ายตรรกะส่วนเดียว (เช่น การคำนวณหรือการตรวจเงื่อนไข) ออกไปเป็น service ตาม prompt แบบ "ดี" ข้างบน รันเทสต์อีกครั้ง แล้วนับดูว่า AI พยายามเปลี่ยนอะไรนอกเหนือคำสั่งบ้าง จดไว้เป็นข้อห้ามสำหรับ prompt รอบถัดไป
ความคิดเห็น (0)
ยังไม่มีความคิดเห็น — เริ่มคุยเป็นคนแรก ถามสิ่งที่สงสัย หรือแชร์ประสบการณ์ของคุณ