On Fri, Sep 24, 2004 at 12:20:54PM +0900, Gavin Kistner wrote:
> While I'm normally a huge duck-typing-plus fan, I'm waffling in this 
> case. I think the reason is that I hope to have adaptor modules 
> (classes) being contributed from many different authors, providing me 
> with two challenges:
> 
> 1) Given the large number of devices which fit under 'automation', I 
> want a hierarchical organization of devices which may be available. If 
> I leave it up to each author, I fear the user will install an adaptor 
> for their hardware and find that it's under LightSwitch/Dimmer while 
> every other similar adaptor is in LightSwitches/Adaptors. Or worse, 
> it's in DimmerSwitches, or Electrical/Wall/Analog/Slider.

If I understand rightly, I think the main constraint here is that you want
to organise the classes logically for the user to see when selecting one (to
create a new instance of a DimmerSwitch, for example, selecting it from a
tree rather than a long linear scrolling list).

You can always have a separate hierarchy for display purposes - or even
multiple different hierarchies, or the same class popping up in multiple
places in the same displayed hierarchy. It doesn't have to follow a class or
module hierarchy, and although it could, I think it will limit your
flexibility if you do.

You might, for example, want to give one view organised by manufacturer, and
another organised by device type.

> and at the same time gives me a modicum of protection from 
> adaptor authors running over each others' code (through namespace 
> collisions).

If that's a concern, then modules should be organised by vendor/author, not
by function, so that each author is responsible for her own namespace.

module ACMEwidgets
  class OnOffSwitch
    ...
  end
end

That's another reason for making the organisation of objects for display
purposes separate from the module/class hierarchy (unless you're happy to
organise the display by vendor).

Of course, when you do "require 'acmewidgets'", you might want the classes
to appear in the correct places in the GUI hierarchy automagically. To do
that, I think they should export an "index card", rather like when a new
book arrives at the library - the librarian looks at the index card to work
out where to file it.

To support that, you need to define your own classification system, and then
modules can pick the one which suits them best.

require 'homecontrol'
module ACMEwidgets
  class OnOffSwitch
    def self.indexcard
      [Homecontrol::DevType::Switch, "ACME Widgets model 345X mains controller"]
    end
  end
end

Or you could use a constant with a well-known name within the class (the
details of the exporting mechanism are unimportant, IMO)

> 2) Similar to the above, the more leeway I give the authors in the 
> method naming convention, the more I fear that some switches will have 
> "Turn On", some will have "On", and some smartass authors will use 
> "Illuminate".Having thought about your points, I will certainly need to 
> look at the exposed methods for any adaptor and be able to use them. 
> (Who knows what bizarre functionality I will forget to include or 
> account for?) But, for the sake of consistency, I think I want to say 
> "Damnit, if you have a 'Light' class, you better have a :turn_on 
> method."

Then you're imposing a duck-typing system anyway. You need to provide
hard-written guidelines for module authors, for each sort of device, what
methods it should have.

Although really, I think that end users are not likely to be invoking
methods on their devices directly (unless they are Hal), in which case the
GUI can take care of it.

> Well, as Robert correctly surmised, I don't actually want to implement 
> common code to them. (Some adaptor authors may want to, and they're 
> welcome to define their own modules if they wish). What I'm probably 
> going to end up with is something like:
> 
> class Device
> 	def self.mandatory_methods= ( *method_names )
> 		@mandatory = method_names.flatten.to_set
> 		@mandatory.merge( superclass.mandatory_methods ) if 
> superclass.respond_to? :mandatory_methods
> 	end
> 
> 	def self.mandatory_methods
> 		@mandatory
> 	end
> 
> 	self.mandatory_methods = [:name, :manufacturer, :models]
> 
> 	class Light < self
> 		self.mandatory_methods = [:turn_on, :turn_off]
> 		class Dimmer < self
> 			self.mandatory_methods = [:level=, :level]
> 		end
> 	end
> 	
> 	class Outlet < self
> 		self.mandatory_methods = [:turn_on, :turn_off]
> 	end
> end
> 
> along with a test for these methods as part of my #instance handling.

Why, then, have these arrays of mandatory_methods? What exactly are you
going to do if the class turns out not to have a method that you claim is
mandatory? I guess you could write code which automatically mails the
offending module back to the author and rm's it from the filesystem! :-)

Seriously... I am a believer in "do the simplest thing which can possibly
work". If the method doesn't exist - then when you call it, Ruby will raise
an exception for you. There's no need to pre-list the methods expected, nor
to do
     def turn_on
       raise "Not implemented!"
     end
in a superclass, because that's essentially the default behaviour which Ruby
has.

As another example: you were talking about using introspection to work out
which classes were subclasses of a top class, to identify which controllable
objects exist in the system. Sure you can do that - but it might be simpler
just to set a global variable which contains an array or hash of them (a
hash indexed by name for the GUI to display). After all, how often do you
add a new *class* of object to the system, like a new type of light switch
(as opposed to a new instance of a light switch)? You'll probably edit a .rb
file to add the appropriate "require" line anyway, so adding a new entry
into a global list of usable object classes isn't hard.

I suppose the introspection way is a little prettier:

module Homecontrol
  module Controllable  # dummy, just flags that this object is controllable
  end
end

...

module ACMEwidgets
  class OnOffSwitch
    include Homecontrol::Controllable
    DESCRIPTION = "ACME Widgets model 345X mains controller"
  end
end

...

klasses = []
ObjectSpace.each_object(Class) { |k| klasses << k if k.include? Homecontrol::Controllable }
klasses.each { |k| puts k.const_get('DESCRIPTION') }

But I'd think of that as cake-icing really.

Cheers,

Brian.