ตัวอย่างโปรแกรม MVC web application
Table of Content
- MVC Web application in NodeJS
- โค้ดเขียนพอให้ใช้ได้ใน 3 ชั่วโมง ดังนั้น not ready for production
- อ่านให้จบ
- มีปัญหาอะไรเกิดขึ้น หรืออยากแก้ไข เปิด issues, pull request มาได้เต็มที่
Code นี้ implement โจทย์ข้อธนาคารในการสอบ Exit Exam @CSKMITL ประจำปีการศึกษา 2561 โดยโจทย์กำหนดให้มีบัญชีธนาคาร 3 บัญชี โดยเจ้าของบัญชีสามารถ login โดยใช้เลขที่บัญชี, Passcode เพื่อทำรายการต่างๆ 3 อย่างได้แก่
- การฝากเงิน
- การถอนเงิน
- การโอนเงินข้ามบัญชี
โดยออกแบบหน้าจอให้เหมาะสม
- Runtime: NodeJS
- Language: TypeScript
- Web framework: routing-controllers with Koa.js (Koa.js)
- View template: lit-ntml
- CSS, Front-End Component: Bootstrap
- Database helper: knexjs
- Visual Studio Code with extensions
- TSLint สำหรับ TypeScript linting
- lit-plugin สำหรับ lit-ntml syntax hilighting & completion
- NodeJS LTS version
- Yarn package manager
- ติดตั้ง module
typescript
และtslint
เป็น global module ผ่าน command line> yarn add -g typescript tslint
- เปิด command prompt ที่โฟลเดอร์โปรแกรม หรือเปิดโฟลเดอร์โปรแกรมด้วย vscode แล้วเปิด integrated command prompt ขึ้นมา
- รันคำสั่ง
รอจน yarn รันเสร็จ
> yarn
- รันคำสั่ง
> yarn dev
โปรแกรมจะรันโดยใช้ port ที่ตั้งค่าไว้ในไฟล์ .env (ค่าเริ่มต้นคือ 8080)
- src: โค้ดโปรแกรม
- assets: bootstrap, js, css ของหน้าเว็บ
- controllers: controllers
- core: คลาสหลักๆ ของ boilerplate
- models: model class / entity class
- repository: ส่วนติดต่อ data source
- services: service layer
- views: html template
- index.ts: ไฟล์เริ่มต้นการทำงาน
- .env: ไฟล์ config
MVC (Model-View-Controller) เป็น design pattern รูปแบบหนึ่งที่ใช้กับโปรแกรมที่มี GUI เป็นหลัก โดยแยกโค้ดออกเป็นส่วนๆ ตามหน้าที่ที่รับผิดชอบ (Separation of Concern: SoC)
โดยหลักการคิดในการออกแบบโปรแกรมตามโครงสร้าง MVC จะประกอบไปด้วย 3 ขั้นตอนหลักๆ คือ
- How you model your data (Model โมเดลข้อมูล)
- How you display your data (View การแสดงผล)
- How you interact with your data (Controller เรียก/เปลี่ยนแปลงข้อมูลจาก model แล้ว update view ส่งให้ user ตาม action)
- เริ่มต้นด้วย user เรียก request มาทาง web browser ตาม route ที่ระบุ
- จากนั้น controller จะทำงานตาม function ที่ระบุ route ไว้
- โดย controller จะเรียกหรือเปลี่ยนแปลงข้อมูลจาก model ที่ต้องการ
- เมื่อได้ข้อมูลก็จะส่งไปให้ view render จนได้หน้า HTML แล้วจึง response กลับไปให้ user
ส่วนที่มักจะสับสนกันคือ model จริงๆ มันเป็นอะไรกันแน่ เพราะหลายบทความบอกไว้ว่า model เป็นส่วนที่ใช้ติดต่อกับ data source แต่ใน code หลายๆ ตัวอย่าง model เป็นแค่ class โง่ๆ
class โง่ๆ จริงๆ แล้วเรียกว่า Entity model (หรือเรียกว่า Entity class ก็ได้) เป็น class ที่หน้าตาเหมือนกับ entity อาจจะมีการเขียน action คลุมไว้ได้บ้างอย่างเช่น การถอนเงินออกจากบัญชีห้ามเกินจากจำนวนที่มีอยู่ เราอาจจะเขียน method .withdraw(amount)
ไว้ใน BankAccount
object เพื่อใม่ให้โปรแกรมเมอร์พลาด ซึ่งจะมี layer บางๆ ที่เรียกว่า Data Access Layer (DAL) เช่น Repository เพื่อสร้างหรือ update ข้อมูลใน data source จาก entity model อีกทีหนึ้ง
บาง framework เช่น laravel นั้น entity model จะเป็นแบบ active record สามารถใช้ในการ select, insert, update, delete ได้เลย เป็น DAL ในตัวเอง
ดังนั้น คำว่า model ใน MVC หมายถึง Entity class รวมถึง DAL เลย
แล้วแต่ ประเภทของ logic
- ถ้าเกี่ยวช้องกับ domain model เช่น
BankAccount
ไม่สามารถถอนเงินออกได้มากกว่าจำนวนเงินที่มีอยู่ logic นี้จะอยู่ที่ Model - ถ้าเกี่ยวข้องกับการแสดงผล เช่นถ้า
a.b == 2
จะต้องแสดงปุ่ม OK logic นี้จะอยู่ที่ View - ถ้าเกี่ยวข้องกับ flow การทำงานของโปรแกรม เช่น ถ้าตรวจสอบแล้ว user ระบุ id, passcode ถูกต้อง ให้ generate token แล้ว set cookie กลับไปด้วย logic นี้ควรอยู่ที่ controller
ไม่ตายตัว แต่ถ้าเมื่อไหร่ logic เริ่มซับซ้อนมากขึ้น เช่น
public async transfer(token: string, toAccountId: string, transferAmount: number) {
const account = await this.validateToken(token);
const toAccount = await this.findAccountById(toAccountId);
if (account.accountId === toAccount.accountId) {
throw {
message: "Cannot transfer to the origin account",
};
}
if (account.balance < transferAmount) {
throw {
message: "Insufficient balance.",
};
}
account.balance -= transferAmount;
toAccount.balance += transferAmount;
this._bankAccountRepository.updateAccount(account);
this._bankAccountRepository.updateAccount(toAccount);
this.recordTransaction(account, transferAmount, `Transfer to account ${toAccount.accountId}`);
this.recordTransaction(toAccount, transferAmount, `Transfer from account ${account.accountId}`);
}
ตามตัวอย่าง กำหนดให้ก่อนทำการโอนต้องตรวจสอบบัญชีปลายทางว่ามีอยู่จริง และยอดเงินที่ต้องการโอนจะต้องน้อยกว่าหรือเท่ากับบัญชีปลายทาง หลังจากโอนเงินเรียกร้อยให้ทำการบันทึกรายการลงไปด้วยทั้งสองบัญชี
ดังนั้นไม่มีอะไรตายตัว แต่ถ้าความซับซ้อนเริ่มมากขึ้นก็ควรแยกออกมาเพื่อไม่ให้ model และ controller รกเกินไป