What object-configuration approach to use?

M

Mike Williams

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.
 
A

Ara.T.Howard

Date: Wed, 14 Jan 2004 13:04:24 +0900
From: Mike Williams <[email protected]>
Newsgroups: comp.lang.ruby
Subject: What object-configuration approach to use?

Consider a class that supports a number of configurable properties.
----
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 :eek:pts
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'
===============================================================================
 
G

Gavin Sinclair

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

T. Onoma

----
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).
 
M

Mauricio Fernández

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
 
G

Gavin Sinclair

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
 
M

Mauricio Fernández

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!
 
S

Simon Strandgaard

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?
 
G

Gavin Sinclair

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

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
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,766
Messages
2,569,569
Members
45,042
Latest member
icassiem

Latest Threads

Top