Velocity Reviews

Velocity Reviews (http://www.velocityreviews.com/forums/index.php)
-   Ruby (http://www.velocityreviews.com/forums/f66-ruby.html)
-   -   What object-configuration approach to use? (http://www.velocityreviews.com/forums/t813088-what-object-configuration-approach-to-use.html)

Mike Williams 01-14-2004 04:04 AM

What object-configuration approach to use?
 
Consider a class that supports a number of configurable properties.

As a concrete example, take a class that represents an HTML link. It
requires that a "url" and link "content" be provided. It also supports two
optional attributes: "title" and "target".

Link
url
content
?title
?target

I've seen a bunch of different ways of configuring such objects ...

----
1. SettingAttributes

The obvious way is just to create the instance, and then call
writer-methods to set attributes, e.g.

class Link
def initialize(url, content)
@url = url
@content = content
end
attr_accessor :url, :content, :link, :target
end

link = Link.new(url, "here")
link.title = "the whole story"

----
2. OptionalArguments

You can have initialize() support optional (positional) arguments, e.g.

class Link
def initialize(url, content, title = nil, target = nil)
@url = url
@content = content
@title = title
@target = target
end
attr_accessor :url, :content, :link, :target
end

link = Link.new(url, "here", "the whole story")

but, this doesn't scale well: if you have a bunch of optional attributes,
you have to start inserting "nil":

link2 = Link.new(url, "here", nil, "helpFrame")

----
3. OptionalArgumentMap

Another alternative is pass in a map of named optional args, e.g.

class Link
def initialize(url, content, args = {})
@url = url
@content = content
@title = args[:title]
@target = args[:target]
end
end

link = Link.new("http://", "here", {:title => "the whole story"})
helplink = Link.new("/help", "help" {:target => "helpFrame"})

As I understand it, there's some syntax-sugar planned for Ruby-2.0, that
would make this a bit cleaner:

link = Link.new("http://", "here", title: "the whole story")

Right?

----
4. YieldSelf

I've seen some people include a "yield self" in the initialize() method,
which allows you to do something like this:

class Link
def initialize(url, content)
@url = url
@content = content
yield self if block_given?
end
end

link = Link.new(url, "here") { |l|
l.title = "the whole story"
}

Hmmm. How does this help, exactly? I guess it could save you from having
to use a temp variable, in some cases.

----
5. InstanceEval

Or, you can use instance_eval():

class Link
def initialize(url, content, &config)
@url = url
@content = content
instance_eval(&config) if (config)
end
end

link = Link.new("http://", "here") {
@title = "the whole story"
}

This is a nifty trick, though it does have the downside that you can set
arbitrary instance-variables in the block, breaking class encapsulation.

----
In Summary

Where am I going to with this? No idea. I guess I'm just wondering what
y'all consider to be the benefits/problems of each approach.

--
cheers, MikeW

"He smelled as if he had just eaten a mustard-coated camel ..."
-- Martin Amis, "London Fields"



Ara.T.Howard 01-14-2004 05:06 AM

Re: What object-configuration approach to use?
 
On Wed, 14 Jan 2004, Mike Williams wrote:

> Date: Wed, 14 Jan 2004 13:04:24 +0900
> From: Mike Williams <mwilliams@agentissoftware.com>
> Newsgroups: comp.lang.ruby
> Subject: What object-configuration approach to use?
>
> Consider a class that supports a number of configurable properties.

<snip>


> ----
> 3. OptionalArgumentMap
>
> Another alternative is pass in a map of named optional args, e.g.
>
> class Link
> def initialize(url, content, args = {})
> @url = url
> @content = content
> @title = args[:title]
> @target = args[:target]
> end
> end
>
> link = Link.new("http://", "here", {:title => "the whole story"})
> helplink = Link.new("/help", "help" {:target => "helpFrame"})
>
> As I understand it, there's some syntax-sugar planned for Ruby-2.0, that
> would make this a bit cleaner:
>
> link = Link.new("http://", "here", title: "the whole story")
>
> Right?


i use variations of this alot:

~/eg/ruby > cat foo.rb
module M
def hashify(*args); args.inject({}){|h,a| h.update a}; end
end

class C
include M
attr :opts
attr :foo, true
attr :bar, true
def initialize(*args)
@opts = hashify(*args)
@opts.map{|k,v| send "#{ k }=".intern, v}
end
end


c = C.new :foo => 42,
:bar => 42.0

d = C.new c.opts, 'bar' => 'over-ridden'

p c.foo
p c.bar
p d.foo
p d.bar

~/eg/ruby > ruby foo.rb
42
42.0
42
"over-ridden"


it's great way to get up and running quickly and not need to change function
prototypes (read 'interface') so often. as the need arises for type/range
checking, etc. i just do

def foo= value
raise unless Array = value
@foo = value
end

etc.

i've found this to be very useful for at least a couple of reaons:

* keeps prototypes short
* keeps interface consistent - it never changes ;-)
* allows 'argumement inheritence' (see above)
* makes it _easy_ to pass in options from command line to objects or yaml
configs
* reads nice IMHO - i never forget what each parm is since it's named

Connection.new :port => 80, :type => 'udp'

vs

Connection.new 80, 'udp'

* allows one to quite easily pass a new parameter to a deeply nested method
without changes 40 prototypes on the way down

about the only thing i can say is that i makes it quite important to doccument
your methods so it is known what parameters a method expects. on the other
hand it easy enough to make methods throw a 'usage' exception with details of
calling semantics.


-a
--

ATTN: please update your address books with address below!

================================================== =============================
| EMAIL :: Ara [dot] T [dot] Howard [at] noaa [dot] gov
| PHONE :: 303.497.6469
| ADDRESS :: E/GC2 325 Broadway, Boulder, CO 80305-3328
| STP :: http://www.ngdc.noaa.gov/stp/
| NGDC :: http://www.ngdc.noaa.gov/
| NESDIS :: http://www.nesdis.noaa.gov/
| NOAA :: http://www.noaa.gov/
| US DOC :: http://www.commerce.gov/
|
| The difference between art and science is that science is what we
| understand well enough to explain to a computer.
| Art is everything else.
| -- Donald Knuth, "Discover"
|
| /bin/sh -c 'for l in ruby perl;do $l -e "print \"\x3a\x2d\x29\x0a\"";done'
================================================== =============================


Gavin Sinclair 01-14-2004 05:07 AM

Re: What object-configuration approach to use?
 
> Consider a class that supports a number of configurable properties.

> [snip all but my favourite one]


> 4. YieldSelf
>
> I've seen some people include a "yield self" in the initialize() method,
> which allows you to do something like this:
>
> class Link
> attr_accessor :url, :content, ... # this line was missing
> def initialize(url, content)
> @url = url
> @content = content
> yield self if block_given?
> end
> end
>
> link = Link.new(url, "here") { |l|
> l.title = "the whole story"
> }


> Where am I going to with this? No idea. I guess I'm just wondering
> what y'all consider to be the benefits/problems of each approach.


I like that approach because it's clean, it's explicit, it's
self-documenting (just look at which attributes are writable, and each
attribute can be RDoc'ed), and the resulting code is attractive:
lower-level configuration code is indented.

It's easier to read than the hash approach, while taking a similar amount
of typing.

The only downside is that it requires you to set *accessors* instead of
the more restrictive *readers*.

I've actually written an article on this topic. Without having read it (I
haven't e-published it yet), you summarised it very well!

Cheers,
Gavin





T. Onoma 01-14-2004 05:57 AM

Re: What object-configuration approach to use?
 
On Wednesday 14 January 2004 05:04 am, Mike Williams wrote:
> ----
> 1. SettingAttributes
>
> The obvious way is just to create the instance, and then call
> writer-methods to set attributes, e.g.
>
> class Link
> def initialize(url, content)
> @url = url
> @content = content
> end
> attr_accessor :url, :content, :link, :target
> end
>
> link = Link.new(url, "here")
> link.title = "the whole story"
>
> ----


I might point out another means, not so well explored, through the use of
singletons and runtime typing if required (read 'duck typing' for the less
squeamish) as follows:

class Link
def initialize(url, content)
@url = url
@content = content
end
end

link = Link.new(url, "here")
def link.title; "the whole story"; end

One of the interesting things to note about this approach is that class code
responsible for assignment is in no way needed, and tests for the available
singleton can be easily done through respond_to? (hence the typing).

--
T.




Mauricio Fernández 01-14-2004 08:15 AM

Re: What object-configuration approach to use?
 
On Wed, Jan 14, 2004 at 02:07:43PM +0900, Gavin Sinclair wrote:
> > 4. YieldSelf
> >
> > I've seen some people include a "yield self" in the initialize() method,
> > which allows you to do something like this:
> >
> > class Link
> > attr_accessor :url, :content, ... # this line was missing
> > def initialize(url, content)
> > @url = url
> > @content = content
> > yield self if block_given?
> > end
> > end
> >
> > link = Link.new(url, "here") { |l|
> > l.title = "the whole story"
> > }

>
> The only downside is that it requires you to set *accessors* instead of
> the more restrictive *readers*.


Credits go to Hal Fulton

batsman@tux-chan:/tmp$ expand -t2 a.rb
class A
attr_reader :foo, :bar

# only for initialize! :nodoc: or something for rdoc
attr_writer :foo, :bar

def initialize
yield self
klass = class << self; self end
["foo=", "bar="].each { |m| klass.send :undef_method, m }
self
end
end

a = A.new do |s|
s.foo = 1
s.bar = 2
end

p a
a.foo = 1

batsman@tux-chan:/tmp$ ruby a.rb
#<A:0x401c8a48 @bar=2, @foo=1>
a.rb:22: undefined method `foo=' for #<A:0x401c8a48 @bar=2, @foo=1> (NoMethodError)


--
_ _
| |__ __ _| |_ ___ _ __ ___ __ _ _ __
| '_ \ / _` | __/ __| '_ ` _ \ / _` | '_ \
| |_) | (_| | |_\__ \ | | | | | (_| | | | |
|_.__/ \__,_|\__|___/_| |_| |_|\__,_|_| |_|
Running Debian GNU/Linux Sid (unstable)
batsman dot geo at yahoo dot com

It's computer hardware, of course it's worth having <g>
-- Espy on #Debian



Gavin Sinclair 01-14-2004 12:15 PM

Re: What object-configuration approach to use?
 
On Wednesday, January 14, 2004, 7:15:58 PM, Mauricio wrote:

>> The only downside is that it requires you to set *accessors* instead of
>> the more restrictive *readers*.


> Credits go to Hal Fulton


> batsman@tux-chan:/tmp$ expand -t2 a.rb
> class A
> attr_reader :foo, :bar


> # only for initialize! :nodoc: or something for rdoc
> attr_writer :foo, :bar


> def initialize
> yield self
> klass = class << self; self end
> ["foo=", "bar="].each { |m| klass.send :undef_method, m }
> self
> end
> end



Two comments:

- Couldn't the attr_writers be created within "initialize", making it
more obvious?

- I'd prefer the writers be RDoc'ed anyway so I can see which
attributes I can set in the initializer.

Gavin




Mauricio Fernández 01-14-2004 03:32 PM

Re: What object-configuration approach to use?
 
On Wed, Jan 14, 2004 at 09:15:18PM +0900, Gavin Sinclair wrote:
> Two comments:
>
> - Couldn't the attr_writers be created within "initialize", making it
> more obvious?


def initialize
klass = class << self; self end
[:foo, :bar].each {|m| klass.module_eval{ attr_writer m } }
yield self
[:foo=, :bar=].each { |m| klass.send :undef_method, m }
self
end
?

> - I'd prefer the writers be RDoc'ed anyway so I can see which
> attributes I can set in the initializer.


On second thought me too :) I guess I'd just do

class A
attr_reader :foo, :bar

# use only with A.new { |o| ... }
attr_writer :foo, :bar

...
end

since rdoc will use the same description for all the 'attributes'
defined in the same attr_* call.

--
_ _
| |__ __ _| |_ ___ _ __ ___ __ _ _ __
| '_ \ / _` | __/ __| '_ ` _ \ / _` | '_ \
| |_) | (_| | |_\__ \ | | | | | (_| | | | |
|_.__/ \__,_|\__|___/_| |_| |_|\__,_|_| |_|
Running Debian GNU/Linux Sid (unstable)
batsman dot geo at yahoo dot com

Q: Why are Unix emulators like your right hand?
A: They're just pussy substitutes!



Simon Strandgaard 01-14-2004 04:58 PM

Re: What object-configuration approach to use?
 
On Wed, 14 Jan 2004 13:04:24 +0900, Mike Williams wrote:
[snip]
> class Link
> def initialize(url, content)
> @url = url
> @content = content
> end
> attr_accessor :url, :content, :link, :target
> end
>
> link = Link.new(url, "here")
> link.title = "the whole story"



I usually wrap my class hierarchy inside a module.
And make another module through which I can build instances, so
that its easy to up a testcase with an expected structure.

For instance the builder code looks like this:

module AbstractSyntaxFactory
def mk_file(name)
AbstractSyntax::File.new(name)
end
def mk_directory(name, *files)
AbstractSyntax::Directory.new(name, files)
end
def mk_link(name, *long_names)
AbstractSyntax::Link.new(name, long_names)
end
def mk_hierarchy(root_dir, *shortcuts)
AbstractSyntax::Hierarchy.new(root_dir, shortcuts)
end
end



Then in the testclass I can include AbstractSyntaxFactory
and easily invoke the 'mk_<type>' methods.

Like this:

class TestLookup < Test::Unit::TestCase
include AbstractSyntaxFactory
def build_hierarchy
@image_dir0 = mk_directory("images_anno_2000",
@img00 = mk_file("ruby-logo.svg"),
@img01 = mk_file("rite-logo.png"),
@img02 = mk_file("ros presentation.avi")
)
@image_dir1 = mk_directory("images_anno_2001",
@img10 = mk_file("baker.dia"),
@img11 = mk_file("pickaxe.thumbnail.gif")
)
...


Its rarely that I invoke Class.new directly.
Is this what you are seeking?

--
Simon Strandgaard




Gavin Sinclair 01-14-2004 09:49 PM

Re: What object-configuration approach to use?
 
On Thursday, January 15, 2004, 2:32:39 AM, Mauricio wrote:

>> - I'd prefer the writers be RDoc'ed anyway so I can see which
>> attributes I can set in the initializer.


> On second thought me too :) I guess I'd just do


> class A
> attr_reader :foo, :bar


> # use only with A.new { |o| ... }
> attr_writer :foo, :bar


> ...
> end


> since rdoc will use the same description for all the 'attributes'
> defined in the same attr_* call.



That's some good thinking. The RDoc output for that is unpleasant,
though (no fault of RDoc). The attributes look like this (sample docs
added):

bar [W] use only with A.new { |o| … }
bar [R] Represents the 'bar' of the object.
foo [W] use only with A.new { |o| … }
foo [R] The user's 'foo'.

That's obviously too crowded and repetitive.

At the end of the day, I just think "stuff it, I don't like making
things accessors when readers will do, but since I'm making the class
easier to use, I expect users to be more careful in return."

Cheers,
Gavin




All times are GMT. The time now is 03:07 PM.

Powered by vBulletin®. Copyright ©2000 - 2014, vBulletin Solutions, Inc.
SEO by vBSEO ©2010, Crawlability, Inc.