[Series-DesignPatternInRuby] Singleton - Phần 1

Mở đầu

Chào mọi người, đây là bài đầu tiên trong series DesignPatternInRuby mà mình sẽ dịch từ cuốn Design Pattern in Ruby (2007) Trong series này mình sẽ cố gắng dịch toàn bộ cuốn sách, cố gắng 1 tuần có ít nhất 1 bài. Mong mọi người ủng hộ.

Making sure there is only One with the Singleton

Ngay cả khi những coders biết rất ít về các mẫu design pattern (DP) thì cũng biết về Singleton. Phần lớn họ đều biêt 1 thứ:

Singleton are Bad, with a capital "B"

Singleton xuất hiện ở mọi nơi. Trong Java nó xuất hiện trong hầu hết những thứ xung quanh ngôn ngữ này: tomcat, ant, JDOM. Trong Ruby chúng ta có thể tìm nó ở Webrick, rake, và ngay cả trong Rails. Vậy Singleton là gì mà nó lại trở nên cần thiết nhưng cũng lại bị ghét đến vậy. Trong chương này chúng ta sẽ tìm hiểu tại sao lại cần Singleton, học cách tạo nên một Singleton trong Ruby, tại sao Singleton lại có thể gây rắc rối, và làm cách nào để tránh những rắc rối mà nó mang lại.

One Object, Global Access

Ý tưởng của Singleton rất đơn giản: Nếu project của bạn có một số thứ "duy nhất", chỉ có duy nhất một file config, hay một log file duy nhất. Hoặc có thể bạn làm việc với 1 màn hình duy nhất, hay dữ liệu trong chương trình được nhập duy nhất từ một bàn phím. Nhiều thành phần cũng truy cập vào một DB duy nhất. Class mà bạn đang viết sinh ra một instance variable mà nhiều nơi trong chương trình dùng đến, và bạn cảm thấy việc truyền instance variable đó đến một đống method khác nhau trông thật ngốc nghếch. Singleton được sinh ra để giải quyết những vấn đề kể trên. Gang of Four (GOF) gợi ý hãy sử dụng Singleton để giải quyết những vấn đề:

A class that can have only one instance and that provides global access to that one instance.

Có rất nhiều cách khác nhau để tìm hiểu về Singleton trong Ruby, nhưng chúng ta sẽ bắt đầu với một method được recommend bởi GOF:

Giao quyền quản lý việc tạo và truy cập đến Singleton object cho class của nó.

Hãy cùng xem lại một số kiến thức về class variableclass methods trong Ruby.

Class Variables and Methods

Class variable

Class variable là biến mà nó được "đính kèm" với class, chứ không phải là instance như instance variable. Việc khai báo class variable trong Ruby rất đơn giản: chỉ cần thêm 2 kí tự @ trước tên biến. Ví dụ như class sau:

class ClassVariableTester
    @@class_count = 0
    
    def initialize 
        @instance_count = 0
    end
    
    def increment
        @@class_count = @class_count + 1
        @instance_count = @instance_count + 1
    end
     
    def to_s
         "class_count: #{@class_count} instance_count: #{instance_count}"
    end
 end 

Giờ hãy tạo instance của class ClassVariableTester mà chúng ta viết ở trên:

c1 = ClassVariableTester.new 
c1.increment 
c1.increment
p c1

Không có gì quá bất ngờ, kết quả như sau:

class_count: 2 instance_count: 2

Nhưng chuyện gì sẽ xảy ra trong trường hợp chúng ta tạo thêm một instance c2 cho class ClassVariableTester?

c2 = ClassVariableTester.new 
p c2

Kết quả nhận được:

class_count: 2 instance_count: 0

Chuyện gì đã xảy ra vậy? tại sao instance_count bị "reset" về 0, nhưng class_count vẫn đếm đúng số instance variable đã được tạo ra?

Class methods

Việc tạo ra class-level method trong Ruby hơi khó khăn hơn chút xíu, nhưng vẫn rất dễ dàng.

class SomeClass
    def self.class_level_method
        p "hello from class class method"
    end
end

Giờ chúng ta đã có thể gọi class methods đó từ class

SomeClass.class_level_method

Nếu bạn không muốn dùng từ khóa self, Ruby cung cấp công cụ khác cho bạn.

class SomeClass
    def SomeClass.class_level_method
        p "hello from class method"
    end 
end

A First Try at a Ruby Singleton

Bây giờ chúng ta đã biết cách để tạo ra một class variable và method, vậy là đủ công cụ để tạo ra một Singleton object trong Ruby. Giả sử chúng ta cần implement một class dùng cho việc logging như sau:

class SimpleLogger
    attr_accessor :level
    ERROR = 1
    WARNING = 2
    INFO = 3
    
    def initialize
        @log = File.open("log.txt", "w")
        @level = WARNING
    end 
    
    def error msg
        @log.puts msg 
        @log.flush
    end
    
   def warning msg
       @log.puts msg if @level >= INFO
       @log.flush
   end
end 

Đầu tiên chúng ta sẽ xem xét cách viết không sử dụng Singleton như trên.

logger = SimpleLogger.new 
logger.level = SimpleLogger::INFO
logger.info "Doing the first thing"
logger.info "Now doing the second thing"

Managing the Single Instance

Điểm quan trọng nhất của Singleton là tránh việc truyền các object như logger trên đi khắp chương trình. Thay vào đó, chúng ta sẽ giao trách nhiệm quản lý object đó cho class SimpleLogger. Vậy làm sao chúng ta có thể chuyển class trên trở thành một Singleton class? Đầu tiên, chúng ta sẽ tạo một biến để giữ instance duy nhất mà class Singleton cần quản lý. Bạn cần một method để trả về instance duy nhất đó lúc cần.

class SimpleLogger
    @@instance = SimpleLogger.new 
    
    def self.instance
        @@instance 
    end 
end

Và bây giờ, mỗi khi gọi SimpleLogger.instance, chúng ta đều sẽ nhận được duy nhất một instance duy nhất của SimpleLogger

logger1 = SimpleLogger.instance #=> return the logger
logger2 = SimpleLogger.instance #=> return the same logger

Và giờ đây, chúng ta có thể sử dụng singleton logger ở mọi nơi trong code của mình.

SimpleLogger.instance.info "Computer win chess game"
SimpleLogger.instance.warning "AE-35 hardware failure predicted"
SimpleLogger.instance.error "HAL-9000 malfunction, take emergency action!"

Making Sure There Is Only One

Hãy nhớ rằng, một yêu cầu tối quan trọng của singleton là chỉ có duy nhất một và chỉ một instance của class Singleton. Đoạn code định nghĩa SimleLogger của chúng ta phía trên có vấn đề. Chúng ta vừa có thể lấy ra 1 instance bằng SimpleLogger.instance lại vừa có thể bằng SimpleLogger.new, vì vậy chúng ta cần chỉnh sửa lại một chút bằng cách đưa method new trở thành private:

class SimpleLogger
    @@instance = SimpleLogger.new 
    
    def self.instance 
        @@instance
    end
    
    private_class_method :new 
end

The Singleton Module

Class SimpleLogger của chúng ta đã thỏa mãn điều kiện của GOF: chỉ tồn tại duy nhất một instance, nó có thể cung cấp 1 instance để sử dụng bất cứ lúc nào chúng ta cần, không bất kì ai được phép tạo instance thứ 2. Nhưng vấn đề vẫn chưa dừng lại ở đó, sẽ ra sao nếu chúng ta muốn tạo ra một class Singleton thứ 2, thứ 3? Điều đó dẫn đến việc trùng lặp code không cần thiết. Vậy phải giải quyết như thế nào? Ruby đưa ra một phương pháp đơn giản, ít nhức đầu hơn. Chỉ cần include Singleton module:

require "singleton"
class SimpleLogger
    include Singleton
end 

module Singleton đã làm tất cả những gì mà chúng ta cần: tạo ra class method: instance, đưa new trở thành private method. Việc sử dụng là hoàn toàn tương tự: SimpleLogger.instance

Lazy and Eager Singleton

Có một điểm khác biệt lớn nhất giữa việc chúng ta tự viết Singleton class và việc sử dụng module Singleton. Hãy xem lại code chúng ta tự viết lúc đầu:

class SimpleLogger
    @@instance = SimpleLogger.new
end

Chúng ta có thể nhận thấy, instance của SimpleLogger được tạo trước khi sử dụng, nói cách khác, mặc dù chưa gọi SimpleLogger.instance nhưng instance của SimpleLogger đã được khởi tạo giá trị. Việc sử dụng module Singleton thì khác, nó đợi đến khi SimpleLogger.instance được gọi thì instance mới được khởi tạo. Kỹ thuật này gọi là lazy instantiation

To be continued...


All Rights Reserved