Supporting multiple versions of an API

Any recommendations on the management of API versions. I have a case in
which it is important that my library support all versions.

For example, currently I am basically doing:

  module MyLib
    module V0
      module Main
        ...
      end
    end

    class Main
      def initialize(version)
        extend MyLib.const_get("V#{version}")::Main
      end
    end
  end

Does that seem like a good approach. Or is it overkill? Is there a better
way to handle this?

Thanks.

For what it's worth, I kind of like this solution, but I get the
feeling there's something not quite right about it. An alternative
could be to strategically name source files, and `require` the
appropriate one; in that case each version-specific file could
redefine the relevant part of Main. E.g.

  # File: main.rb
  class Main
    def initialize version
      require "./main-v#{version}.rb"
    end
    # universal code
  end

  # File main-v0.rb
  class Main
    # version-specific code
  end

It splits things up into maintainable files quite nicely, and also has
less parsing (for what that's worth.) However it wouldn't work if you
need two different versions of Main in the one program.

Alternatively you could create a factory, which is essentially what
you've done, but might be a bit more recognisable to, or better
understood by, maintainers. E.g.

  class Main
    # ... universal code
  end
  class MainV0 < Main
     # ... version-specific code
  end
  module MainFactory
    def self.create version
      const_get("MainV#{version}").new
    end
  end

It's mostly a fluff change to what you've already got, but it means
each object has the version-specific API as their class rather than a
mixed-in module (i.e. no real difference as far as I'm aware), and
it's clear that the MainFactory is a factory and that each of the
MainVx classes are what it instantiates, whereas a partially
implemented Main class with some strange magic in its #initialize
method might be a bit less clear.

That all said good documentation will almost always trump recognisable
patterns or otherwise self-documenting code.

I'd be interested to see other peoples' comments.

···

On 31 July 2012 07:54, Intransition <transfire@gmail.com> wrote:

Any recommendations on the management of API versions. I have a case in
which it is important that my library support all versions.

For example, currently I am basically doing:

  module MyLib
    module V0
      module Main
        ...
      end
    end

    class Main
      def initialize(version)
        extend MyLib.const_get("V#{version}")::Main
      end
    end
  end

Does that seem like a good approach. Or is it overkill? Is there a better
way to handle this?

Thanks.

--
  Matthew Kerwin, B.Sc (CompSci) (Hons)
  http://matthew.kerwin.net.au/
  ABN: 59-013-727-651

  "You'll never find a programming language that frees
  you from the burden of clarifying your ideas." - xkcd

For what it's worth, I kind of like this solution, but I get the
feeling there's something not quite right about it.

Same here, but it's probably just b/c its unusual --its not something you
can really do in any other language but Ruby, that I know of. Granted it is
not perfectly ideal in that modules don't "inherit" exactly in the same way
classed, but that turns out not to be much of an issue in this case. The
whole notion of this was a sort of inverted factory, so the forward facing
API looks normal, e.g. `Main.new`, but versioning still occurs under the
hood.

An alternative
could be to strategically name source files, and `require` the
appropriate one; in that case each version-specific file could
redefine the relevant part of Main. E.g.

  # File: main.rb
  class Main
    def initialize version
      require "./main-v#{version}.rb"
    end
    # universal code
  end

  # File main-v0.rb
  class Main
    # version-specific code
  end

It splits things up into maintainable files quite nicely, and also has
less parsing (for what that's worth.) However it wouldn't work if you
need two different versions of Main in the one program.

Yep. Exactly why that approach does work in my case.

Alternatively you could create a factory, which is essentially what
you've done, but might be a bit more recognisable to, or better
understood by, maintainers. E.g.

  class Main
    # ... universal code
  end
  class MainV0 < Main
     # ... version-specific code
  end
  module MainFactory
    def self.create version
      const_get("MainV#{version}").new
    end
  end

It's mostly a fluff change to what you've already got, but it means
each object has the version-specific API as their class rather than a
mixed-in module (i.e. no real difference as far as I'm aware), and
it's clear that the MainFactory is a factory and that each of the
MainVx classes are what it instantiates, whereas a partially
implemented Main class with some strange magic in its #initialize
method might be a bit less clear.

Yes, that's the traditional factory approach. I actually would not want to
use "MainFactory", as don't want it to be explicit. But it occurs to me
that I could have redefined `Main.new` as a factory method and done it that
way. And the more I think about it the more that seems like a better
approach. It would work well for Main as well as other classes within it.

···

On Monday, July 30, 2012 9:28:04 PM UTC-4, Matthew Kerwin wrote:

It seems that in your case the caller decides on the version. Is that correct?

What about this?

file mylib.rb:

module MyLib
  # manual
  autoload :V0 'mylib/v0'
  autoload :V1 'mylib/v1'

  # generated
  %w{V0 V1}.each do |ver|
    autoload ver.to_sym "mylib/#{ver.downcase}"
  end

  # the default
  Default = V1

  # to make access to the current version easy:
  def self.const_missing(c)
    Default.const_get(c)
  end
end

and in mylib/v0 etc.

module MyLib
  module V0
    class Main; end
  end
end

Usage

# current
m = MyLib::Main.new(...)

# explicit
m = MyLib::V0::Main.new(...)

The const_missing trick works only if there are no name conflicts, of course.

Kind regards

robert

···

On Thu, Aug 2, 2012 at 4:47 PM, Intransition <transfire@gmail.com> wrote:

Yes, that's the traditional factory approach. I actually would not want to
use "MainFactory", as don't want it to be explicit. But it occurs to me that
I could have redefined `Main.new` as a factory method and done it that way.
And the more I think about it the more that seems like a better approach. It
would work well for Main as well as other classes within it.

--
remember.guy do |as, often| as.you_can - without end
http://blog.rubybestpractices.com/

That should be "does not".

···

On Thursday, August 2, 2012 10:47:19 AM UTC-4, Intransition wrote:

It splits things up into maintainable files quite nicely, and also has
less parsing (for what that's worth.) However it wouldn't work if you
need two different versions of Main in the one program.

Yep. Exactly why that approach does work in my case.

Yes, but it is actually via YAML file. It will read in, eg.

···

On Thursday, August 2, 2012 12:14:27 PM UTC-4, Robert Klemme wrote:

On Thu, Aug 2, 2012 at 4:47 PM, Intransition <transfire@gmail.com> wrote:

> Yes, that's the traditional factory approach. I actually would not want
to
> use "MainFactory", as don't want it to be explicit. But it occurs to me
that
> I could have redefined `Main.new` as a factory method and done it that
way.
> And the more I think about it the more that seems like a better
approach. It
> would work well for Main as well as other classes within it.

It seems that in your case the caller decides on the version. Is that
correct?

  ---
  revision: 1
  ...

Then when it does a `Main.load` on the file, it will look for the
`revision` field and determine which version of the API to use.

What about this?

file mylib.rb:

module MyLib
  # manual
  autoload :V0 'mylib/v0'
  autoload :V1 'mylib/v1'

  # generated
  %w{V0 V1}.each do |ver|
    autoload ver.to_sym "mylib/#{ver.downcase}"
  end

  # the default
  Default = V1

  # to make access to the current version easy:
  def self.const_missing(c)
    Default.const_get(c)
  end
end

and in mylib/v0 etc.

module MyLib
  module V0
    class Main; end
  end
end

Usage

# current
m = MyLib::Main.new(...)

# explicit
m = MyLib::V0::Main.new(...)

The const_missing trick works only if there are no name conflicts, of
course.

Adding some convenience, nice. I like autoload too, but word has it, it is
being deprecated (albeit in the distant future).

So it's *not* the user deciding on the API version! That's a
different situation. Or does the user present the loaded YAML file?
Can you be a bit more specific about your usage scenario?

Kind regards

robert

···

On Thu, Aug 2, 2012 at 10:08 PM, Intransition <transfire@gmail.com> wrote:

On Thursday, August 2, 2012 12:14:27 PM UTC-4, Robert Klemme wrote:

On Thu, Aug 2, 2012 at 4:47 PM, Intransition <transfire@gmail.com> wrote:

> Yes, that's the traditional factory approach. I actually would not want
> to
> use "MainFactory", as don't want it to be explicit. But it occurs to me
> that
> I could have redefined `Main.new` as a factory method and done it that
> way.
> And the more I think about it the more that seems like a better
> approach. It
> would work well for Main as well as other classes within it.

It seems that in your case the caller decides on the version. Is that
correct?

Yes, but it is actually via YAML file. It will read in, eg.

Then when it does a `Main.load` on the file, it will look for the `revision`
field and determine which version of the API to use.

--
remember.guy do |as, often| as.you_can - without end
http://blog.rubybestpractices.com/

Well ultimately the user creates the files, in some fashion or another. So
it just depends on how you look at it, I guess.

I'll put a link to the project once it's a bit more settled. Right now the
project is in the demonic throws of the Over-Thought.

···

On Thursday, August 2, 2012 5:16:34 PM UTC-4, Robert Klemme wrote:

So it's *not* the user deciding on the API version! That's a
different situation. Or does the user present the loaded YAML file?
Can you be a bit more specific about your usage scenario?