"Jamis Buck" <jgb3 / email.byu.edu> schrieb im Newsbeitrag
news:4127FC2B.60608 / email.byu.edu...
> Just wanted to share a recent "discovery" of mine. (I'm sure many of you
> have already discovered this some time ago, but I've found value in list
> members sharing their discoveries, so I figured I'd try and give
> something back.)
>
> I'm polishing Copland, getting it ready for RubyConf (and my "Dependency
> Injection in Ruby" presentation), and I needed some way to make certain
> operations available during an early stage of an object's lifecycle, but
> disallowed at a later stage. I was originally thinking of two options:
>
> 1) don't do anything about it. Expect undefined behavior if the user
> tries do to a disallowed operation.
>
> 2) use reflection to dynamically remove the methods when they are no
> longer acceptable.
>
> After some thought, a better solution came to me. Create a submodule of
> the class called "Fixated". Then, when the object needs to change, call
> some "fixate" method of the object, which will extend the object with
> the "Fixated" module. The Fixated module then implements those
> disallowed methods by simply raising an exception:
>
>    class ServicePoint
>      def add_pending_interceptor( a )
>        ( @pending_interceptors ||= [] ).push a
>      end
>
>      def fixate!
>        self.extend Fixated
>      end
>
>      def fixated?
>        false
>      end
>
>      module Fixated
>        def add_pending_interceptor( *args )
>          raise NotImplementedException,
>            "cannot add pending interceptors to fixated object"
>        end
>
>        def fixate!
>          # does nothing
>        end
>
>        def fixated?
>          true
>        end
>      end
>    end
>
>    svc = ServicePoint.new
>    svc.add_pending_interceptor( "mock object" )
>    svc.fixate!
>
>    # the next line raises an exception
>    svc.add_pending_interceptor( "mock object #2" )
>
> Anyway, it works wonderfully for me. :) Here's where those of you more
> knowledgable point out a much more efficient way. ;)

Some remarks: #freeze might do what you want if you want to prevent any
change after a certain point in time.

I would not use NotImplementedException because that generates the false
impression that an undefined method was invoked while really it was a state
violation (in Java you'd use IllegalStateException for this).

While using extend might work it's not easy to reverse (if you want to reset
an instance's state later for example).

So I'd rather use either strategy pattern (or state pattern, they are quite
similar) or model states with constants (less typing than those patterns but
better documentation IMHO because each method states clearly what
precondition it has; and might be a bit more runtime overhead).

class ServicePoint
  def initialize
    @state = :initial
  end

  def add_pending_interceptor( a )
    pre :initial
    ( @pending_interceptors ||= [] ).push a
  end

  def fixate!
    @state = :fixed
  end

  def fixated?
    @state == :fixed
  end

private

  def pre(*states)
    raise "State error" unless states.include? @state
  end

end


?> svc = ServicePoint.new
=> #<ServicePoint:0x101985c0 @state=:initial>
>> svc.add_pending_interceptor( "mock object" )
=> ["mock object"]
>> svc.fixate!
=> :fixed
>>
?> # the next line raises an exception
?> svc.add_pending_interceptor( "mock object #2" )
RuntimeError: State error
        from (irb):41:in `pre'
        from (irb):45:in `add_pending_interceptor'
        from (irb):64
        from (null):0

From here it's just a small step to using full preconditions:

  def pre
    raise "Precondition Error" unless yield
  end

  def add_pending_interceptor( a )
    pre { @state == :initial }
    ( @pending_interceptors ||= [] ).push a
  end

Kind regards

    robert