0

Given a set of class definitions:

class Application
    def self.open_current
        return Current.new()
    end
end
class Current
    def get_row(row)
        Row.new(row)
    end
end
class Row
    def get_col(row)
        #...
    end
end

Design a Proxy class which will:

  • Create Proxy<<Class>> versions of each class which are extendable via

    class ProxyApplication
        def myMethod()
            #...
        end
    end
    #...
    
  • Wrap all return values of all methods in each class such that a proxied class is always used instead of a standard class. I.E.

    app = Proxy.new(Application) #app == ProxyApplication
    current = app.open_current   #current == #<ProxyCurrent>
    

Ultimately, the definition of Proxy must be dynamic rather than static definitions.

I've been working on this problem for about 6 hours now. I've got the following code. This includes 3 sections:

  • Initial class setup
  • Proxy class definition
  • Testing proxy class

Currently I've got to the point where pApplication=Proxy.new(Application) returns #<Proxy> and pApplication.open_current returns #<ProxyCurrent> which seems kind of on the correct line. However currently it errors when delegate.rb tries to call test() with 2,3 arguments instead of 0...

But my question is, realistically am I going about this correctly? Is using SimpleDelegator the easiest way to do this? One current problem is I'm basically having to add new functionality to the existing SimpleDelegator. I've also looked at using Forwardable, but having to delegate methods manually is not where I want to go with this project, if possible.

Any ideas?

Sancarn
  • 2,575
  • 20
  • 45
  • So Every class should be a Proxy Class? e.g `ProxyString`, `ProxyHash`, etc. Also why should calling `test` with arguments not error out neither definition of `test` accepts arguments? – engineersmnky May 30 '18 at 15:29
  • Yup, Every class should become a proxy class. ProxyString, ProxyHash galore! And @Why should calling test with arguments not error - Because it is never called with arguments... Look at the code again `puts PApplication.test()` <-- No arguments – Sancarn May 30 '18 at 17:35

1 Answers1

0

Due to numerous limitations with the initial idea of packing all the calls into a single class, I redesigned it a bit, but it works in exactly the same way.

def proxy__enwrap(obj)
    isClass = obj.is_a?(Class)
    oldClass = isClass ? obj : obj.class
    sNewClass = "Proxy#{oldClass.to_s}"
    code = <<-EOF
        class #{sNewClass}
            include InstanceProxy
            def self.__cinit__(obj)
                @@__cobj__ = obj
            end
            def self.__cget__
                @@__cobj__
            end
            def self.method_missing(m,*args,&block)
                if @@__cobj__.respond_to? m
                    retVal = @@__cobj__.public_send(m,*args,*block)
                    return proxy__enwrap(retVal)
                else
                    puts "ERROR " + m.to_s + "(" + args.to_s + ") + block?"
                    #Throw error
                end
            end
        end
        #{sNewClass}.__cinit__(#{oldClass.to_s})
        if isClass
            return #{sNewClass}
        else
            return #{sNewClass}.new(obj)
        end
    EOF

    ::Kernel.eval(code)
end

module InstanceProxy
    def method_missing(m,*args,&block)
        retVal = @__obj__.__send__(m,*args,&block)
        return proxy__enwrap(retVal)
    end
    def initialize(obj)
        @__obj__ = obj
    end
end

XXApplication = Application
::Object.const_set "Application", proxy__enwrap(Application)

Currently the only issue is that the object doesn't wrap correctly around yielded objects... I'm not sure if that's even possible though.

Edit:

I've improved the system to also wrap objects passed in as blocks:

def proxy__enwrap(obj)
    isClass = obj.is_a?(Class)
    oldClass = isClass ? obj : obj.class
    sNewClass = "Proxy#{oldClass.to_s}"
    code = <<-EOF
        class #{sNewClass}
            include InstanceProxy
            def self.__cinit__(obj)
                @@__cobj__ = obj
            end
            def self.__cget__
                @@__cobj__
            end
            def self.to_ary
                #fix for puts (puts calls to_ary. see: https://stackoverflow.com/questions/8960685/ruby-why-does-puts-call-to-ary)
                [self.to_s]
            end
            def self.method_missing(m,*args,&block)
                #Wrap block arguments:
                newBlock = Proc.new {}
                if block_given?
                    newBlock = Proc.new do |*args|
                        args = args.map {|arg| proxy__enwrap(arg)}
                        block.call(*args)
                    end
                end

                #Call delegated functions. Raise error if object doesn't respond to method.
                #Return wrapped value
                if @@__cobj__.respond_to? m
                    retVal = @@__cobj__.public_send(m,*args,*block)
                    return proxy__enwrap(retVal)
                else
                    raise ArgumentError.new("Method '\#\{m.to_s}' doesn't exist.")
                end
            end
        end
        #{sNewClass}.__cinit__(#{oldClass.to_s})
        if isClass
            return #{sNewClass}
        else
            return #{sNewClass}.new(obj)
        end
    EOF

    ::Kernel.eval(code)
end

module InstanceProxy
    def method_missing(m,*args,&block)
        #Wrap block arguments:
        newBlock = Proc.new {}
        if block_given?
            newBlock = Proc.new do |*args|
                args = args.map {|arg| proxy__enwrap(arg)}
                block.call(*args)
            end
        end

        #Call delegated functions. Raise error if object doesn't respond to method.
        #Return wrapped value
        if @__obj__.respond_to? m
            retVal = @__obj__.__send__(m,*args,&newBlock)
            return proxy__enwrap(retVal)
        else
            raise ArgumentError.new("Method '#{m.to_s}' doesn't exist.")
        end
    end
    def initialize(obj)
        @__obj__ = obj
    end
    def to_ary
        #fix for puts (puts calls to_ary. see: https://stackoverflow.com/questions/8960685/ruby-why-does-puts-call-to-ary)
        [self.to_s]
    end
end

#
XXApplication = Application

#Silence warnings of overwriting constant
original_verbosity = $VERBOSE
$VERBOSE = nil
    ::Object.const_set "Application", proxy__enwrap(Application)
$VERBOSE = original_verbosity
Sancarn
  • 2,575
  • 20
  • 45
  • But this does not do what you had requested. You asked that all objects be wrapped as proxy objects so where is `ProxyString` in say `Application.to_s`? – engineersmnky May 30 '18 at 18:44
  • @engineersmnky Well the `to_s` method doesn't belong to `Application`. It belongs to the `BasicObject` class. If `Application` returned a String then you do get a [`ProxyString`](https://i.imgur.com/JzYar9B.png). But you are correct that `to_s` indeed does not return a `ProxyString` – Sancarn May 30 '18 at 20:21
  • Well technically it belongs to `Object` and in addition if `ProxyApplication` returned a `String` it would still just be a `String` but can you explain the purpose of this pattern if you can so easily break the encapsulation? – engineersmnky May 30 '18 at 20:36
  • Specifically a ruby C plugin I use implements ruby class instances in a way that is not extendable by conventional means. (It does not inherit methods from the class directly). So this pattern is only for wrapping these classes in an extendable and convenient way. – Sancarn May 30 '18 at 20:55