What object-configuration approach to use?

Discussion in 'Ruby' started by Mike Williams, Jan 14, 2004.

  1. 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"
     
    Mike Williams, Jan 14, 2004
    #1
    1. Advertising

  2. Mike Williams

    Ara.T.Howard Guest

    On Wed, 14 Jan 2004, Mike Williams wrote:

    > Date: Wed, 14 Jan 2004 13:04:24 +0900
    > From: Mike Williams <>
    > 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 :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'
    ===============================================================================
     
    Ara.T.Howard, Jan 14, 2004
    #2
    1. Advertising

  3. > 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
     
    Gavin Sinclair, Jan 14, 2004
    #3
  4. Mike Williams

    T. Onoma Guest

    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.
     
    T. Onoma, Jan 14, 2004
    #4
  5. 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
     
    Mauricio Fernández, Jan 14, 2004
    #5
  6. 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
     
    Gavin Sinclair, Jan 14, 2004
    #6
  7. 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!
     
    Mauricio Fernández, Jan 14, 2004
    #7
  8. 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
     
    Simon Strandgaard, Jan 14, 2004
    #8
  9. 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
     
    Gavin Sinclair, Jan 14, 2004
    #9
    1. Advertising

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

It takes just 2 minutes to sign up (and it's free!). Just click the sign up button to choose a username and then you can ask your own questions on the forum.
Similar Threads
  1. les
    Replies:
    1
    Views:
    364
    Elliot Rodriguez
    Jul 31, 2003
  2. Mark
    Replies:
    0
    Views:
    861
  3. James Yong

    Which approach would you use?

    James Yong, Sep 24, 2005, in forum: Java
    Replies:
    3
    Views:
    350
    HalcyonWild
    Sep 26, 2005
  4. Replies:
    10
    Views:
    540
    Mark Rae [MVP]
    Nov 6, 2007
  5. Himanshu Garg
    Replies:
    1
    Views:
    97
    Chris Angelico
    Nov 15, 2013
Loading...

Share This Page