从 class 中动态删除 mixin 祖先
Dynamically remove mixin ancestor from class
我正在尝试在 ruby 中开发一个简单的纸牌游戏控制台应用程序作为一个宠物项目。我想添加到这款纸牌游戏中的互动之一是“在轮到你的时候,你可以购买纸牌,就好像它们的价格便宜 X 一样”。现在,我考虑在 mixin 装饰器的帮助下对这种行为进行建模。假设以下 class:
class Card
attr_reader :cost
def initialize(cost)
self.cost = cost
end
def play(*_args)
raise NotImplementedError, "Override me!"
end
private
attr_writer :cost
end
我认为一个简单的解决方案是这样的:
class CardThatDiscounts < Card
def play(*_args)
@mod = Module.new do
def cost
super - 1
end
end
Card.prepend(@mod)
end
end
这将使我能够灵活地定位 class 本身或卡片的特定实例。但是,我不确定如何在回合结束时扭转这种影响。我看到了一些与此类似的问题,其答案大致如下:
- 手动删除定义的方法。
这种方法对我来说并不适用,因为我是在修饰现有方法而不是添加新方法。我可以在添加模块之前重命名现有方法,然后 return 重新命名,但是如果我想链接多个这样的装饰器会怎样?
- 使用gemmixology
Mixology 是旧的 gem,似乎不再维护。虽然,根据文档,这正是我所需要的。然而,该实现主要涉及 ruby 内部,不幸的是我不明白。是否无法从 Ruby 中修改 class 祖先链,或者我是否需要为此扩展 c/Java?
我考虑过其他替代方案,例如 SimpleDelegator
或 DelegateClass
,但这两种方案都需要我实例化新对象,然后以某种方式用这些替换对现有 Card
对象的引用新包裹的物品,并在回合结束时再次返回。这似乎比直接修改祖先链要复杂一些。
我想我的问题有两部分。是否可以使用纯 ruby 从 class 的祖先链中删除特定祖先(因为混合学 gem 已经表明可以使用 c/Java 扩展)?如果不是,获得类似行为的合适解决方法是什么?
你想要实现的是一个非常糟糕的模式。您几乎不应该动态地使用 prepend
或 include
,而是围绕您(以及可能阅读您的代码的人)确实理解的概念对您的代码进行建模。
你可能想做的是创建(某种)一个名为 CardAffectedByEnvironment
的委托人 - 然后你将始终做 CardAffectedByEnvironment.new(Card.new(x), env)
而不是 Card.new(x)
,其中env
将保留您所有的状态更改,或者添加一个方法 real_cost
来根据您的 cost
和环境计算事物并使用此方法。
下面是一个带有 CardAffectedByEnvironment
的代码,它可能会更好地描述我认为它是如何工作的:
class Environment
def initialize
@modifiers = []
end
attr_reader :modifiers
def next_round
@modifiers = []
end
def modifiers_for(method)
@modifiers.select { |i| i.first == method }
end
end
class CardAffectedByEnvironment
def initialize(card, env)
@card, @env = card, env
end
# Since Ruby 3.0 you can write `...` instead of `*args, **kwargs, &block`
def method_missing(method, *args, **kwargs, &block)
value = @card.public_send(method, *args, **kwargs, &block)
@env.modifiers_for(method).each do |_, modifier|
value = modifier.call(value)
end
value
end
end
class Joker
def cost
10
end
end
env = Environment.new
card = CardAffectedByEnvironment.new(Joker.new, env)
p card.cost # => 10
env.modifiers << [:cost, ->(i) { i - 1 }]
p card.cost # => 9
env.next_round
p card.cost # => 10
我正在尝试在 ruby 中开发一个简单的纸牌游戏控制台应用程序作为一个宠物项目。我想添加到这款纸牌游戏中的互动之一是“在轮到你的时候,你可以购买纸牌,就好像它们的价格便宜 X 一样”。现在,我考虑在 mixin 装饰器的帮助下对这种行为进行建模。假设以下 class:
class Card
attr_reader :cost
def initialize(cost)
self.cost = cost
end
def play(*_args)
raise NotImplementedError, "Override me!"
end
private
attr_writer :cost
end
我认为一个简单的解决方案是这样的:
class CardThatDiscounts < Card
def play(*_args)
@mod = Module.new do
def cost
super - 1
end
end
Card.prepend(@mod)
end
end
这将使我能够灵活地定位 class 本身或卡片的特定实例。但是,我不确定如何在回合结束时扭转这种影响。我看到了一些与此类似的问题,其答案大致如下:
- 手动删除定义的方法。
这种方法对我来说并不适用,因为我是在修饰现有方法而不是添加新方法。我可以在添加模块之前重命名现有方法,然后 return 重新命名,但是如果我想链接多个这样的装饰器会怎样?
- 使用gemmixology
Mixology 是旧的 gem,似乎不再维护。虽然,根据文档,这正是我所需要的。然而,该实现主要涉及 ruby 内部,不幸的是我不明白。是否无法从 Ruby 中修改 class 祖先链,或者我是否需要为此扩展 c/Java?
我考虑过其他替代方案,例如 SimpleDelegator
或 DelegateClass
,但这两种方案都需要我实例化新对象,然后以某种方式用这些替换对现有 Card
对象的引用新包裹的物品,并在回合结束时再次返回。这似乎比直接修改祖先链要复杂一些。
我想我的问题有两部分。是否可以使用纯 ruby 从 class 的祖先链中删除特定祖先(因为混合学 gem 已经表明可以使用 c/Java 扩展)?如果不是,获得类似行为的合适解决方法是什么?
你想要实现的是一个非常糟糕的模式。您几乎不应该动态地使用 prepend
或 include
,而是围绕您(以及可能阅读您的代码的人)确实理解的概念对您的代码进行建模。
你可能想做的是创建(某种)一个名为 CardAffectedByEnvironment
的委托人 - 然后你将始终做 CardAffectedByEnvironment.new(Card.new(x), env)
而不是 Card.new(x)
,其中env
将保留您所有的状态更改,或者添加一个方法 real_cost
来根据您的 cost
和环境计算事物并使用此方法。
下面是一个带有 CardAffectedByEnvironment
的代码,它可能会更好地描述我认为它是如何工作的:
class Environment
def initialize
@modifiers = []
end
attr_reader :modifiers
def next_round
@modifiers = []
end
def modifiers_for(method)
@modifiers.select { |i| i.first == method }
end
end
class CardAffectedByEnvironment
def initialize(card, env)
@card, @env = card, env
end
# Since Ruby 3.0 you can write `...` instead of `*args, **kwargs, &block`
def method_missing(method, *args, **kwargs, &block)
value = @card.public_send(method, *args, **kwargs, &block)
@env.modifiers_for(method).each do |_, modifier|
value = modifier.call(value)
end
value
end
end
class Joker
def cost
10
end
end
env = Environment.new
card = CardAffectedByEnvironment.new(Joker.new, env)
p card.cost # => 10
env.modifiers << [:cost, ->(i) { i - 1 }]
p card.cost # => 9
env.next_round
p card.cost # => 10