Rails ActiveRecord告诉不要问

时间:2012-12-04 18:10:09

标签: ruby-on-rails activerecord tdd tell-dont-ask

有两个这样的课程:

class Site < ActiveRecord::Base
  has_one :subscription, dependent: :destroy

  def self.hostname_active?(hostname)
    site = where(hostname: hostname)
    site.exists? && site.first.active?
  end

  def active?
    subscription.active?
  end
end

class Subscription < ActiveRecord::Base
  belongs_to :site

  def active?
    (starts_at..ends_at).cover?(Date.current)
  end
end

describe Site do
  let(:site) { Fabricate.build(:site) }

  describe "#hostname_active?" do
    it "Returns true if site with hostname exists & is active" do
      described_class.stub_chain(:where, :exists?).and_return(true)
      described_class.stub_chain(:where, :first) { site }
      site.stub(:active?).and_return(true)
      described_class.hostname_active?('www.example.com').should be_true
    end

    it "Returns false if site with hostname doesn't exist" do
      described_class.stub_chain(:where, :exists?).and_return(false)
      described_class.stub_chain(:where, :first) { site }
      site.stub(:active?).and_return(false)
      described_class.hostname_active?('www.example.com').should be_false
    end

    it "Returns false if site is not active" do
      described_class.stub_chain(:where, :exists?).and_return(true)
      described_class.stub_chain(:where, :first) { site }
      site.stub(:active?).and_return(false)
      described_class.hostname_active?('www.example.com').should be_false
    end
  end
end

如果相关订阅确定站点是否处于活动状态,我使用方法hostname_active?作为路由中的约束以及我需要确定a)是否存在的其他类,以及b)很活跃。

取自另一个关于SO的问题:

  

Tell-not-ask基本上意味着你不应该查询关于它状态的对象,根据它的状态做出决定,然后告诉同一个对象要做什么。如果对象具有所需的所有信息,则应自行决定。

虽然我不这样做,但我的代码确实感觉非常耦合,无论是在网站和网站之间的耦合方面。订阅,但也与ActiveRecord的耦合,这使得很难在不触及数据库的情况下进行测试。

如何构建它以避免询问相关订阅以确定网站的状态?而且,你会认为这违反了告诉不要问吗?

1 个答案:

答案 0 :(得分:1)

使用ActiveRecord,你会有一些耦合,而且没关系,而你正在做的事情并没有违反LOD。您可以对active字段进行非规范化以将其保留在网站本身上,但我不会这样做。

我要改变的一件事是急于加载订阅。

#Just showing changes

class Site < ActiveRecord::Base
  scope :active, includes(:subscription).merge(Subscription.active)
  has_one :subscription, dependent: :destroy

  def self.hostname_active?(hostname)
    active.where(hostname: hostname).exists?
  end
end

class Subscription < ActiveRecord::Base
  scope :active, where(arel_table[:starts_at].lteq(Date.current), arel_table[:ends_at].gteq(Date.current))
end

至少,这将使您不必进行两次查询以确定主机名是否处于活动状态。

就存根ActiveRecord而言,我通常不会发现这种情况。普遍接受的做法是使用夹具或工厂来构建测试对象。就个人而言,我使用FactoryGirl:https://github.com/thoughtbot/factory_girl_rails

在你的情况下,我会有像:

这样的工厂
FactoryGirl.define do
  factory :subscription do
    site
    factory :active_subscription do
      starts_at { Date.today.beginning_of_month }
      ends_at { Date.today.end_of_month }
    end

    factory :inactive_subscription do
      starts_at { Date.today.beginning_of_month - 3.months }
      ends_at { Date.today.end_of_month - 3.months }
    end
  end
end

FactoryGirl.define do
  factory :site do
    sequence(:hostname, 1000) {|h| "site#{h}.example.com" }
    factory :active_site do
      after(:create) do |site|
        FactoryGirl.create(:active_subscription, site: site)
      end
    end
    factory :inactive_site do
      after(:create) do |site|
        FactoryGirl.create(:inactive_subscription, site: site)
      end
    end
  end
end

这样我的规格就可以了:

describe Site do 
  describe "Active site" do
    subject(:site) { FactoryGirl.create :active_site }
    its(:active?) { should eq(true) }
  end

  #etc...
end