[Series-DesignPatternInRuby] Singleton - Phần 1
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ộ. Ngay cả khi những coders biết rất ít về các mẫu design ...
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ộ.
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 variable và class 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
và 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