From: Gavin Sinclair Date: 2002-08-08T01:41:17+09:00 Subject: Re: Class hierarchy... for data ----- Original Message ----- From: "Tom Gilbert" > Hey, > > I'm currently trying to create some kind of hierarchial data class. Kind > of like "sparse" data layers that sit on top of each other and let > lookups "fall through" them. > > An example of this would be for multiple configuration layers > (application defaults, user defaults, current settings) - the idea being > that any change in the app defaults (including live changes) propagates > through to any derived settings where that value has not been overridden > from the default. > > I have hacked something out which comes maybe 20% of the way, and was > hoping for suggestions to make this nicer. Here's what I have so far: > > === > > class DataDelegator > def initialize(parent=nil, hash=nil) > @parent = parent > @hash = hash > @hash = Hash.new if @hash == nil Style only: @hash = hash || {} > end > > def [](key) > if @hash.has_key?(key) > return @hash[key] > elsif @parent != nil > return @parent[key] > else > # raise "no such member" > return nil > end > end > > def []=(key,value) > @hash[key] = value > end > end > > app_defaults = DataDelegator.new > app_defaults["foo"] = "default_foo" > app_defaults["bar"] = "default_bar" > app_defaults["baz"] = "default_baz" > > user_defaults = DataDelegator.new(app_defaults) > user_defaults["foo"] = "user_foo" > > current_settings = DataDelegator.new(user_defaults) > current_settings["baz"] = "my_baz" > > puts current_settings["foo"] > puts current_settings["bar"] > puts current_settings["baz"] > > === > > This outputs: > $ ./test.rb > user_foo > default_bar > my_baz > > So it kind of works, Yep, seems pretty good to me. > but I'd really like it not to use a hash I don't see why not. You want "some kind of hierarchial data class"; a hash is a data class, you're providing the hierarchy. It sounds perfect. > ideally I'd like to access current_settings.foo I don't like that idea. current_settings["foo"] is fine. > and not be allowed to lookup > something not found in the instance or any of its parents (very much > like a class hierarchy). That's the case at present, isn't it? Maybe I don't understand this point. > I'd like current_settings.each to do the right > thing, So implement it! What is "the right thing"? Getting all elements, including those defined in a parent, but not overwritten in the child? Following code is not tested. class DataDelegator def keys if parent == nil @parent.keys | @hash.keys else @hash.keys end end def each for key in keys value = self[key] yield key, value end end end > and even nicer would be the ability to do stuff > like: > current_settings["list of stuff"] << "whoopie" > and have that .dup the parent's list before appending to it, and have > current_settings["some array"].clear not clear the array in some parent. Nice, but I've no idea, I'm sorry. Hopefully someone else will know. Otherwise, I'd be providing methods to perform these operations. If you really wanted to do what you say, you'd probably have to implement an Association class for your use. Something like: class Association attr_accessor :key, :value def initialize(key, value) @key, @value = key, value end def <<(element) if @value.responds_to? << value << element else raise ArgumentError end end def clear value.clear if value.responds_to? clear raise ArgumentError otherwise # ! end end Then you'd need to redefine a bit, as the @hash now stores Associations. This means it can intercept methods like << and clear, and cause them to operate on the value. class DataDelegator def [](key) if @hash.has_key?(key) return @hash[key].value # this line changed elsif @parent != nil return @parent[key] else # raise "no such member" return nil end end def []=(key,value) if (key = @hash[key]) key.value = value else @hash[key] = Association.new(key, value) end end end Ahem. I've just realised that this is garbage. Since it doesn't change the interface (current_settings["foo"] still returns what they want), the behaviour is not going to change when you call current_settings["foo"] << "whoopie". Still, I'll leave the code there in case you can do something with it. One possibility is to do the following: current_settings.send("foo", <<, whoopie) It may not be pretty, but you may like it and improve it. It wouldn't need the Association class. > This is all so like a class hierarchy I wonder if I'm missing something > obvious that would stop this becoming rather hacky (I had considered > using method_missing to catch "foo=" and "foo" etc and do the lookup > from there). Eurgghhh... I don't like method_missing. (I know cleverer people than me will disagree.) What if you have a key named "hash", so you call current_settings.hash. What do you get? Either: (a) the hash code of the object, which is not what you want; or (b) the setting, which screws you if you want to put the object in a Hash. > Anyone got anything similar? Or maybe some suggestions? Keep going; you're nearly there. > Tom. > -- Sorry for all the mess. It's last; I'll clean up in the morning ;) Cheers, Gavin