Design pattern question

Hi group,

I wonder whether you could give me any advice on how to structure some
code?

Its pretty simple really. I want to be able to call an API like this:

SourceControl.checkout
SourceControl.update

Etc

I want to be able to plugin different source control systems that
conform to the API.

My initial thought was just to go with a simple C++ style
wrapper/delegate approach:

module SourceControl

def self.checkout(repository, dest, options={})
@sourceControl.checkout(repository, dest, options)
end

def self.update(path, options={})
@sourceControl.update(parth, options)
end

class SourceControlAPI

def checkout(repository, dest, options={})
end

def update(path, options={})
end

end

end

But I bet there is a nicer way, perhaps without having to duplicate the
API?

Any pointers appreciated.

Cheers,
James

Hi,

I wonder whether you could give me any advice on how to structure some code?
Its pretty simple really. I want to be able to call an API like this:

SourceControl.checkout

SourceControl.update

Maybe this one gives some good pointers…

hope it helps,
saji

Saji N Hameed,
ARC-ENV, Center for Advanced Information Science and Technology,
University of Aizu, Tsuruga, Ikki-machi,
Aizuwakamatsu-shi, Fukushima 965-8580,
Japan

Tel: +81242 37-2736
Fax:+81242 37-2760
email: [email protected]
url: http://www.u-aizu.ac.jp
bib: Web of Science

James F. wrote in post #1055131:

Its pretty simple really. I want to be able to call an API like this:

SourceControl.checkout
SourceControl.update

You better do

sc.checkout
sc.update

i.e. not use a constant but an instance variable.

I want to be able to plugin different source control systems that
conform to the API.

Well, in Ruby there are no interfaces so you can, but do not need to use
inheritance.

My initial thought was just to go with a simple C++ style
wrapper/delegate approach:

module SourceControl

def self.checkout(repository, dest, options={})
@sourceControl.checkout(repository, dest, options)
end

def self.update(path, options={})
@sourceControl.update(parth, options)
end

class SourceControlAPI

def checkout(repository, dest, options={})
end

def update(path, options={})
end

end

end

But I bet there is a nicer way, perhaps without having to duplicate the
API?

Especially, what advantages does it have?

I’d probably simply do

class CVS
def checkout(dest, options={})
end

def update(path, options={})
end
end

class SVN
def checkout(dest, options={})
end

def update(path, options={})
end
end

class Perforce
def checkout(dest, options={})
end

def update(path, options={})
end
end

and then

connection

sc = SVN.new(“svn-repo.my.network.com”, “James”, “p@ssword”)

repository

sc.checkout(“repo”, “/tmp/work”) do |repo|
repo.update(“foo”)
File.open(repo.path + “foo”, “a”) {|io| io.write("a change)}
repo.submit(“foo”)
end # auto commit

sc.close # needed?

You could even make SVN.new accept a block which receives “sc” and
cleans up in ensure regardless how the block terminates.

http://blog.rubybestpractices.com/posts/rklemme/002_Writing_Block_Methods.html

Kind regards

robert

On Thu, Apr 5, 2012 at 4:25 PM, James F.
[email protected] wrote:

end
end
end

That’s just a mechanism to generate delegation. And you redefine
methods all the time. I don’t think meta programming is necessary at
all here.

class SourceControlAPI

def checkout(repository, dest, options={})
end

def update(path, options={})
end

Those are not really needed. And if you want to have them then I’d
make them either contain a default implementation if there exists one
or throw an exception which explains that this method must be
implemented for the class.

end

SourceControl.init(SVN.new)
SourceControl.checkout(‘foo’, ‘bar’)

You are hiding specific state in a constant. I don’t like that
because it will prevent accessing multiple different source control
systems in the same program or from multiple threads. I find that
requirement to be able to invoke all the methods through the constant
leads to a situation where you severely cripple modularity and
reusability without really gaining something.

Kind regards

robert

True, but I want to document the API somewhere central. I raise exceptions
saying the method must be implement.

That’s not very ruby-like at all.

We’ll have to agree to disagree on this one, Ryan. Abstract classes
are pretty silly in Ruby. Then again, I find subclassing distasteful
in general, so this might just be a style/preference thing.

On Apr 5, 2012, at 09:19 , Steve K. wrote:

True, but I want to document the API somewhere central. I raise exceptions
saying the method must be implement.

That’s not very ruby-like at all.

Yes it is, in that it is good design. Otherwise, why would we have
NotImplementedError? We use it all the time in our projects that
implement an abstract interface:

graph/dev/lib/dep_analyzer.rb: raise NotImplementedError, “subclass
responsibility”
graph/dev/lib/dep_analyzer.rb: raise NotImplementedError, “subclass
responsibility”
graph/dev/lib/dep_analyzer.rb: raise NotImplementedError, “subclass
responsibility”

W dniu 5 kwietnia 2012 21:27 użytkownik Steve K.
[email protected] napisał:

We’ll have to agree to disagree on this one, Ryan. Abstract classes
are pretty silly in Ruby. Then again, I find subclassing distasteful
in general, so this might just be a style/preference thing.

Abstract classes are sometimes useful (although rarely). For example,
how else could you do the kind of database adapter handling Sequel[1]
does?

[1] sequel | RubyGems.org | your community gem host

– Matma R.

Abstract classes are sometimes useful (although rarely). For example,
how else could you do the kind of database adapter handling Sequel[1]
does?

Use duck typing.

This:

class Abstract
def foo
throw NotImplementedException
end
end

class Foo < Abstract
def foo
puts “in foo!”
end
end

class Bar < Abstract
def foo
puts “in bar’s foo”
end
end

Is the exact same thing as

class Foo
def foo
puts “in foo!”
end
end

class Bar
def foo
puts “in bar’s foo”
end
end

in fact…

class Baz
end

Baz.new.foo # => NoMethodError

class Baz < Abstract
end

Baz.new.foo # => NotImplementedError

Almost the exact same thing. Just a different exception.

Insert a ‘def bar; puts “bar”; end’ into my Abstract class, the end
point is still the same. Adding an empty method that throws a slightly
different error doesn’t really do anything worthwhile in my opinion.
If it makes you happy, then do it.

Sequel provides a lot of helper methods and aliases in the abstract
class. Just look thru the docs of Sequel::Database (this is the
abstract class) and it’s list of methods - you only need to implement
a handful, the rest are helpers.

– Matma R.

On Apr 5, 2012, at 15:05 , Steve K. wrote:

Adding an empty method that throws a slightly
different error doesn’t really do anything worthwhile in my opinion.

On Apr 5, 2012, at 15:05 , Steve K. wrote:

Adding an empty method that throws a slightly
different error doesn’t really do anything worthwhile in my opinion.

Because being explicit about the interactions across classes isn’t
worthwhile? Having an explicit place to document the contract isn’t
worthwhile? Sorry… I’m not buying it. From your example [with my
edits]:

class Abstract
[# contract doco]
def foo
[raise] NotImplemented[Error][, “helpful explanation–much more so than
NoMethodError”]
end
end

Now I know where I can document the foo contract and suggest proper ways
to implement it. You lose that when you throw that out the window and go
with duck typing.

That reminds me… How do you suggest a beginner know the difference
between a typo and an unimplemented ducktyped method interface?