Estoy tratando de desarrollar una aplicación de consola de juegos de cartas simple en Ruby como un proyecto favorito. Una de las interacciones que me gustaría agregar a este juego de cartas es algo como "Durante tu turno, puedes comprar cartas como si costaran X menos". Ahora, pensé en modelar este comportamiento con la ayuda de decoradores mixin. Suponiendo la siguiente clase:
class Card attr_reader :cost def initialize(cost) self.cost = cost end def play(*_args) raise NotImplementedError, "Override me!" end private attr_writer :cost endPensé que una solución simple sería algo como esto:
class CardThatDiscounts < Card def play(*_args) @mod = Module.new do def cost super - 1 end end Card.prepend(@mod) end endEsto me daría la flexibilidad de apuntar a la clase en sí o a instancias específicas de las tarjetas. Sin embargo, no estoy seguro de cómo revertir este efecto al final del turno. Vi algunas preguntas similares a esta, cuyas respuestas decían algo como:
Este enfoque realmente no funcionará para mí, ya que estoy decorando métodos existentes en lugar de agregar otros nuevos. Tal vez podría cambiar el nombre del método existente cuando antepongo el módulo y luego devolverlo, pero ¿qué sucede en caso de que quiera encadenar varios decoradores de este tipo?
La mixología es una joya antigua y parece que ya no se mantiene. Aunque, según la documentación, es exactamente lo que necesito. Sin embargo, la implementación se ocupa principalmente de los componentes internos de Ruby, que lamentablemente no entiendo. ¿No es posible modificar una cadena de ancestros de clase desde Ruby, o necesito la extensión c/Java para esto?
Consideré otras alternativas, como SimpleDelegator o DelegateClass , pero ambas requieren que cree instancias de nuevos objetos y luego, de alguna manera, reemplace las referencias a los objetos Card existentes con estos nuevos objetos envueltos, y viceversa, al final del turno. Lo que parecía un poco más complicado que modificar la cadena de ascendencia directamente.
Supongo que mi pregunta tiene 2 partes. ¿Es posible eliminar un ancestro específico de la cadena de ancestros de una clase usando rubí puro (dado que mixology gem ya sugiere que es posible hacerlo con extensiones c/Java)? Si no, ¿cuál sería una solución adecuada para obtener un comportamiento similar?
Lo que estás tratando de lograr es un patrón muy malo. Casi nunca debe usar prepend o include dinámicamente, pero modele su código en torno a los conceptos que usted (y las personas que posiblemente lean su código) entienden.
Lo que probablemente quieras hacer es crear (algún tipo de) un Delegador llamado CardAffectedByEnvironment - y luego, en lugar de hacer Card.new(x) , siempre harás CardAffectedByEnvironment.new(Card.new(x), env) , donde env mantendrá todos sus cambios de estado, o agregará un método real_cost que calcularía las cosas en función de su cost y entorno y usaría este método.
A continuación se muestra un código con CardAffectedByEnvironment que tal vez describa mejor cómo asumiría que funcionaría:
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