การเขียนโปรแกรมแบบ Data-Oriented Programming (DOP) คือ แนวทางการเขียนโปรแกรมที่มุ่งเน้นการจัดวางโครงสร้างข้อมูลและอัลกอริธึมที่ดำเนินการบน data structures นั้นให้มีประสิทธิภาพสูงสุดเมื่อการเข้าถึงและการประมวลผลข้อมูลจำนวนมาก โดยเน้นการแยกส่วนของข้อมูลและโค้ดออกจากกัน
4 หลักการหลักของ DOP ได้แก่
- แยกโค้ดจากข้อมูล
- นำเสนอข้อมูลด้วย Generic Data Structure
- ห้ามแก้ไขข้อมูล
- แยก data schema จาก Data representation
ตัวอย่างโค้ด JavaScript
const sqlite3 = require('sqlite3').verbose();
// Immutable data structures
const createBook = (id, title, author) => Object.freeze({id, title, author});
const createLibrary = (name, books) => Object.freeze({name, books});
// Database operations
const initDB = () => {
return new Promise((resolve, reject) => {
const db = new sqlite3.Database('./library.db', (err) => {
if (err) reject(err);
db.run(`CREATE TABLE IF NOT EXISTS books (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT,
author TEXT
)`, (err) => {
if (err) reject(err);
resolve(db);
});
});
});
};
// Generic data manipulation functions
const addBook = (db, title, author) => {
return new Promise((resolve, reject) => {
db.run('INSERT INTO books (title, author) VALUES (?, ?)', [title, author], function(err) {
if (err) reject(err);
resolve(createBook(this.lastID, title, author));
});
});
};
const removeBook = (db, id) => {
return new Promise((resolve, reject) => {
db.run('DELETE FROM books WHERE id = ?', [id], (err) => {
if (err) reject(err);
resolve();
});
});
};
const getAllBooks = (db) => {
return new Promise((resolve, reject) => {
db.all('SELECT * FROM books', (err, rows) => {
if (err) reject(err);
resolve(rows.map(row => createBook(row.id, row.title, row.author)));
});
});
};
// Behavior (pure functions that don't modify data directly)
const displayLibrary = (library) => {
console.log(`Library: ${library.name}`);
library.books.forEach(book => {
console.log(`- ${book.title} by ${book.author} (ID: ${book.id})`);
});
};
// Main execution
async function main() {
try {
const db = await initDB();
// Add initial books
await addBook(db, "1984", "George Orwell");
await addBook(db, "To Kill a Mockingbird", "Harper Lee");
// Get all books and display library
let books = await getAllBooks(db);
let library = createLibrary("City Library", books);
console.log("Initial Library:");
displayLibrary(library);
// Add a new book
const newBook = await addBook(db, "The Great Gatsby", "F. Scott Fitzgerald");
library = createLibrary(library.name, [...library.books, newBook]);
console.log("\nAfter adding a book:");
displayLibrary(library);
// Remove a book
await removeBook(db, 1);
// Get updated books and display library
books = await getAllBooks(db);
library = createLibrary(library.name, books);
console.log("\nAfter removing a book:");
displayLibrary(library);
db.close();
} catch (error) {
console.error("An error occurred:", error);
}
}
main();
จากตัวอย่าง:
-
Immutable Data: ใช้
Object.freeze()
เพื่อสร้าง immutable objects ให้ไม่สามารถแก้ไขได้ สำหรับสร้าง books และ library. -
Separation of Data and Behavior: ข้อมูล (
books
และlibrary
) แยกจาก functions ที่จะประมวลผล. - Generic Data Structures: ใช้ชนิดข้อมูลในการนำเสนอง่ายๆอย่าง objects และ array
-
Data Manipulation Functions: ฟังก์ชั่นต่างๆ
addBook
,removeBook
และfindBook
ทำงานกับข้อมูลโดยที่ไม่มีการแก้ไขกับข้อมูลโดยตรงเพียงรับค่ามาแลส่งต่อ - Pure Functions: ทุกฟังก์ชั่นมีความเพียวไม่มี side effects และคืนค่าข้อมูลใหม่อยู่เสมอแทนการแก้ไขข้อมูล
-
Centralized Data:
libraryData
เป็นตัวแปรที่ทำงานกับ central data store ในตัวอย่างคือ sqlite ทำการแก้ไขค่าใหม่ด้วยการคืนค่าผลลัพธ์จาก pure functions.
ตัวอย่างภาษา go
package main
import (
"database/sql"
"fmt"
"log"
_ "github.com/mattn/go-sqlite3"
)
// Immutable data structures
type Book struct {
ID int
Title string
Author string
}
type Library struct {
Name string
Books []Book
}
// Database operations
func initDB(dbPath string) (*sql.DB, error) {
db, err := sql.Open("sqlite3", dbPath)
if err != nil {
return nil, err
}
_, err = db.Exec(`
CREATE TABLE IF NOT EXISTS books (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT,
author TEXT
)
`)
if err != nil {
return nil, err
}
return db, nil
}
// Generic data manipulation functions
func addBook(db *sql.DB, title, author string) (Book, error) {
result, err := db.Exec("INSERT INTO books (title, author) VALUES (?, ?)", title, author)
if err != nil {
return Book{}, err
}
id, err := result.LastInsertId()
if err != nil {
return Book{}, err
}
return Book{ID: int(id), Title: title, Author: author}, nil
}
func removeBook(db *sql.DB, id int) error {
_, err := db.Exec("DELETE FROM books WHERE id = ?", id)
return err
}
func getAllBooks(db *sql.DB) ([]Book, error) {
rows, err := db.Query("SELECT id, title, author FROM books")
if err != nil {
return nil, err
}
defer rows.Close()
var books []Book
for rows.Next() {
var b Book
err := rows.Scan(&b.ID, &b.Title, &b.Author)
if err != nil {
return nil, err
}
books = append(books, b)
}
return books, nil
}
// Behavior (pure functions that don't modify data directly)
func displayLibrary(library Library) {
fmt.Printf("Library: %s\n", library.Name)
for _, book := range library.Books {
fmt.Printf("- %s by %s (ID: %d)\n", book.Title, book.Author, book.ID)
}
}
func main() {
db, err := initDB("library.db")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// Add books
_, err = addBook(db, "1984", "George Orwell")
if err != nil {
log.Fatal(err)
}
_, err = addBook(db, "To Kill a Mockingbird", "Harper Lee")
if err != nil {
log.Fatal(err)
}
// Get all books and display library
books, err := getAllBooks(db)
if err != nil {
log.Fatal(err)
}
library := Library{Name: "City Library", Books: books}
fmt.Println("Initial Library:")
displayLibrary(library)
// Add a new book
newBook, err := addBook(db, "The Great Gatsby", "F. Scott Fitzgerald")
if err != nil {
log.Fatal(err)
}
library.Books = append(library.Books, newBook)
fmt.Println("\nAfter adding a book:")
displayLibrary(library)
// Remove a book
err = removeBook(db, 1)
if err != nil {
log.Fatal(err)
}
// Get updated books and display library
library.Books, err = getAllBooks(db)
if err != nil {
log.Fatal(err)
}
fmt.Println("\nAfter removing a book:")
displayLibrary(library)
}
จากตัวอย่างรูปแบบการเขียนโค้ดแบบ DOP จะเป็นการประยุกต์ใช้ functional programming เข้ามาช่วยจัดระเบียนการเขียนโค้ดโดยแยกส่วนของ Data กับ Behaviour ออกจากกัน มีข้อดีที่เห็นได้ชัดจากโค้ดตัวอย่าง คือ
ข้อดี
-
มีความยืดหยุ่น เมื่อต้องการเปลี่ยนฐานข้อมูลจาก Sqlite ก็ฐานข้อมูลอื่นที่รองรับภาษา SQL ก็เปลี่ยนในส่วนของออบเจค db ได้เลย ในส่วนของ golang เปลี่ยนที่
import _ "github.com/mattn/go-sqlite3"
ได้เลย แต่ตัวอย่าง JavaScript จะยังไม่ได้ออกแบบให้ยืดหยุ่น ยังต้องเปลี่ยนหลายจุด แต่จะเห็นว่าการออกแบบโค้ดแบบนี้ ช่วยให้มีระเบียบขึ้นมาก - สามารถทดสอบได้ง่ายขึ้นมาก สามารถนำไปเขียนเทสได้ง่าย ทดสอบได้ง่าย เพราะแต่ละฟังก์ชั่น รับ input และ return เป็น data ที่สามารถเขียนโค้ดเตรียมข้อมูลและทำ assert ตรวจคำตอบได้ง่าย
- นำกลับมาใช้ใหม่ สามารถนำกลับมาใช้หรือประกอบร่างเป็นฟังก์ชั่นใหม่ได้ง่าย
- เพิ่ม Productivity มี pattern ไม่ซับซ้่อนมากเมื่อต้องทำงานลักษณะคล้ายกันหรือร่วมกับทีม จะสร้างลายมือเหมือนๆกันทั้งทีมได้ง่าย อ่านโค้ดได้ง่าย เขียนไปในแนวทางเดียวกัน
- ลด Side effect มั่นใจได้ว่าโค้ดทำงานถูกต้องไม่ถูกเปลี่ยนแปลงค่าจากฟังก์ชั่นอื่นๆ
ข้อเสีย
- Performance overhead ทุกการสร้าง immutable objects ใหม่จะเพิ่มการใช้งาน memory ขึ้นอยู่กับความสามารถของแต่ละภาษาในการจัดการหน่วยความจำในส่วนนี้ บางภาษาอาจจะไม่ได้กระทบ
- มีความซับซ้อนในบาง Scenarios อาจจะไม่เหมาะ รูปแบบของ DOP ค่อนข้างเหมาะกับงานที่ทำงานร่วมกับ Data Source, Data Store โดยตรงที่ีมีการประมวลให้เข้ากับโครงสร้างข้อมูล ในบางแอพพลิเคชั่นที่มีการออกแบบซับซ้อนและโครงสร้างไม่เหมือนกับ Data Source โดยตรงอย่างโปรแกรมแบบ OOP อาจจะทำได้ยากและเพิ่มความซับซ้อนเกินความจำเป็น
- Potential for data inconsistency การทำ immutable เพื่มลบโดยห้ามแก้ไขข้อมูล ไม่ได้เหมาะกับ Traditional Database ที่ใช้กันอยู่่ จำเป็นต้องเปลี่ยนวิธีคิดหรือใช้ฐานข้อมูลที่เหมาะกับ immutable หากอยากให้แนวทางของโค้ดและฐานข้อมูลสอดคล้องกัน หากโค้ดและดาต้าแนวคิดไม่สอดคล้องกันจะทำให้ข้อมูลไม่ Consistency
- Difficulty in representing stateful objects: อาจจะทำให้ทำงานร่วมกับระบบอื่นที่เป็น Stateful ได้ยาก
- Potential overuse of generic data structures: ใช้ Generic data มากเกินไป อาจจะไม่ใช่ทุกงานที่ต้องใช้แค่ Generic data เสมอไป บางงานที่ต้องการความ dynamic มาก อาจจะไม่เหมาะ
ในทุกๆ Pattern ก็มีข้อดีข้อเสียแตกต่างกันไป เลือกใช้ที่คิดว่าเหมาะกับงานและแก้ปัญหาให้ได้โดยไม่เพิ่มปัญหา เท่าที่ดูแล้ว DOP เหมาะกับงานที่ต้องเขียนโปรแกรมแล้วแต่โครงสร้างข้อมูลที่ต้องยุ่งเกี่ยวกับ data store โดยตรง เหมาะกับงาน data platform , open data น่านำไปประยุกต์ใช้ทีเดียว
Top comments (0)