From: Simon Kitching Date: 2003-11-06T10:16:41+09:00 Subject: Re: Managing metadata about attribute types What a vigorous discussion I seem to have triggered :-) It's really nice to see so many people interested in the topic - thanks to all who replied. Rather than reply individually to the several emails which raise interesting points, I'll try to gather all the different bits here. I really am interested in the points raised, and am definitely still in the learning phase with Ruby. So all of the statements below should really be prefaced with "I think", "It seems to me", "Do you think that ... is right?". However that would double the size of this email. Please assume all below is tentative, and that comments/corrections are welcome. And I hope those people who "really wanted to stay out..." don't stay out and chip in. I'm interested at the very least! ==== Re "xml-config" module, raised by Austin: there are significant differences between the "xml-config" and "xmldigester" approaches. Which is "better" will depend on circumstances and developer taste. The most significant differences are: (a) Xml-config first builds a complete representation of the input xml in memory, then starts extracting data. For large xml files this is not a good idea. Xmldigester is "event-based", so the input xml does not have to be completely loaded into memory. (b) I *believe* that the xmldigester rules-based approach will take less client code, and will bind the "parsing" program less tightly to the API of the objects being built than the xml-config approach. (c) If building inter-object relationships that are more complex than simple parent/child references, the xml-config approach may prove easier. There are some tricks that can be played with xmldigester (eg a common "registry" object used to resolve relations), but having the entire xml document available (DOM-style) can allow things that an event-style approach cannot. (d) The xmldigester event-based approach is likely to be faster. Of course the best test of all the above opinions is actually to create the code, then compare the two. Once I have xmldigester knocked into reasonable shape, I might port the xmldigester examples to xml-config (and vice-versa) to see if any of the above is true! Regardless of the results, I think that both approaches have their place. ==== Regarding whether the target class should be responsible for accepting a string and doing the conversion... I think it is definitely *not* the receiving classes' responsibility to do the conversion. Here's my original class, with the initial implicit assumptions spelled out more clearly as comments. # Ruby class StockItem # contract with user: any value assigned to name must # act like a string attr_accessor :name # contract with user: any value assigned to cost must act # like a float object. attr_accessor :cost end Isn't this a valid API for a class to provide? As far as the author of StockItem is concerned, cost is a float. I don't see why the author of StockItem should even consider the possibility that a string could be assigned to cost; that would violate the contract of the class, so any program that does so can damn well take the consequences :-). The StrictTyping module can enforce this, but perhaps does so over-eagerly, as it doesn't allow "duck-typing" ie objects which aren't Float objects but do behave like them. Now I happen to want to configure this object based on some string-typed information. But that's my problem, not the problem of the author of the StockItem class. And if I wanted to use ASN.1 format input to initialise an instance of StockItem, then it is still my responsibility to convert the ASN.1 representation to an appropriate type before assigning to cost, not the StockItem class' responsibility to understand ASN.1 format input. Ok, with Ruby's "open" classes, I can alias the original cost= method and insert some wrapper code. But I will have to restore the original method after parsing is complete, otherwise during the real "running" part of the program, the StockItem's cost= method won't behave like other classes expect it to. Not to mention that writing those "conversion" methods by hand is ugly. >You're right, they shouldn't. But if your warehouse management >classes don't do what they can to ensure their data integrity, then >there's a problem with the classes -- not with the XML library. I'm >not trying to be difficult here; just pointing out that I think >you're trying to fix the problem from the wrong end. The StockItem's contract clearly states that it only accepts Float types for the cost attribute. It doesn't actually need to enforce its data integrity - it is the calling code's responsibility to use StockItem correctly. > 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. The data conversion clearly has to be done somewhere, but I would like it to be done separately from the target class so as not to muck around with its API. Here's the "conversion" code extracted out into a helper class: def StockItemHelper def StockItemHelper.cost=(stock_item, str) stock_item.cost = str.to_f end end In fact, why not use the Java convention and call it StockItemBeanInfo? Applying a modified version of your attr_accessor code, this could be written more succinctly as the following, generating effectively the same code as shown above: def StockItemBeanInfo attr_from_str :name, String attr_from_str :cost, Float end However I can also use something like Ryan's MetaTag syntax to write this. I'm not sure which syntax is more convenient. desc = <<-END !class StockItem !name String !cost Float END # parse the string and dynamically create a wrapper class beanInfo = createBeanInfoClass(desc) # because cost was declared as a Float in the MetaTag string, # the beanInfo class knows to convert the second (string) param # to a float. beanInfo.set_cost(stock_item, '3.50') As you can see, I'm not interested in "type strictness" at all. What I need is simply "what type of object should I generate in order to be able to validly assign to cost without violating the API contract of the StockItem class"... Changing the StockItem class contract is one solution, but that screws up all other code that really depended on the original contract being valid. Oh, and what if the target attribute is a "Date" class, and I want to globally control the way string->date mapping works? If it is distributed across every class that has a Date attribute that is much trickier to handle than if I somehow know that classes X, Y and Z have date attributes and the xmldigester code does the string->date conversions before the assignment. ==== >From Ryan: > Using #to_* methods are the ruby equivalent of type casting. The > point in this case is not to _convert_ types, it's to provide the > right type in the first place. Instead of giving the attribute a > string and expecting it to be parsed, we want to create the desired > type and hand that off. > > It has nothing to do with the #attr= function. Strict type checking > at that point is merely a convenience. It's all about getting the > input into a useful format without writing n^2 functions (for n > classes). This is the primary reason I wrote StrongTyping in fact; > the strict checking has merely helped with debugging a whole lot. > Yep, that's exactly how I see it. However I don't want the "type enforcement at runtime" feature of StrongTyping, and I want to avoid changing the target class' behaviour in any significant way. Is it possible to get the "type info" part of StrongTyping without the "type enforcement"? ==== The thread about namespaces still has me pondering a little. I'm not sure it's relevant to my issue, though, is it? I need to *instantiate* an object in order to assign it to an attribute on a target object. So I do need to know the name of a concrete class to instantiate. There's no "duck typing" there, is there? ==== Thanks Ryan, Chad, Austin, Richard, James, David, Christoph (phew!) As said in the intro, all comments/corrections welcome! Regards, Simon