Data Oriented Programming คืออะไร ?

devmountaintechfest - Jul 14 - - Dev Community

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

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

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

DOP Principle

ตัวอย่างโค้ด 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();
Enter fullscreen mode Exit fullscreen mode

จากตัวอย่าง:

  • 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)
}
Enter fullscreen mode Exit fullscreen mode

จากตัวอย่างรูปแบบการเขียนโค้ดแบบ 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 น่านำไปประยุกต์ใช้ทีเดียว

. . . . . . . . .