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"

  1. 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")

  1. 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?


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


  1. 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”

Consider a class that supports a number of configurable properties.

[snip all but my favourite one]

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

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

···

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"


T.

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.


  1. 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 :wink:

  • 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

···

On Wed, 14 Jan 2004, Mike Williams wrote:

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 :: Solar-Terrestrial Physics Data | NCEI
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’
===============================================================================

[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_’ 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?

···

On Wed, 14 Jan 2004 13:04:24 +0900, Mike Williams wrote:


Simon Strandgaard

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)

···

On Wed, Jan 14, 2004 at 02:07:43PM +0900, Gavin Sinclair wrote:

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


_ _

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

It’s computer hardware, of course it’s worth having
– Espy on #Debian

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

···

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?

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 :slight_smile: 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.

···

On Wed, Jan 14, 2004 at 09:15:18PM +0900, Gavin Sinclair wrote:


_ _

__ __ | | ___ _ __ ___ __ _ _ __
'_ \ / | __/ __| '_ _ \ / ` | ’ \
) | (| | |
__ \ | | | | | (| | | | |
.__/ _,
|_|/| || ||_,|| |_|
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!

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

···

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 :slight_smile: 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.