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 ก่อน
}
โครงสร้าง Project ที่ควรจะมี
.
├── bank
│ ├── bank.go
│ └── bank_test.go
├── banking
├── go.mod
└── main.go