ข้ามไปยังเนื้อหาหลัก
ProgrammerThailand

AI & Vibe Coding

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

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 ได้:

  1. แยกการคำนวณยอดรวมออกเป็น method ใน service ใหม่ โดย controller เรียกใช้ service แทน
  2. แยกการตรวจสต็อกออกเป็น method ใน service เดียวกัน
  3. ย้ายการบันทึกคำสั่งซื้อและตัดสต็อกเข้า service พร้อมครอบด้วย transaction
  4. ให้ 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)

ยังไม่มีความคิดเห็น — เริ่มคุยเป็นคนแรก ถามสิ่งที่สงสัย หรือแชร์ประสบการณ์ของคุณ