Object Pool Pattern

Giới thiệu

Chúng ta đều quen thuộc với singleton - một đối tượng một khi đã được khởi trị sẽ tồn tại suốt vòng đời chương trình. Tuy nhiên, trong một số trường hợp, chúng ta cần khởi tạo và sử dụng một tập hợp các đối tượng, mà với số lượng lớn, thì việc khởi tạo nhiều lần sẽ gây lãng phí không cần thiết, trong những trường hợp như vậy, chúng ta dùng object pool pattern

Khái niệm

Chúng ta dùng Object Pool Pattern quản lý một tập hợp các objects mà sẽ được tái sử dụng trong chương trình. Chúng được gọi ra từ pool, sử dụng trong một khoảng thời gian nhất định rồi trả về pool. Trong khoảng thời gian vắng mặt đó của object, không thành phần nào có thể sử dụng tận khi nó được quay trở về pool.

Object Pool Pattern được coi là sử dụng thích hợp khi có nhiều hơn một đối tượng và số đối tượng được khởi tạo là hạn chế.

Ví dụ trong iOS

Trong iOS, chúng ta đều đã quen thuộc với tableView, và đều biết cell trong tableview có thể reuse, về bản chất chính là dùng Object Pool Pattern.

Nội dung

Một OPP có 4 hành động quan trọng, bao gồm:

  1. Khởi tạo --- Một tập hợp các đối tượng được tạo ra
  2. Xuất --- Lấy một đối tượng ra khỏi pool trong một khoảng thời gian nhất định
  3. Sử dụng --- Sau khi lấy đối tượng ra khỏi pool, đối tượng sẽ được sử dụng cho những mục đích cụ thể
  4. Nhập --- Đối tượng được trả lại pool

4 hành động trên được thể hiện cụ thể qua generic class gọi là Pool, class này có hàm init khởi trị, hàm checkOut để xuất đối tượng khỏi pool, hàm checkIn để trả đối tượng về pool sau khi sử dụng xong. Cụ thể như sau:

class Pool<T> {
private var data = [T]()
private let arrayQ = dispatch_queue_create("arrayQ", DISPATCH_QUEUE_SERIAL); private let semaphore:dispatch_semaphore_t

    init(items:[T]) {
        data.reserveCapacity(data.count)
         for item in items {
            data.append(item)
        }
        semaphore = dispatch_semaphore_create(items.count)
    }
    
    func getFromPool() -> T? {
        var result:T?
        if (dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER) == 0) {
            dispatch_sync(arrayQ, {() in
                result = self.data.removeAtIndex(0)
            })
        }
        return result
    }
    
    func returnToPool(item:T) {
        dispatch_async(arrayQ, {() in
            self.data.append(item)
            dispatch_semaphore_signal(self.semaphore)
        })
    }
}

Trong đoạn code trên, chúng ta tạo một serial queue gọi là arrayQ để chứa các hành động xuất - nhập object, việc dùng serial queue đảm bảo rằng các hành động xuất - nhập sẽ luôn được thực hiện trên một thread, và điều này tránh được việc 2 thread cùng thay đổi giá trị của mảng chứa các objects ở cùng một thời điểm, nguyên nhân khiến app bị crash.

Đem áp dụng pool trên vào ứng dụng quản lý thư viện, nơi mà các đầu sách thường xuyên được xuất - nhập (mượn - trả), ta có:

final class Library {
    private let books:[Book]
    private let pool:Pool<Book>
    
    private init(stockLevel:Int) {
        books = [Book]()
        for count in 1 ... stockLevel {
            books.append(Book(author: "Dickens, Charles", title: "Hard Times",
                stock: count))
        }
        pool = Pool<Book>(items:books);
    }
    
    private class var singleton:Library {
        struct SingletonWrapper {
            static let singleton = Library(stockLevel:2);
        }
        return SingletonWrapper.singleton;
    }
    
    class func checkoutBook(reader:String) -> Book? {
        var book = singleton.pool.getFromPool()
        book?.reader = reader
        book?.checkoutCount += 1
        return book
    }

     class func returnBook(book:Book) {
        book.reader = nil
        singleton.pool.returnToPool(book)
    }
    
    class func printReport() {
        for book in singleton.books {
            println("...Book#\(book.stockNumber)...")
            println("Checked out \(book.checkoutCount) times")
            if (book.reader != nil) {
                println("Checked out to \(book.reader!)")
            } else {
                println("In stock")
            }
        }
    }
}

Trong đoạn code trên, chúng ta tạo đối tượng Libary quản lý thư viện, trong đó có chứa một mảng cách đầu sách (books) và một pool. Vì đối tượng thư viện sẽ được dùng chung toàn ứng dụng nên chúng ta khởi tạo nó như một singleton. Library có các hàm init() với stockLevel được coi như số bản copy của một quyển sách, checkoutBook(😃, returnBook() tương ứng với các hành động của OPP ở phần trước chúng ta từng đề cập. Và cuối cùng là print kết quả số lần checkout ra màn hình console.

Kết luận

Bằng việc tìm hiểu ví dụ trên, chúng ta đã nắm được 4 hành động cơ bản của Object Pool Pattern, qua đó dễ dàng áp dụng pool vào từng ứng dụng cụ thể. Điểm lưu ý khi dùng pattern này là đúng mục đích và tránh những lỗi xảy ra khi sử dụng đa luồng như 2 thread cùng thay đổi một mảng cùng một thời điểm... Hy vọng bài viết nhỏ này sẽ giúp ích cho các bạn có cái nhìn đúng đắn và sử dụng hợp lý design pattern nói chung và Object Pool Pattern nói riêng nhằm nâng cao chất lượng cũng như hiệu quả công việc.

Happy coding!