Hi David, I am not entirely sure about the list's etiquette. I hope it's OK to reply to you _and_ to the list (I imagine somebody in the future might be very interested in this discussion!) First of all: _THANK YOU_ David! I think I'm getting there... >> I feel the need to start this email with an apology. I am a terrible >> programmer. > Meh. So am I, and I do it full-time ;) (For given values of full.) Yeah right, I know people like you :-D >> class Subscribers > Subscriber is probably a better name. Start creating single > entities of your > application. OK. > A deinitialize method seems completely useless to me. If you want > an object (a > subscriber record) to stop existing, delete it from disk, remove it > from any > listings or caches you store on disk separately. If it's a persistent > application, drop the old object representing it from memory too. > Clobber and > forget, you don't need to cater to an invalid object that's not > being used / > doing anything anymore in the application. OK. > You might want to make some sort of #delete method for the > abovementioned > "housekeeping". OK. Is there a way of "forcing" the deallocation of an object? > Use accessors (see below) and instance variables for this. A big > hash for all > attributes of the object is very bad style IMO. OK, done. > E.g. you'd have in the class definition: > > attr_reader :good_state > > def first_letter > email[0, 1].upcase > end > > def full_path > # insert that string addition thingy I'm too cheap to copy / paste > end Oh... OK. I was thinking about performance. This way, Ruby has to recalculate the string all the time. However, I can see that this should be the way to go... > attr_accessor :email > > and change the method body to: > > def link_to_fs(email) > @good_state = true > self.email = email > end OK. >> # Hang on: if the file doesn't exist, undo everything >> # >> if ! File.exist?(@full_path) >> de_initialize() >> return false >> end >> > > Not quite good. First find out if you can create a new object - > then proceed > if you can. I'd move the code of #link_to_fs into #initialize > myself in this > case. You can throw an exception if the record creation fails, but > I'd use a > different approach. OK, done. > Replace your own catch-all getters and setters with proper > accessors for the > subscriber attributes - this data access class becomes a bit more > self-descriptive. Done. > You can define your own accessors per these universal getters and > setters, but > I'd tag them as private methods - they seem a bit bug-prone to be > part of the > interface. OK, true. > E.g.: > > def premium? > get_flag(:premium_flag) > end > > def premium=(value) > set_flag(:premium_flag, value) > end Yep. > You could cache this way with ordinary instance attributes. Unless > you expect > the subscriber attributes (not their values) to change very often and > unexpectedly. It also gives a bit more exact behavior. Done! > he above would change to something like with what I have in mind: > > # Presuming the record exists. > sub = Subscriber.load("merc2 / mobily.com") > puts sub.email > puts sub.premium? > puts sub.moderator? > puts "OK:" > puts sub.name = "BLAH!!!" > puts sub.name > sub.save # To put the changes to disk. For a number of reasons, I took the approach that changing a variable also changes the file right away. One of the reasons is that I will _very_ often 1) Open a subscriber 2) Change a little piece of information 3) Close the subscriber. The sub.save method would have to be intelligent enough to work out what's changed... it's a little too messy. It's much easier this way. >> * Is it sane for the class to set the class variable >> @@config_data_dir? @@config_data_dir=IO.read("#{ENV['HOME']}/.subs/ >> data_dir") > For a simple enough application, this seems like a perfectly good > organization. Bonus points for tying configuration to the model. Cool! >> * Would it make more sense to create a new person with the >> method ::new? In this case, what would you call the method to access >> an existing person? > ::load? The path where a subscriber's data rests is directly > computable from > the e-mail adress. Very true. I chose "link_to()". > The solution I'd probably go for is: > - ::new would create a subscriber record in memory. OK. > - ::load would load a record from disk, or return nil if there is > no record > for a given e-mail. Actually, it wouldn't load any data, just > some "proxy" > object that would load the data from disk when needed and keep > (cache) them in memory. > - ::save would store a (changed) object back into the respective > files. You > could have ::save take an optional flag to explicitly allow / > deny the > creation of new records - to prevent duplicate records clobbering > each > other. OK. That's exactly what I did. It seems to be working fine. >> * Is this solution OO sound? > Definately, Your Subscriber object is a data access object, which > uses the > filesystem as the underlying "database". Very commonly done / used > piece of > code, I'd say. OK. >> I can't think of a way, in Ruby, to do access the information like >> this: a_subscriber.get_field[:name] or a_subscriber.get_flag >> [:moderator_flag]. This is the tricky part, and that's where my lack >> of understanding of OOP shows blatantly. > Use instance attributes and accessors instead of the catch-all > hash. It's a > rather basic part of OO, but understandably foreign to anyone with > a strict C > background. OK. >> a.subscriber.get_field[:name] is the equivalent of saying >> a.subscriber.get_field.[](name) > Mind you, you don't use this construct in the code you posted. I know. And I can see why it doesn't really make sense to do it this way. THANKS! >> This implies that get_field returns an object of some kind able to >> respond to []. If this were the way to go design-wise (which I doubt, >> but now I am confused, so...), where would such an object be created? > Well, in the #get_field method :P The way to do so would be > returning a Hash > with the required attributed in it. But it's much more concise OO- > wise to > have a data object respond to queries about its data directly than > via a > "middle-man" hash. ...and it would be really quite messy to manage the "writer", for example! >> How would it access the class information such as @@config_data_dir, >> or the subscriber's instance variable? > Accessors, accessors, acceessors... Oh... true! > You can have Ruby generate them if you can do with the default ones > that only > read / write to instance attributes, or custom ones as the examples > I've > shown above > Even class objects have accessors: > class Subscriber > def self.config_data_dir > @@config_data_dir > end > end OK. I can't believe I didn't think of this myself. >> * I am thinking about a container class for this: >> SubscribersContainer. The class would implement the method each(), so >> that I can scan through the list of people stored (creating a >> Subscriber object for iteration). Is this a sane approach? > Yes. This class could also store the data directory path and manage > the > lifecycle of Subscriber objects, reducing those to only data > retrieval / > caching. You mean with something like: sub_container=SubscribersContainer.new() sub_container['merc'].name="tony" ...? When the method [](email) is called, the object SubscribersContainer would need to create a new object (if necessary), or return the one already created at some point in the past. Is that right? I am not sure why you say that this class would need the data directory...! Also, I wonder if accessing too many objects that way wouldn't clutter the collection too much (the "real" number of subscribers we have is about 14000. I KNOW we need a DB. We didn't expect quite so many. I am planning to switch to DB) > If you wanted to go extreme, you could also separate the data > retrieval part > and keep subscribers only as dumb data structured, but I'd say it > would only > be deconstructing code for the sake of deconstruction, and more > confusing > than anything else in this case. I'll ask about this later... The question will include the fact that I would like to make this class "generic", so that in the future I can change it so that it connects to a DB rather than reading the file system. At this point, I am not sure what the right path is. Maybe create a subscribers class, and then create a SubscriberFileSystem subclass which needs to implement get_flag, set_flag, get_field, set_field...? >> I am just a little scared of doing anything right now. Did I design >> it all wrong? > Surprisingly well actually, saying you're an OO beginner. The > changes I > proposed are more tweaks and use of common idioms than horrible flaws. OK. >> Should I have created the classes SubscribersFlags and >> SubscribersAttributes, and have two instance variables in Subscribers >> derived from SubscribersFlags and SubscribersAttributes? > Overkill. The attributes and flags belong to the subscriber. They > are inherent > to a subscriber, and should stay there. OK. >> * The most important of all: can you suggest a book or a web site >> which would help me design more decent classes? Possibly something >> that is Ruby-centric... > I'd say read through the Gang of Four and Refactoring, Woops... I've lost you here. Are you talking about one specific book? Or two books? There are quite a few books which have a title that starts with "refactoring", and I couldn't find any with "Gang of four"...! What I am really after, is a book with a list of common problems and common patterns, so that I can just apply the one that fits my design. Something Ruby-oriented would be _perfect_! > but those might be out > of your scope. Not necessarily though, and I believe those two to > be very > important not-too-advanced OO reading. Java inside : you have been > warned ;) OK :-) Well... here is the latest version of my class. It would be fantastic if you could have a quick look at it, and let me know if I actually got it right design-wise. I haven't actually tested it properly - I am more interested in the big picture for now... THANKS AGAIN! #!/usr/local/bin/ruby -w class Subscriber # Set the config file. # begin @@config_data_dir=IO.read("#{ENV['HOME']}/.subs/ data_dir") @@config_data_dir.chomp! rescue SystemCallError STDERR.puts("WARNING! Can't find the config file!"); @@config_data_dir="" end def initialize() @email=nil end def first_letter return nil if ! @email @email[0, 1].upcase if @email end def full_path return nil if ! @email @@config_data_dir+"/current/"+self.first_letter +"/"+@email+"/" end # This just checks that the directory actually # exists. It creates a "link" # def link_to(email_param) # Hang on: if the file doesn't exist, undo everything # @email=email_param if ! File.exist?(self.full_path) @email=nil return false end @premium_flag=nil @moderator_flag=nil @unconfirmed_flag=nil @country=nil @creation_date=nil @name=nil @password=nil @postcode=nil @premium_expiry_date=nil @questionnaire_res=nil @subscriber_code=nil @subscriber_comments=nil true end attr_reader :email # Accessors for the subscriber's information # # FLAGS: # # premium_flag # def premium_flag? @premium_flag ||= get_flag(:premium_flag) end def premium_flag= @premium_flag = set_flag(:premium_flag,value) end # moderator_flag # def moderator_flag? @moderator_flag ||= get_flag(:moderator_flag) end def moderator_flag=(value) @moderator_flag = set_flag(:moderator_flag,value) end # unconfirmed_flag # def unconfirmed_flag? @unconfirmed_flag ||= get_flag(:unconfirmed_flag) end def unconfirmed_flag=(value) @unconfirmed_flag=set_flag(:unconfirmed_flag,value) end # # ATTRIBUTES # def country @country ||= get_field(:country) end def country=(value) @country = set_field(:country,value) end def creation_date @creation_date ||= get_field(:creation_date) end def creation_date=(value) @creation_date = set_field(:creation_date,value) end def name @name ||= get_field(:name) end def name=(value) @name = set_field(:name,value) end def password @password ||= get_field(:password) end def password=(value) @password = set_field(:password,value) end def postcode @postcode ||= get_field(:postcode) end def postcode=(value) @postcode = set_field(:postcode,value) end def questionnaire_res @questionnaire_res ||= get_field(:questionnaire_res) end def questionnaire_res=(value) @questionnaire_res = set_field (:questionnaire_res,value) end def subscriber_code @subscriber_code ||= get_field(:subscriber_code) end def subscriber_code=(value) @subscriber_code = set_field(:subscriber_code,value) end def subscriber_comments @subscriber_comments ||= get_field (:subscriber_comments) end def subscriber_comments=(value) @subscriber_comments = set_field (:subscriber_comments,value) end private def get_flag(flag) return nil if ! @email File.exist?(self.full_path+flag.to_s) end def get_field(field) return nil if ! @email # Reads the file from the file system; returns # nil if the file can't be opened # begin IO::read(self.full_path+field.to_s) rescue SystemCallError nil end end def set_field(field,value) return nil if ! @email # Open the file # begin ios=File::open(self.full_path+field.to_s,"w") rescue SystemCallError return nil end # Set the value to nil. This is to reflect the # "real" state of the variable (the file has just been # cleared up by the previous call) # begin ios.print(value) rescue SystemCallError ios.close return nil end # OK, it worked: assign the new value # ios.close value end def set_flag(flag,value) return nil if ! @email if(value) begin File.open(self.full_path+flag.to_s,"w") rescue SystemCallError return nil end return true else begin File.delete(self.full_path+fiag.to_s) rescue SystemCallError return nil end return false end end end a_subscriber=Subscriber.new() p a_subscriber.email p a_subscriber.link_to("merc2 / mobily.com") p a_subscriber.email p a_subscriber.premium_flag? p a_subscriber.moderator_flag? puts "OK:" p a_subscriber.name="ooppp!!!" p a_subscriber.name Merc.