Câu hỏi khó, người đơn thô. Một phần vì lý do bạn đang hiển thị (cách đặt lại), và một phần vì chúng tạo ra các giả định có khuynh hướng cắn bạn sau này (ví dụ: hầu hết Rails).
Có một vài điều bạn có thể làm, tất cả chúng đều "ổn" ở mức tốt nhất. Giải pháp tốt nhất là tìm cách để loại bỏ những người độc thân. Điều này là lượn sóng, tôi biết, bởi vì không có một công thức hay thuật toán bạn có thể áp dụng, và nó loại bỏ rất nhiều tiện lợi, nhưng nếu bạn có thể làm điều đó, nó thường đáng giá.
Nếu bạn không thể làm điều đó, ít nhất hãy thử tiêm singleton thay vì truy cập trực tiếp. Thử nghiệm có thể khó khăn ngay bây giờ, nhưng hãy tưởng tượng phải đối phó với các vấn đề như thế này khi chạy. Để làm được điều đó, bạn cần có cơ sở hạ tầng được xây dựng để xử lý nó.
Dưới đây là sáu cách tiếp cận mà tôi đã nghĩ đến.
Cung cấp một thể hiện của lớp, nhưng cho phép các lớp học để được instantiated. Điều này là phù hợp nhất với cách thức những người độc thân được trình bày theo truyền thống. Về cơ bản bất cứ lúc nào bạn muốn tham khảo singleton, bạn nói chuyện với trường hợp singleton, nhưng bạn có thể kiểm tra đối với các trường hợp khác. Có một mô-đun trong số stdlib để trợ giúp điều này, nhưng nó làm cho .new
riêng tư, vì vậy nếu bạn muốn sử dụng nó, bạn phải sử dụng một cái gì đó như let(:config) { Configuration.send :new }
để kiểm tra nó.
class Configuration
def self.instance
@instance ||= new
end
attr_writer :credentials_file
def credentials_file
@credentials_file || raise("credentials file not set")
end
end
describe Config do
let(:config) { Configuration.new }
specify '.instance always refers to the same instance' do
Configuration.instance.should be_a_kind_of Configuration
Configuration.instance.should equal Configuration.instance
end
describe 'credentials_file' do
specify 'it can be set/reset' do
config.credentials_file = 'abc'
config.credentials_file.should == 'abc'
config.credentials_file = 'def'
config.credentials_file.should == 'def'
end
specify 'raises an error if accessed before being initialized' do
expect { config.credentials_file }.to raise_error 'credentials file not set'
end
end
end
Sau đó, bất cứ nơi nào bạn muốn truy cập nó, sử dụng Configuration.instance
Lập singleton một thể hiện của một số lớp khác. Sau đó, bạn có thể kiểm tra các lớp khác trong sự cô lập, và không cần phải kiểm tra singleton của bạn một cách rõ ràng.
class Counter
attr_accessor :count
def initialize
@count = 0
end
def count!
@count += 1
end
end
describe Counter do
let(:counter) { Counter.new }
it 'starts at zero' do
counter.count.should be_zero
end
it 'increments when counted' do
counter.count!
counter.count.should == 1
end
end
Sau đó, trong ứng dụng của bạn ở đâu đó:
MyCounter = Counter.new
Bạn có thể chắc chắn rằng không bao giờ sửa lại lớp chính, sau đó chỉ cần lớp con nó cho các bài kiểm tra của bạn:
class Configuration
class << self
attr_writer :credentials_file
end
def self.credentials_file
@credentials_file || raise("credentials file not set")
end
end
describe Config do
let(:config) { Class.new Configuration }
describe 'credentials_file' do
specify 'it can be set/reset' do
config.credentials_file = 'abc'
config.credentials_file.should == 'abc'
config.credentials_file = 'def'
config.credentials_file.should == 'def'
end
specify 'raises an error if accessed before being initialized' do
expect { config.credentials_file }.to raise_error 'credentials file not set'
end
end
end
Sau đó, trong ứng dụng của bạn ở đâu đó:
MyConfig = Class.new Configuration
Đảm bảo rằng có một cách để reset singleton. Hoặc nói chung, hoàn tác mọi thứ bạn làm. (ví dụ: nếu bạn có thể đăng ký một số đối tượng với singleton, thì bạn cần phải có thể hủy đăng ký đối tượng đó, chẳng hạn như khi bạn phân lớp Railtie
, nó ghi lại trong một mảng, nhưng bạn có thể access the array and delete the item from it).
class Configuration
def self.reset
@credentials_file = nil
end
class << self
attr_writer :credentials_file
end
def self.credentials_file
@credentials_file || raise("credentials file not set")
end
end
RSpec.configure do |config|
config.before { Configuration.reset }
end
describe Config do
describe 'credentials_file' do
specify 'it can be set/reset' do
Configuration.credentials_file = 'abc'
Configuration.credentials_file.should == 'abc'
Configuration.credentials_file = 'def'
Configuration.credentials_file.should == 'def'
end
specify 'raises an error if accessed before being initialized' do
expect { Configuration.credentials_file }.to raise_error 'credentials file not set'
end
end
end
Clone lớp thay vì thử nghiệm nó trực tiếp. Điều này xuất phát từ một số gist Tôi đã thực hiện, về cơ bản bạn chỉnh sửa bản sao thay vì lớp thực.
class Configuration
class << self
attr_writer :credentials_file
end
def self.credentials_file
@credentials_file || raise("credentials file not set")
end
end
describe Config do
let(:configuration) { Configuration.clone }
describe 'credentials_file' do
specify 'it can be set/reset' do
configuration.credentials_file = 'abc'
configuration.credentials_file.should == 'abc'
configuration.credentials_file = 'def'
configuration.credentials_file.should == 'def'
end
specify 'raises an error if accessed before being initialized' do
expect { configuration.credentials_file }.to raise_error 'credentials file not set'
end
end
end
Phát triển hành vi trong module, sau đó mở rộng đó vào singleton. Here là một ví dụ liên quan nhiều hơn một chút. Có lẽ bạn sẽ phải xem xét các phương thức self.included
và self.extended
nếu bạn cần khởi tạo một số biến trên đối tượng.
module ConfigurationBehaviour
attr_writer :credentials_file
def credentials_file
@credentials_file || raise("credentials file not set")
end
end
describe Config do
let(:configuration) { Class.new { extend ConfigurationBehaviour } }
describe 'credentials_file' do
specify 'it can be set/reset' do
configuration.credentials_file = 'abc'
configuration.credentials_file.should == 'abc'
configuration.credentials_file = 'def'
configuration.credentials_file.should == 'def'
end
specify 'raises an error if accessed before being initialized' do
expect { configuration.credentials_file }.to raise_error 'credentials file not set'
end
end
end
Sau đó, trong ứng dụng của bạn ở đâu đó:
class Configuration
extend ConfigurationBehaviour
end
Wow, câu trả lời tuyệt vời Joshua! Có rất nhiều thức ăn để suy nghĩ ở đó. Tôi đã đọc bài đăng trên blog này [Tại sao Singletons là Evil] (http://blogs.msdn.com/b/scottdensmore/archive/2004/05/25/140827.aspx) trước đây và tôi đi đến kết luận rằng sử dụng singleton mô hình là một sự lựa chọn thiết kế xấu trên một phần của tôi. Nếu tôi đã TDD'ed lớp này từ quan niệm, tôi không nghĩ rằng tôi đã làm nó theo cách này. Tôi đã học được bài học của tôi, những người độc thân thực sự khó kiểm tra!Tôi sẽ sử dụng một trong các đề xuất của bạn khi tôi thiết kế lại lớp này (và TDD nó!). Cảm ơn rất nhiều! – thegreendroid
Theo ý kiến của tôi, câu trả lời dưới đây sẽ trả lời tốt hơn câu hỏi gốc và câu hỏi của tôi về việc đặt lại một singleton trong một rspec. –