提交 4a9644a0 编写于 作者: D Dmitriy Kiriyenko

Prevent callback from being set twice.

When you add one callack in two separate `set_callback` calls - it is
only called once.

When you do it in one `set_callback` call - it is called twice.

This violates the principle of least astonishment for me. Duplicating
callback is usually an error. There is a correct and obvious way to do
anything without this "feature".

If you want to do

    before_save :clear_balance, :calculate_tax, :clear_balance

or whatever, you should better do

    before_save :carefully_calculate_tax

    def carefully_calculate_tax
      clear_balance
      calculate_tax
      clear_balance
    end

And this even opens gates for some advanced refactorings, unlike the
first approach.

My assumptions are:

- Principle of least astonishment is violated, when callbacks are either
  prevented from duplication, or not.
- Duplicating callbacks is usually an error. When it is intentional -
  it's a smell of a bad design and can be approached without abusing
  this "feature".

My suggestion is: do not allow duplicating callbacks in one callback
call, like it is not allowed in separate callbacks call.
上级 01d3a36b
## Rails 4.0.0 (unreleased) ## ## Rails 4.0.0 (unreleased) ##
* Prevent `Callbacks#set_callback` from setting the same callback twice.
before_save :foo, :bar, :foo
will at first call `bar`, then `foo`. `foo` will no more be called
twice.
*Dmitriy Kiriyenko*
* Deprecate `Time.time_with_date_fallback`, `Time.utc_time` and `Time.local_time`. * Deprecate `Time.time_with_date_fallback`, `Time.utc_time` and `Time.local_time`.
These methods were added to handle the limited range of Ruby's native Time These methods were added to handle the limited range of Ruby's native Time
implementation. Those limitations no longer apply so we are deprecating them in 4.0 implementation. Those limitations no longer apply so we are deprecating them in 4.0
......
...@@ -133,6 +133,10 @@ def matches?(_kind, _filter) ...@@ -133,6 +133,10 @@ def matches?(_kind, _filter)
@kind == _kind && @filter == _filter @kind == _kind && @filter == _filter
end end
def duplicates?(other)
matches?(other.kind, other.filter)
end
def _update_filter(filter_options, new_options) def _update_filter(filter_options, new_options)
filter_options[:if].concat(Array(new_options[:unless])) if new_options.key?(:unless) filter_options[:if].concat(Array(new_options[:unless])) if new_options.key?(:unless)
filter_options[:unless].concat(Array(new_options[:if])) if new_options.key?(:if) filter_options[:unless].concat(Array(new_options[:if])) if new_options.key?(:if)
...@@ -327,6 +331,30 @@ def compile ...@@ -327,6 +331,30 @@ def compile
method.join("\n") method.join("\n")
end end
def append(*callbacks)
callbacks.each { |c| append_one(c) }
end
def prepend(*callbacks)
callbacks.each { |c| prepend_one(c) }
end
private
def append_one(callback)
remove_duplicates(callback)
push(callback)
end
def prepend_one(callback)
remove_duplicates(callback)
unshift(callback)
end
def remove_duplicates(callback)
delete_if { |c| callback.duplicates?(c) }
end
end end
module ClassMethods module ClassMethods
...@@ -412,11 +440,7 @@ def set_callback(name, *filter_list, &block) ...@@ -412,11 +440,7 @@ def set_callback(name, *filter_list, &block)
Callback.new(chain, filter, type, options.dup, self) Callback.new(chain, filter, type, options.dup, self)
end end
filters.each do |filter| options[:prepend] ? chain.prepend(*mapped) : chain.append(*mapped)
chain.delete_if {|c| c.matches?(type, filter) }
end
options[:prepend] ? chain.unshift(*(mapped.reverse)) : chain.push(*mapped)
target.send("_#{name}_callbacks=", chain) target.send("_#{name}_callbacks=", chain)
end end
......
...@@ -297,7 +297,7 @@ def save ...@@ -297,7 +297,7 @@ def save
end end
end end
end end
class AroundPersonResult < MySuper class AroundPersonResult < MySuper
attr_reader :result attr_reader :result
...@@ -308,7 +308,7 @@ class AroundPersonResult < MySuper ...@@ -308,7 +308,7 @@ class AroundPersonResult < MySuper
def tweedle_dum def tweedle_dum
@result = yield @result = yield
end end
def tweedle_1 def tweedle_1
:tweedle_1 :tweedle_1
end end
...@@ -316,7 +316,7 @@ def tweedle_1 ...@@ -316,7 +316,7 @@ def tweedle_1
def tweedle_2 def tweedle_2
:tweedle_2 :tweedle_2
end end
def save def save
run_callbacks :save do run_callbacks :save do
:running :running
...@@ -410,7 +410,7 @@ def test_save_around ...@@ -410,7 +410,7 @@ def test_save_around
], around.history ], around.history
end end
end end
class AroundCallbackResultTest < ActiveSupport::TestCase class AroundCallbackResultTest < ActiveSupport::TestCase
def test_save_around def test_save_around
around = AroundPersonResult.new around = AroundPersonResult.new
...@@ -607,6 +607,45 @@ def save ...@@ -607,6 +607,45 @@ def save
end end
end end
class OneTwoThreeSave
include ActiveSupport::Callbacks
define_callbacks :save
attr_accessor :record
def initialize
@record = []
end
def save
run_callbacks :save do
@record << "yielded"
end
end
def first
@record << "one"
end
def second
@record << "two"
end
def third
@record << "three"
end
end
class DuplicatingCallbacks < OneTwoThreeSave
set_callback :save, :before, :first, :second
set_callback :save, :before, :first, :third
end
class DuplicatingCallbacksInSameCall < OneTwoThreeSave
set_callback :save, :before, :first, :second, :first, :third
end
class UsingObjectTest < ActiveSupport::TestCase class UsingObjectTest < ActiveSupport::TestCase
def test_before_object def test_before_object
u = UsingObjectBefore.new u = UsingObjectBefore.new
...@@ -709,5 +748,18 @@ def test_per_key_option_deprecaton ...@@ -709,5 +748,18 @@ def test_per_key_option_deprecaton
end end
end end
end end
class ExcludingDuplicatesCallbackTest < ActiveSupport::TestCase
def test_excludes_duplicates_in_separate_calls
model = DuplicatingCallbacks.new
model.save
assert_equal ["two", "one", "three", "yielded"], model.record
end
def test_excludes_duplicates_in_one_call
model = DuplicatingCallbacksInSameCall.new
model.save
assert_equal ["two", "one", "three", "yielded"], model.record
end
end
end end
...@@ -122,12 +122,12 @@ class AlsoDoingNothingTest < ActiveSupport::TestCase ...@@ -122,12 +122,12 @@ class AlsoDoingNothingTest < ActiveSupport::TestCase
# Setup and teardown callbacks. # Setup and teardown callbacks.
class SetupAndTeardownTest < ActiveSupport::TestCase class SetupAndTeardownTest < ActiveSupport::TestCase
setup :reset_callback_record, :foo setup :reset_callback_record, :foo
teardown :foo, :sentinel, :foo teardown :foo, :sentinel
def test_inherited_setup_callbacks def test_inherited_setup_callbacks
assert_equal [:reset_callback_record, :foo], self.class._setup_callbacks.map(&:raw_filter) assert_equal [:reset_callback_record, :foo], self.class._setup_callbacks.map(&:raw_filter)
assert_equal [:foo], @called_back assert_equal [:foo], @called_back
assert_equal [:foo, :sentinel, :foo], self.class._teardown_callbacks.map(&:raw_filter) assert_equal [:foo, :sentinel], self.class._teardown_callbacks.map(&:raw_filter)
end end
def setup def setup
...@@ -147,7 +147,7 @@ def foo ...@@ -147,7 +147,7 @@ def foo
end end
def sentinel def sentinel
assert_equal [:foo, :foo], @called_back assert_equal [:foo], @called_back
end end
end end
...@@ -159,7 +159,7 @@ class SubclassSetupAndTeardownTest < SetupAndTeardownTest ...@@ -159,7 +159,7 @@ class SubclassSetupAndTeardownTest < SetupAndTeardownTest
def test_inherited_setup_callbacks def test_inherited_setup_callbacks
assert_equal [:reset_callback_record, :foo, :bar], self.class._setup_callbacks.map(&:raw_filter) assert_equal [:reset_callback_record, :foo, :bar], self.class._setup_callbacks.map(&:raw_filter)
assert_equal [:foo, :bar], @called_back assert_equal [:foo, :bar], @called_back
assert_equal [:foo, :sentinel, :foo, :bar], self.class._teardown_callbacks.map(&:raw_filter) assert_equal [:foo, :sentinel, :bar], self.class._teardown_callbacks.map(&:raw_filter)
end end
protected protected
...@@ -168,7 +168,7 @@ def bar ...@@ -168,7 +168,7 @@ def bar
end end
def sentinel def sentinel
assert_equal [:foo, :bar, :bar, :foo], @called_back assert_equal [:foo, :bar, :bar], @called_back
end end
end end
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册