Then I ran into a situation in my extreme solitaire project. I needed to have my SolitaireBoard (not sure if I like that name) class be able to modify the various stacks of cards it used, so those stacks needed to be mutable. However, when I returned those stacks to the player code I didn't want the player to be able to modify them, as it could let the player learn some things they shouldn't know (removing all the cards from a stack would cause the next hidden card to be shown, and the player shouldn't know what's there until they commit to a move).
I started thinking about how I would solve this in Java or C++. I could refactor my code so that I'd have IStackOfCards and IMutableStackOfCards interfaces, and have the board return IStackOfCards objects. The player could still try manually casting to a mutable stack, which we don't want (no cheating!), so the next idea is to actually write a StackOfCardsProxy class that wraps the mutable stack but doesn't provide any mutating functions (or throws exceptions if they're called). This would work, but it would involve a bunch of code that felt unecessary.
Ruby doesn't really use interfaces, so implementing a StackOfCardsProxy would involve a little less code (don't have to declare the interface), but still felt clunky. Then I remembered two important things:
- All the methods on my classes that cause the object to mutate end in a "!"
- Methods are just messages, so I can write a generic Proxy easily.
# Proxies any object but doesn't allow any mutating method (methods ending in # !) from being called. class ImmutableProxy def initialize(target) @target = target end def method_missing(name, *args, &block) raise "Mutable methods not allowed" unless allowed?(name) @target.send(name, *args, &block) end def respond_to?(name, include_private=false) return false unless allowed?(name) @target.respond_to?(name, include_private) end private def allowed?(name) ! name.to_s.end_with?('!') || !@target.respond_to?(name) end end
The reason for the
respond_to?
check in allowed? is so that calling ! methods that don't actually exist behave the same. We only want to throw the custom error if it's a method that does exist but isn't allowed by the proxy. I'm still debating whether or not I want to keep it this way or just have it raise a NoMethodError for all ! methods (completely hide the fact that it could be mutable).Right now this proxy doesn't handle attribute setters, but the classes that I'm wrapping with it don't use those right now. I'd like to handle those as well, but I'm not quite sure how.
Proxy object in ruby that blocks mutating functions.
No comments:
Post a Comment