Data Oriented Programming คืออะไร ?
Tags: webdev, programming
Published: 2024-07-14 12:21:15

การเขียนโปรแกรมแบบ Data-Oriented Programming (DOP) คือ  แนวทางการเขียนโปรแกรมที่มุ่งเน้นการจัดวางโครงสร้างข้อมูลและอัลกอริธึมที่ดำเนินการบน data structures นั้นให้มีประสิทธิภาพสูงสุดเมื่อการเข้าถึงและการประมวลผลข้อมูลจำนวนมาก โดยเน้นการแยกส่วนของข้อมูลและโค้ดออกจากกัน

## 4 หลักการหลักของ DOP ได้แก่

1. แยกโค้ดจากข้อมูล
2. นำเสนอข้อมูลด้วย Generic Data Structure
3. ห้ามแก้ไขข้อมูล
4. แยก data schema จาก Data representation


![DOP Principle](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ctzpajh054g3fqq45zm4.png)

## ตัวอย่างโค้ด JavaScript

```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

```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 ออกจากกัน มีข้อดีที่เห็นได้ชัดจากโค้ดตัวอย่าง คือ
### ข้อดี
1. **มีความยืดหยุ่น** เมื่อต้องการเปลี่ยนฐานข้อมูลจาก Sqlite ก็ฐานข้อมูลอื่นที่รองรับภาษา SQL ก็เปลี่ยนในส่วนของออบเจค db ได้เลย ในส่วนของ golang เปลี่ยนที่ `import _ "github.com/mattn/go-sqlite3"` ได้เลย แต่ตัวอย่าง JavaScript จะยังไม่ได้ออกแบบให้ยืดหยุ่น ยังต้องเปลี่ยนหลายจุด แต่จะเห็นว่าการออกแบบโค้ดแบบนี้ ช่วยให้มีระเบียบขึ้นมาก
2. **สามารถทดสอบได้ง่ายขึ้นมาก** สามารถนำไปเขียนเทสได้ง่าย ทดสอบได้ง่าย เพราะแต่ละฟังก์ชั่น รับ input และ return เป็น data  ที่สามารถเขียนโค้ดเตรียมข้อมูลและทำ assert ตรวจคำตอบได้ง่าย
3. **นำกลับมาใช้ใหม่** สามารถนำกลับมาใช้หรือประกอบร่างเป็นฟังก์ชั่นใหม่ได้ง่าย
4. **เพิ่ม Productivity** มี pattern ไม่ซับซ้่อนมากเมื่อต้องทำงานลักษณะคล้ายกันหรือร่วมกับทีม จะสร้างลายมือเหมือนๆกันทั้งทีมได้ง่าย อ่านโค้ดได้ง่าย เขียนไปในแนวทางเดียวกัน
5. **ลด Side effect** มั่นใจได้ว่าโค้ดทำงานถูกต้องไม่ถูกเปลี่ยนแปลงค่าจากฟังก์ชั่นอื่นๆ

### ข้อเสีย
1. **Performance overhead** ทุกการสร้าง immutable objects ใหม่จะเพิ่มการใช้งาน memory ขึ้นอยู่กับความสามารถของแต่ละภาษาในการจัดการหน่วยความจำในส่วนนี้ บางภาษาอาจจะไม่ได้กระทบ
2. **มีความซับซ้อนในบาง Scenarios อาจจะไม่เหมาะ** รูปแบบของ DOP ค่อนข้างเหมาะกับงานที่ทำงานร่วมกับ Data Source, Data Store โดยตรงที่ีมีการประมวลให้เข้ากับโครงสร้างข้อมูล ในบางแอพพลิเคชั่นที่มีการออกแบบซับซ้อนและโครงสร้างไม่เหมือนกับ Data Source โดยตรงอย่างโปรแกรมแบบ OOP อาจจะทำได้ยากและเพิ่มความซับซ้อนเกินความจำเป็น
3. **Potential for data inconsistency** การทำ immutable เพื่มลบโดยห้ามแก้ไขข้อมูล ไม่ได้เหมาะกับ  Traditional Database ที่ใช้กันอยู่่ จำเป็นต้องเปลี่ยนวิธีคิดหรือใช้ฐานข้อมูลที่เหมาะกับ immutable หากอยากให้แนวทางของโค้ดและฐานข้อมูลสอดคล้องกัน หากโค้ดและดาต้าแนวคิดไม่สอดคล้องกันจะทำให้ข้อมูลไม่ Consistency
4. **Difficulty in representing stateful objects:** อาจจะทำให้ทำงานร่วมกับระบบอื่นที่เป็น Stateful ได้ยาก
5. **Potential overuse of generic data structures:**  ใช้ Generic data มากเกินไป อาจจะไม่ใช่ทุกงานที่ต้องใช้แค่ Generic data เสมอไป บางงานที่ต้องการความ dynamic มาก อาจจะไม่เหมาะ

ในทุกๆ Pattern ก็มีข้อดีข้อเสียแตกต่างกันไป เลือกใช้ที่คิดว่าเหมาะกับงานและแก้ปัญหาให้ได้โดยไม่เพิ่มปัญหา เท่าที่ดูแล้ว DOP เหมาะกับงานที่ต้องเขียนโปรแกรมแล้วแต่โครงสร้างข้อมูลที่ต้องยุ่งเกี่ยวกับ data store โดยตรง เหมาะกับงาน data platform , open data น่านำไปประยุกต์ใช้ทีเดียว