Banking System with Error Handling (pair programming : 100 คะแนน)

บทความโดย ผศ.ดร.ณัฐโชติ พรหมฤทธิ์
ภาควิชาคอมพิวเตอร์
คณะวิทยาศาสตร์
มหาวิทยาลัยศิลปากร

ให้นักศึกษา จับคู่ (Pair Programming) เพื่อร่วมกันพัฒนา Program บริหารจัดการธุรกรรมธนาคาร (Banking System) ในภาษา Go โดยมีการ แยก Code เป็น Package 2 Package ได้แก่

  • Package bank สำหรับเก็บ Code ส่วน Business Logic เกี่ยวกับบัญชีธนาคาร (เช่น Struct ต่าง ๆ Interface, Error Handling)
  • Package main File หลักสำหรับรัน Program และทดสอบ โดยเรียกใช้ Package bank

Program ต้องประกอบด้วยบัญชีหลายประเภท (เช่น บัญชีออมทรัพย์ บัญชีให้ดอกเบี้ย) พร้อมรองรับการจัดการ Error, Defer, Panic และ Recover อย่างเหมาะสม

รายละเอียดสำคัญที่ต้องมี

1. Account Interface (Abstraction + Polymorphism)
กำหนด Interface ชื่อ Account ที่มี 3 Method ได้แก่
- Deposit(amount float64) error
- Withdraw(amount float64) error
- GetBalance() float64

2. Encapsulation
- มี Struct สำหรับ “บัญชีออมทรัพย์” (savingsAccount)
Field ภายใน (balance) ขึ้นต้นด้วยตัวพิมพ์เล็ก (private field) และให้มี Method (public) สำหรับจัดการ

3. Struct Embedding (Composition Over Inheritance)
- สร้างอีก Struct สำหรับ “บัญชีที่มีดอกเบี้ย” (interestAccount) ด้วยการ Embed savingsAccount และเพิ่ม Field ใหม่ (interestRate) และ Method คำนวณดอกเบี้ย (AddInterest())

4. Polymorphism ผ่าน Account Interface
- Function Transfer(from, to Account, amount float64) error สำหรับโอนเงินระหว่างบัญชี
- Function PrintBalance(a Account) สำหรับพิมพ์ยอดเงินคงเหลือ

5. Error Handling
- เมื่อมีการฝาก/ถอนเป็นจำนวนไม่เหมาะสม (ฝาก/ถอนติดลบ หรือถอนเกินยอด) ให้ส่ง error กลับ
- มี Custom Error  1 ชนิด (InsufficientFundsError)

Function Error return String ดังต่อไปนี้

return fmt.Sprintf("ยอดเงินไม่เพียงพอในการถอน: ต้องการ %.2f แต่มีแค่ %.2f", e.Requested, e.Balance)

6. Panic และ Recover (จำลองสถานการณ์ Error ร้ายแรง)
- สร้าง Function RiskyBankOperation() ที่มีเงื่อนไขบางอย่างจะทำ panic
- ใช้ defer + recover() เพื่อจัดการเมื่อเกิด panic โดยยังให้ Program ทำงานต่อได้

7. Defer
ใช้ defer เพื่อสาธิตว่า Code ส่วนหนึ่งจะถูกเรียกก่อนที่ Function จะ return หรือในกรณีเกิด panic ใน RiskyBankOperation()

8. ใน main.go (package main)
- สร้างบัญชีต่าง ๆ (Savings / Interest)
- สาธิตการฝาก/ถอน/โอน
- ทดลอง Error (เช่น ถอนเกินยอด)
- ทดลอง RiskyBankOperation() ให้เกิด panic และใช้ recover()
- พิมพ์ผลลัพธ์และ Error ที่เกิดขึ้นให้ชัดเจน

การ Pair Programming

  • วางแผนร่วมกัน ตกลงกันว่าจะสร้าง Function อะไรบ้าง และจะแยก File/PAckage อย่างไร
  • ทำโครงสร้าง bank.go และ main.go ที่จะใช้
  • สลับบทบาท Driver/Navigator
    Driver คนพิมพ์ Code
    Navigator คนแนะนำ/ตรวจสอบ Code
    สลับกันทุก ๆ 10 นาที
  • ทดสอบกับไฟล์ Unit Test ที่กำหนดต่อไปนี้ ด้วยคำสั่ง go test -v ./bank
// bank_test.go
package bank

import (
	"bytes"
	"io"
	"os"
	"strings"
	"testing"
)

// ทดสอบการสร้างบัญชีออมทรัพย์ (savingsAccount) และการใช้งานเบื้องต้น
func TestSavingsAccount(t *testing.T) {
	acc := NewSavingsAccount(1000)

	// ทดสอบ GetBalance() หลังสร้าง
	if acc.GetBalance() != 1000 {
		t.Errorf("expected balance = 1000, but got %v", acc.GetBalance())
	}

	// ทดสอบฝากเงิน (Deposit)
	err := acc.Deposit(500)
	if err != nil {
		t.Errorf("unexpected error on deposit: %v", err)
	}
	if acc.GetBalance() != 1500 {
		t.Errorf("expected balance = 1500 after deposit, but got %v", acc.GetBalance())
	}

	// ทดสอบฝากเงินไม่ถูกต้อง (Deposit <= 0)
	err = acc.Deposit(0)
	if err == nil {
		t.Error("expected error when depositing 0, but got nil")
	}

	// ทดสอบถอนเงิน (Withdraw)
	err = acc.Withdraw(1000)
	if err != nil {
		t.Errorf("unexpected error on withdraw: %v", err)
	}
	if acc.GetBalance() != 500 {
		t.Errorf("expected balance = 500 after withdraw, but got %v", acc.GetBalance())
	}

	// ทดสอบถอนเงินเกินยอด (InsufficientFundsError)
	err = acc.Withdraw(600)
	if err == nil {
		t.Error("expected error on insufficient funds, but got nil")
	} else {
		// เช็คว่าเป็นประเภท InsufficientFundsError หรือไม่
		if _, ok := err.(*InsufficientFundsError); !ok {
			t.Errorf("expected InsufficientFundsError, but got %T", err)
		}
	}
}

// ทดสอบการสร้างบัญชีดอกเบี้ย (interestAccount)
func TestInterestAccount(t *testing.T) {
	intAcc := NewInterestAccount(2000, 0.05)

	// ทดสอบค่าเริ่มต้น
	if intAcc.GetBalance() != 2000 {
		t.Errorf("expected balance = 2000, but got %v", intAcc.GetBalance())
	}

	// ทดสอบ AddInterest()
	intAcc.AddInterest()
	// ดอกเบี้ย 5% ของ 2000 = 100
	if intAcc.GetBalance() != 2100 {
		t.Errorf("expected balance = 2100 after interest, but got %v", intAcc.GetBalance())
	}

	// ทดสอบ Withdraw (สังเกตว่าจะมีข้อความ "Withdrawing from interest account...")
	err := intAcc.Withdraw(100)
	if err != nil {
		t.Errorf("unexpected error on withdraw from interest account: %v", err)
	}
	if intAcc.GetBalance() != 2000 {
		t.Errorf("expected balance = 2000 after withdraw, but got %v", intAcc.GetBalance())
	}
}

// ทดสอบฟังก์ชันโอนเงิน (Transfer)
func TestTransfer(t *testing.T) {
	from := NewSavingsAccount(1000)
	to := NewSavingsAccount(500)

	// โอน 300 จาก from -> to
	err := Transfer(from, to, 300)
	if err != nil {
		t.Errorf("unexpected error on transfer: %v", err)
	}
	if from.GetBalance() != 700 {
		t.Errorf("expected from-balance = 700, but got %v", from.GetBalance())
	}
	if to.GetBalance() != 800 {
		t.Errorf("expected to-balance = 800, but got %v", to.GetBalance())
	}

	// ทดสอบโอนเกินยอด (ให้เกิด InsufficientFundsError)
	err = Transfer(from, to, 2000) // from มีแค่ 700
	if err == nil {
		t.Error("expected error on transfer exceeding balance, but got nil")
	} else {
		if _, ok := err.(*InsufficientFundsError); !ok {
			t.Errorf("expected InsufficientFundsError, but got %T", err)
		}
	}
}

// ทดสอบ RiskyBankOperation (Panic & Recover)
// วิธีทดสอบพฤติกรรม panic ใน Unit Test
func TestRiskyBankOperation(t *testing.T) {
	// 1) กรณีที่ไม่เกิด Panic
	// (เราจะเรียกฟังก์ชันให้ทำงาน ถ้าไม่มี panic, ทดสอบผ่าน)
	defer func() {
		if r := recover(); r != nil {
			t.Errorf("did not expect a panic, but got one: %v", r)
		}
	}()
	RiskyBankOperation(100)
}

// ทดสอบกรณีเกิด Panic จริง ๆ
// ทดสอบกรณีฟังก์ชัน Recover ภายในตัวเอง (panic ไม่หลุดมาถึง test)
func TestRiskyBankOperationPanic(t *testing.T) {
	// 1) สำรอง stdout ปัจจุบันไว้ เพื่อจะได้คืนค่าทีหลัง
	originalStdout := os.Stdout

	// 2) สร้าง Pipe เพื่อดักจับ output
	r, w, _ := os.Pipe()
	os.Stdout = w // เปลี่ยนให้ stdout ชี้ไปที่ตัว Pipe

	// 3) เรียกฟังก์ชันที่มีการ recover ภายใน
	//    โดยใส่ค่าเป็นลบ (ซึ่งจะ panic แล้ว recover ในตัวเอง)
	RiskyBankOperation(-50)

	// 4) ปิด writer และคืนค่า stdout กลับ
	w.Close()
	os.Stdout = originalStdout

	// 5) อ่านข้อมูลจากตัว Pipe (r)
	var buf bytes.Buffer
	io.Copy(&buf, r)
	output := buf.String()

	// 6) ตรวจสอบข้อความที่คาดว่าจะเกิดขึ้น
	if !strings.Contains(output, "เกิด Panic แล้วทำการ Recover สำเร็จ:") {
		t.Errorf("Expected output to contain \"เกิด Panic แล้วทำการ Recover สำเร็จ:\", but got:\n%s", output)
	}

	// (Optional) ตรวจสอบว่าไม่หลุด crash มาถึง Test
	// ถ้ามี panic หลุดมาถึง Test จริง ๆ Test จะไม่มาถึงบรรทัดนี้ เพราะจะ Fail ก่อน
}
bank_test.go

โครงสร้าง Project ที่ควรจะมี

.
├── bank
│   ├── bank.go
│   └── bank_test.go
├── banking
├── go.mod
└── main.go