Editing Topic: RCR198 Project: RCR | RCRchive home

RCR 198: Publish/Subscribe Method Visibility

submitted by minam on Fri Jan 23 2004 09:52:21 AM -0800

Status: pending


Abstract

Restricting method visibility is often used to hide the internal functionings of an object from clients. However, there are times when a limited API, visible to some objects but not to all, is desired. This RCR proposes an implementation of a publish/subscribe-based approach to method visibility, which may safely be used in conjunction with Ruby's existing public/private method visibilities. This RCR supercedes #189, proposing an improved syntax and implementation of this approach to method visibility.

Problem

Ruby currently has no clean way to selectively export methods from an object. Such selective export is needed when public and private versions of a class's API are desired (as with the State design pattern). The Eiffel language provides one approach to selective export, in which a designer may specify that certain methods are only accessible to specific classes. RCR #187 (which is superceded by this RCR) proposed such a feature be added to Ruby. However, such an approach (as Matz pointed out in his comment to that RCR) is "against the policy of extensibility," meaning that a client that was originally unknown to the designer cannot ever gain access to those selectively exported methods, except by inheriting from any of the classes that were the original targets of the export. Thus, inheritance becomes used solely as a means of subscribing to a particular set of features of a class.

Proposal

Rather than using inheritance to subscribe to an exported API, Matz, in his graduation thesis, created an object-oriented language that used an explicit publish/subscribe approach to selective export. Although his implementation was dependent on static typing, this RCR uses the same idea in a more "Ruby-esque" fashion.

The following example implements the State design pattern (trivially). Consider a system that implements a sequence of integers as a class. Calls to its next method return subsequent values from the sequence. The sequence also allows its state to be saved and restored, via an opaque SequenceState object, thereby allowing the sequence to be restarted from some previous value.

Here is where the selective export is useful. The SequenceState object should be opaque to everyone except the Sequence itself (or to other classes that want to mimic the behavior of a Sequence). The Sequence object must be able to query and modify the values in the SequenceState object that represent the Sequence's internal state.

Here is the implementation of the SequenceState class:

  class SequenceState
    def get_counter
      @counter
    end

    def set_counter( counter )
      @counter = counter
    end

    publish :StateAccess, :get_counter, :set_counter
  end
Notice the last line; this publishes a new profile, called "StateAccess", and indicates that both "get_counter" and "set_counter" should be in this profile. Putting those methods in the profile also implicitly makes those methods private.

The Sequence class is implemented thus:

  class Sequence
    def initialize( start=0 )
      @counter = start-1
    end

    def next
      @counter += 1
      @counter
    end

    def get_state
      state = SequenceState.new.subscribe( :StateAccess ) do |access|
        access.set_counter @counter
      end
      return state
    end

    def restore_state( state )
      state.subscribe( :StateAccess ) do |access|
        @counter = access.get_counter
      end
    end
  end
Notice the "get_state" and "restore_state" methods. In "get_state", a SequenceState object is instantiated. Then, the caller subscribes to the StateAccess profile of the new object. A proxy object, which has access to methods of that profile (including "set_counter"), is then passed to the corresponding block.

In restore_state, the caller subscribes to the StateAccess profile of the state object, which gives them access to the "get_counter" method of the state, thus allowing them to restore the value of the counter.

Analysis

The impact of this feature on the rest of the language would be negligible. Those who would not want to use this feature would not be constrained to do anything special to avoid it--the default would be as it currently is, with methods all being public unless explicitly made protected or private. Those developers who desire this feature may use it without clients either knowing or caring, unless the clients desire to access those methods that require subscription.

The subscription process and use of a proxy object do incur performance penalties, which should be understood by developers that wish to use this feature. However, the performance hit on calling through a proxy is not too bad (about 2-3 times slower than a direct method invocation). The subscription hit is more significant (about 20x slower than doing without a subscription). The following table shows the results of some benchmarks. Each test involved 1,000,000 method invocations.

  Direct:     1.024s
  Subscribed: 2.769s

  1,000,000 subscriptions: 24.26s

In an attempt to improve performance, I wrote a C version of the pubsub module. Although it did not improve the performance of the call through the proxy significantly (2.5s for 1,000,000 method invocations), it did improve the subscription performance (7.5s for 1,000,000 subscriptions).

However, it should be stated that use of this feature is intended for those relatively infrequent occassions when an object needs access to the internal state of another object. In general, this shouldn't happen in tight iterations, where speed is necessary, and so the lower performance of this feature should not be a concern.

It may be argued that this kind of visibility limitation is no limitation at all, since any object at all may subscribe to a service, if it knows about the service. However, the point (in this case) is not to limit visibility. Designers still have access to private and protected access modes, which prevent outside access to methods so annotated. Selective export is not about preventing access, but controlling access. It ensures that the clients must know the proper "protocol" for communicating with an object.

Given that the reference implementation is functional, it may be asked why this RCR is necessary (since the same functionality is now available as a module). There are two reasons:

  1. Although it may be implemented completely in Ruby, there are limitations to such an implementation (among them, speed, since the existance of a proxy object inside of each subscription block introduces overhead for every method call made on that proxy). It may be necessary to implement part, if not all, of the functionality of this in C--perhaps even making minor modifications to the interpreter itself.
  2. Making such functionality a standard part of the Ruby distribution--whether it is implemented purely in Ruby or not--would encourage developers to make use of it. If it must be distributed with every project that uses it, or if it must be installed before projects that use it may be installed, it makes it more difficult for it to be properly adopted.

Implementation

The reference implementation is available on the RAA, and is called . It is implemented in pure Ruby, and thus suffers from a few drawbacks (such as the overhead for method calls on the proxy objects, as described above). Nevertheless, it functions sufficiently well that I would probably use it if this RCR fails to be accepted.

The module referenced above is implemented by reopening Object and adding functionality to it, thereby granting both publish and subscribe to every class. It might also be implemented as a module which is included by those classes that need it; this approach might be more desirable if the Object class needs to be kept "lean".



Back to RCRchive.


RCR Submission page and RCRchive powered by Ruby, Apache, RuWiki (modified), and RubLog