I hope you realise that the StockItem was just an example I made up out
of thin air for the purposes of the discussion - it isn’t a real class
in use anywhere. Xmldigester is like xml-config; it is a library to
configure any set of objects. The StockItem class is just one example.
Here’s a slightly more complex example, that might get away from the
triviality of the StockItem’s cost attribute example.
class Weight
def initialize(units, amount)
@units = units
@amount = amount
end
other weight-related methods here, with defined behaviours
and associated contracts…
end
class StockItem
user contract: anything assigned to this attribute must behave
like a Weight object.
attr_accessor :weight
attr_accessor :name
attr_accessor :cost # Float
end
Now as a programmer dealing with the above problem, I want to be able to
tell xmldigester than when it encounters the tag it is to
create a StockItem instance, then create a Weight instance and
initialise it appropriately from a string, then assign that initialised
object to the StockItem’s weight attribute.
The Java Digester version does this automatically, by determining that
the StockItem has a “weight” attribute of type “Weight”, and that there
is a “weight” xml attribute (with a string value). It then invokes a
table of data-conversion methods to convert the string to the target
type; built-in types are already in the table, and user-specific types
(like Weight) can be added as needed.
You’re suggesting that in the file which contains the “xml-parsing”
code, I re-open the existing StockItem class, and use some approach like
the attr_accessor modification to “wrap” the existing weight= code,
resulting in something that effectively works like:
class StockItem # reopen existing class
alias :__weight= :weight=
def weight=(param)
weight2 = param.to_weight
__weight = weight2
end
end
and Weight doesn’t have a to_weight method yet, so I’d need to reopen
that class too:
class Weight # reopen existing class
def to_weight
return self
end
end
and finally add a method to String:
class String # reopen string
def to_weight
# some code to instantiate a Weight object and
# initialise it from the String’s value
end
end
Yes, I am now able to do this, which would indeed satisfy my
requirements:
can now assign via strings
stock_item.weight = ‘0.75 kg’
and
can still use StockItem instances as per normal
stock_item.weight = Weight.new(‘g’, 750)
It seems a lot of work, though. And I’m not sure I’m too fond of adding
methods to the String class. Nor of the overhead that now exists on
every assignment to stock_item.weight, though that’s not so important.
And what about if Weight is actually Acme::Warehouse::Weight?
I’ll give this approach some serious thought, though.
Hmm … libraries don’t ever call “freeze” on their classes, do they?
s = StockItem.new
s.name = “Apple Pie” # An apple pie…
s.cost = 10 # Costs $10…
per_slice = s.cost / 8 # Split it eight ways…
puts per_slice # => 1
Therefore, by simply assuming that you’re getting an object that
can act like a float, you’ve introduced a huge error. Should I have
entered 10.0 as the price, or divided by 8.0? Either of those would
have guaranteed me a Float context in which type coercion will be
used to ensure a Float result. If, however, we had converted cost to
a float explicitly during assignment, this wouldn’t even be an
issue. Without talking about Strings, we’ve already run into a
problem with StockItem’s assumption of Float-ness.
Yes, but as you noted yourself, you’ve violated the contract on the
class. I haven’t bothered to clutter my example with “defensive
programming”. I grant that you may be right that the cost method should
call to_f on its parameter, so that Integers can also be passed. And
maybe every method expecting a String parameter should call to_s on its
parameter?
Oh, unless nil is acceptable as a parameter, in which case it would be
better to do:
@name = name.to_s if name
I wonder how many of the Ruby standard libraries do this? Certainly no
attribute declared with “attr_reader” etc does.
Hmm … by the way, aren’t you arguing against duck-typing here?
If someone deliberately creates a type which is like a Float, then
this code would force it to be a real float (assuming that to_f returns
a real Float object).
Compare the same Java:
class StockItem {
String name;
float cost;
void setName(String n) { name = n; }
void setCost(float c) { cost = c; }
String getName() { return name; }
float getCost() { return cost; }
}
In Java, it doesn’t matter if you pass an int to setCost because the
compiler has already marked that as a float – and it will do an
implicit conversion from int to float. (IIRC, that won’t work in
Ada, which disallows implicit conversions.)
Yes, but for the Java Digester library, implicit conversions are
irrelevant. It doesn’t try to pass a String to the setCost method and
hope the compiler will insert the correct conversion code (it won’t
anyway). Instead, as described earlier, a table of “type conversion
operations” is used to map from the input String to the target type,
allowing any target type (such as Weight) to be correctly dealt with.
The author of the StockItem class should have considered that any
numeric value could have been assigned – and that integer math
wouldn’t be a good idea.
As above, I agree that for safety cost= could try to ensure the
parameter passed to it complies with the contract. I still don’t believe
that it is mandatory for methods to enforce their contracts,
though…user beware should be the motto, for performance and
simplicity.
attr_accessor proc { |x| x.to_i }, :item_id
That’s some very cool code. I can feel my brain expanding just by
looking at it! However I don’t feel it does what I want, because
this code actually changes the API of the target class, breaking
all other code that accesses that same attribute thereafter.
Actually, it doesn’t change the API at all. It enforces the
documented constraints. It’s the difference between early and late
detection.
I didn’t initially realise that if a real Float was passed to this
method, then calling to_f on it is ok; it just returns the same object.
[snip bean info stuff]
I donno. That still doesn’t feel very “Ruby” to me, and I personally
find both StrongTyping and MetaTag clunky, trying to solve things
that I’m not sure are best solved that way.
Yes, I’m still worried that there is some obvious rubyish way to handle
this. Maybe when I actually issue 0.1 of xmldigester (with whatever
API), then start trying to build some apps with it some epiphany will
happen.
Cheers,
Simon
···
On Thu, 2003-11-06 at 16:27, Austin Ziegler wrote:
On Thu, 6 Nov 2003 10:16:41 +0900, Simon Kitching wrote: