From: Brian Candler Date: 2007-02-27T05:48:54+09:00 Subject: Re: Subclassing Array On Tue, Feb 27, 2007 at 12:42:15AM +0900, El Gato wrote: > I'm sure I'm just being an idiot here... my mind is a little foggy this > morning, but I'm having a hard time understanding how to accomplish > this. I've written a class (I'll just put some snippets in for > understanding) in which I'd like to be able to use the following > behavior: > > irb> columns = [[1, "Hostname"], [2, "Model"], [5, "OS Version"]] > irb> report = InventoryReport.new("HARDWARE_QUERY", columns) > irb> report.query.map {|a| a.first.downcase!; a}.save "/tmp/test.xls" > > Where query() returns an array like [["host1", "Dell", "Windows"], > ["host2", "Hitachi", "Windows"], ...] > > However, throwing a map (or select or whatever) in there (obviously) > ends up returning an Array. How would I handle this so that it works as > I would like? Do I need to move my save(), to_csv(), and to_xls() stuff > into the Array class? It was a Moment of Enlightenment for me when I realised that all this stuff they teach you about OO, class hierarchies and inheritance is actually irrelevant. It's mainly there just to keep C++ and Java compilers happy. A more flexible approach is composition and delegation. (Object A *has a* B, not Object A *is a* B) In this model, what you'd get is something like this: class InventoryReport def initialize(query_name, fields, data = []) @query_name = query_name @fields = fields @data = data end def query ans = ...do database query... self.class.new(@query_name, @fields, ans) end def map(&blk) self.class.new(@query_name, @fields, @data.map(&blk)) end ... etc end But in that case, InventoryReport#query could modify its own @data instance in-place, in which case you'd have def query @data = ...do database query... end def map(&blk) @data = @data.map(&blk) end (Then you can argue about whether the methods should be called 'query!' and 'map!' but I won't go there) Anyway, I find this approach is much more powerful. For example, your object A can contain instances of B, C and D. When an incoming method call arrives, it can forward to one of these, or use them in sequence to perform whatever task is required. If you find you are writing lots of explicit delegation, then use one of the delegation patterns to help you - or just write a method_missing() function. And if you're using inheritance as a form of implementation code sharing between your own objects, then just use mixins for the common code. Going down this route, a lot of the OO conundrums simply disappear - such as "is a circle an oval, or is an oval a circle?" I notice that your InventoryReport is already composed of three elements: a query name, a field array, and a data array. If you use inheritance, you have to worry about which of these elements is the "primary" one which inherits from some other class, and deal with all the nits of inherited methods (such as Array#map always returning an Array, as you discovered). However if you use composition, all the elements are equal, and you can pick whatever behaviour you want from all three. Regards, Brian.