从 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 本身或卡片的特定实例。但是,我不确定如何在回合结束时扭转这种影响。我看到了一些与此类似的问题,其答案大致如下:

  1. 手动删除定义的方法。

这种方法对我来说并不适用,因为我是在修饰现有方法而不是添加新方法。我可以在添加模块之前重命名现有方法,然后 return 重新命名,但是如果我想链接多个这样的装饰器会怎样?

  1. 使用gemmixology

Mixology 是旧的 gem,似乎不再维护。虽然,根据文档,这正是我所需要的。然而,该实现主要涉及 ruby 内部,不幸的是我不明白。是否无法从 Ruby 中修改 class 祖先链,或者我是否需要为此扩展 c/Java?

我考虑过其他替代方案,例如 SimpleDelegatorDelegateClass,但这两种方案都需要我实例化新对象,然后以某种方式用这些替换对现有 Card 对象的引用新包裹的物品,并在回合结束时再次返回。这似乎比直接修改祖先链要复杂一些。

我想我的问题有两部分。是否可以使用纯 ruby 从 class 的祖先链中删除特定祖先(因为混合学 gem 已经表明可以使用 c/Java 扩展)?如果不是,获得类似行为的合适解决方法是什么?

你想要实现的是一个非常糟糕的模式。您几乎不应该动态地使用 prependinclude,而是围绕您(以及可能阅读您的代码的人)确实理解的概念对您的代码进行建模。

你可能想做的是创建(某种)一个名为 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