I am trying to develop a simple card game console application in ruby as a pet project. One of the interactions I would like to add to this card game is something along the lines of "During your turn, you can buy cards as if they cost X less". Now, I thought about modelling this behaviour with the help of mixin decorators. Assuming the following 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
I thought a simple solution would be something like this:
class CardThatDiscounts < Card
def play(*_args)
@mod = Module.new do
def cost
super - 1
end
end
Card.prepend(@mod)
end
end
This would give me the flexibility to target either the class itself, or specific instances of the cards. However, I am unsure of how to reverse this effect, at the end of the turn. I saw a few questions similar to this one, the answers of which said something along the lines of:
- Remove the defined method manually.
This approach won't really work for me, since I am decorating existing methods instead of adding new ones. I could maybe rename the existing method when I prepend the module, and then return it back, but what happens in case I want to chain multiple such decorators?
- Use gem mixology
Mixology is an old gem, and seems no longer maintained. Though, according to the documentation, it is exactly what I need. However, the implementation mostly deals with ruby internals, that I unfortunately do not understand. Is it not possible to modify a class ancestor chain from within Ruby, or do I need c/Java extension for this?
I considered other alternatives, such as SimpleDelegator
or DelegateClass
, but both of these require that I instantiate new objects, and then somehow replace references to existing Card
objects with these new wrapped objects, and back again, at the end of turn. Which seemed a bit more involved than modifying the ancestry chain directly.
I suppose my question has 2 parts. Is it possible to remove a specific ancestor from a class's ancestor chain using pure ruby (since mixology gem already suggests it is possible to do with c/Java extensions)? If not, what would be a suitable workaround to get a similar behaviour?
CodePudding user response:
What you are trying to achieve is a very bad pattern. You should almost never use prepend
or include
dynamically, but model your code around the concepts you (and people possibly reading your code) do understand.
What you probably want to do is to create (some kind of) a Delegator called CardAffectedByEnvironment
- and then instead of doing Card.new(x)
, you will always do CardAffectedByEnvironment.new(Card.new(x), env)
, where env
will keep all your state changes, or add a method real_cost
that would calculate things based on your cost
and environment and use this method.
Below is a code with CardAffectedByEnvironment
that would maybe describe better how I would assume it would work:
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