Generic class/design pattern question

Had a good look at your classes, and I can't see any obvious way of simplifying or improving them. However, please bear in mind that I am a relative newbie to Ruby too, although my primary interest is in RPG/Combat games. - maybe one of our members more skilled at re-factoring may be able to suggest a radical approach from an outsiders viewpoint. My main concern is that your classes are quite large, and may prove difficult to maintain/document.

Just as an aside, have you considered yet how to store and recover the data?

···

On 24/04/2017 09:56, Leam Hall wrote:

On 04/23/17 23:20, Patrick Bayford wrote:

Not sure about others here, but I would find it useful to see at least a
rough idea of how your classes look. Personally, I like the superclass
idea, but know from personal experience, that designing the superclass
is difficult, mainly in respect of how much detail it should include.
This is especially important when dealing with serialized, stored data,
as changing the class/es tends to break the serializer!

Hey Patrick, thanks!

The Character class:
https://github.com/LeamHall/CT_Character_Generator/blob/master/lib/Tools/Character.rb

Uses Module CharacterTools to modify most of the data:
https://github.com/LeamHall/CT_Character_Generator/blob/master/lib/Tools/CharacterTools.rb

The Career class has the methods for running a career:
https://github.com/LeamHall/CT_Character_Generator/blob/master/lib/Tools/Career.rb

And uses data from subclasses, like:
https://github.com/LeamHall/CT_Character_Generator/blob/master/lib/Careers/Citizen.rb

That help?

Leam

Unsubscribe: <mailto:ruby-talk-request@ruby-lang.org?subject=unsubscribe>
<http://lists.ruby-lang.org/cgi-bin/mailman/options/ruby-talk&gt;

--
Patrick Bayford

Hells teeth Ryan, even I should have spotted point 7)! However, observation was NEVER my strong point, not a great attribute for a programmer.

I also tend to forget that Ruby object methods automatically the last value!!! A useful, but strange concept for us folk brought up on OOP extended procedural languages!

···

On 26/04/2017 21:39, Ryan Davis wrote:

On Apr 24, 2017, at 1:56 AM, Leam Hall <leamhall@gmail.com> wrote:

On 04/23/17 23:20, Patrick Bayford wrote:

Not sure about others here, but I would find it useful to see at least a
rough idea of how your classes look. Personally, I like the superclass
idea, but know from personal experience, that designing the superclass
is difficult, mainly in respect of how much detail it should include.
This is especially important when dealing with serialized, stored data,
as changing the class/es tends to break the serializer!

Hey Patrick, thanks!

The Character class:
https://github.com/LeamHall/CT_Character_Generator/blob/master/lib/Tools/Character.rb

Uses Module CharacterTools to modify most of the data:
https://github.com/LeamHall/CT_Character_Generator/blob/master/lib/Tools/CharacterTools.rb

The Career class has the methods for running a career:
https://github.com/LeamHall/CT_Character_Generator/blob/master/lib/Tools/Career.rb

And uses data from subclasses, like:
https://github.com/LeamHall/CT_Character_Generator/blob/master/lib/Careers/Citizen.rb

Just looking at one thing and iterating on it:

# 0) Delete all trailing whitespace. Use your editor to help you code like a pro. Use paragraphs/blank-lines to separate concepts:

def self.title(character)
   nobility = Hash.new
   nobility["B"] = { "f" => "Dame", "m" => "Knight" }
   nobility["C"] = { "f" => "Baroness", "m" => "Baron" }
   nobility["D"] = { "f" => "Marquesa", "m" => "Marquis" }
   nobility["E"] = { "f" => "Countess", "m" => "Count" }
   nobility["F"] = { "f" => "Duchess", "m" => "Duke" }

   soc = character.upp[5,1].upcase

   if nobility.has_key?(soc)
     if character.gender.downcase == "female"
       title = nobility[soc]["f"]
       character.title = title
     else
       title = nobility[soc]["m"]
       character.title = title
     end
   end

   return title
end

# 1) upcase & downcase is showing that you’re not normalizing data
# when it comes in / gets stored. I haven’t looked at your code,
# but chances are all this data is internal and doesn’t need ANY
# up/down case.

def self.title(character)
   nobility = Hash.new
   nobility["B"] = { "f" => "Dame", "m" => "Knight" }
   nobility["C"] = { "f" => "Baroness", "m" => "Baron" }
   nobility["D"] = { "f" => "Marquesa", "m" => "Marquis" }
   nobility["E"] = { "f" => "Countess", "m" => "Count" }
   nobility["F"] = { "f" => "Duchess", "m" => "Duke" }

   soc = character.upp[5,1]

   if nobility.has_key?(soc)
     if character.gender == "female"
       title = nobility[soc]["f"]
       character.title = title
     else
       title = nobility[soc]["m"]
       character.title = title
     end
   end
   return title
end

# 2) nobility doesn't change. Pull it out and make it a constant:

NOBILITY = {
             "B" => { "f" => "Dame", "m" => "Knight" },
             "C" => { "f" => "Baroness", "m" => "Baron" },
             "D" => { "f" => "Marquesa", "m" => "Marquis" },
             "E" => { "f" => "Countess", "m" => "Count" },
             "F" => { "f" => "Duchess", "m" => "Duke" },
            }

def self.title(character)
   soc = character.upp[5,1]

   if NOBILITY.has_key?(soc)
     if character.gender == "female"
       title = NOBILITY[soc]["f"]
       character.title = title
     else
       title = NOBILITY[soc]["m"]
       character.title = title
     end
   end
   return title
end

# 3) You're using data to make a decision w/ soc, but then skip that
# concept for gender. And then duplicate code entirely:

NOBILITY = {
             "B" => { "f" => "Dame", "m" => "Knight" },
             "C" => { "f" => "Baroness", "m" => "Baron" },
             "D" => { "f" => "Marquesa", "m" => "Marquis" },
             "E" => { "f" => "Countess", "m" => "Count" },
             "F" => { "f" => "Duchess", "m" => "Duke" },
            }

def self.title(character)
   soc = character.upp[5,1]
   gender = character.gender[0]

   if NOBILITY.has_key?(soc)
     title = NOBILITY[soc][gender]
     character.title = title
   end
   return title
end

# 4) Don't use return when it isn't an early exit

NOBILITY = {
             "B" => { "f" => "Dame", "m" => "Knight" },
             "C" => { "f" => "Baroness", "m" => "Baron" },
             "D" => { "f" => "Marquesa", "m" => "Marquis" },
             "E" => { "f" => "Countess", "m" => "Count" },
             "F" => { "f" => "Duchess", "m" => "Duke" },
            }

def self.title(character)
   soc = character.upp[5,1]
   gender = character.gender[0]

   if NOBILITY.has_key?(soc)
     title = NOBILITY[soc][gender]
     character.title = title
   end

   title
end

# 5) Everything can be collapsed to the one real concept here:

NOBILITY = {
             "B" => { "f" => "Dame", "m" => "Knight" },
             "C" => { "f" => "Baroness", "m" => "Baron" },
             "D" => { "f" => "Marquesa", "m" => "Marquis" },
             "E" => { "f" => "Countess", "m" => "Count" },
             "F" => { "f" => "Duchess", "m" => "Duke" },
            }

def self.title(character)
   soc = character.upp[5,1]
   gender = character.gender[0]

   character.title = NOBILITY[soc][gender] if NOBILITY.has_key?(soc)
end

# 6) Which shows one real problem, this is called `title` but really
# sets it and returns it, every time. Either rename it or just
# return and set it somewhere else.

NOBILITY = {
             "B" => { "f" => "Dame", "m" => "Knight" },
             "C" => { "f" => "Baroness", "m" => "Baron" },
             "D" => { "f" => "Marquesa", "m" => "Marquis" },
             "E" => { "f" => "Countess", "m" => "Count" },
             "F" => { "f" => "Duchess", "m" => "Duke" },
            }

def self.calculate_title(character)
   soc = character.upp[5,1]
   gender = character.gender[0]

   character.title = NOBILITY[soc][gender] if NOBILITY.has_key?(soc)
end

# 7) The other real problem is that this is a class method on a module
# that is acting on Character. This isn't OO. This is procedural.
# It is the responsibility of a Character to know its title and
# shouldn't be externalized.

Unsubscribe: <mailto:ruby-talk-request@ruby-lang.org?subject=unsubscribe>
<http://lists.ruby-lang.org/cgi-bin/mailman/options/ruby-talk&gt;

--
Patrick Bayford

BTW - I understand the Class method concept, and why it is necessary for the language to support it (cases like MyObj = someobject.Create), but am unclear when we should use Class methods as opposed to Object methods. I suspect that Leam may have the same problem.

···

On 26/04/2017 21:39, Ryan Davis wrote:

On Apr 24, 2017, at 1:56 AM, Leam Hall <leamhall@gmail.com> wrote:

On 04/23/17 23:20, Patrick Bayford wrote:

Not sure about others here, but I would find it useful to see at least a
rough idea of how your classes look. Personally, I like the superclass
idea, but know from personal experience, that designing the superclass
is difficult, mainly in respect of how much detail it should include.
This is especially important when dealing with serialized, stored data,
as changing the class/es tends to break the serializer!

Hey Patrick, thanks!

The Character class:
https://github.com/LeamHall/CT_Character_Generator/blob/master/lib/Tools/Character.rb

Uses Module CharacterTools to modify most of the data:
https://github.com/LeamHall/CT_Character_Generator/blob/master/lib/Tools/CharacterTools.rb

The Career class has the methods for running a career:
https://github.com/LeamHall/CT_Character_Generator/blob/master/lib/Tools/Career.rb

And uses data from subclasses, like:
https://github.com/LeamHall/CT_Character_Generator/blob/master/lib/Careers/Citizen.rb

Just looking at one thing and iterating on it:

# 0) Delete all trailing whitespace. Use your editor to help you code like a pro. Use paragraphs/blank-lines to separate concepts:

def self.title(character)
   nobility = Hash.new
   nobility["B"] = { "f" => "Dame", "m" => "Knight" }
   nobility["C"] = { "f" => "Baroness", "m" => "Baron" }
   nobility["D"] = { "f" => "Marquesa", "m" => "Marquis" }
   nobility["E"] = { "f" => "Countess", "m" => "Count" }
   nobility["F"] = { "f" => "Duchess", "m" => "Duke" }

   soc = character.upp[5,1].upcase

   if nobility.has_key?(soc)
     if character.gender.downcase == "female"
       title = nobility[soc]["f"]
       character.title = title
     else
       title = nobility[soc]["m"]
       character.title = title
     end
   end

   return title
end

# 1) upcase & downcase is showing that you’re not normalizing data
# when it comes in / gets stored. I haven’t looked at your code,
# but chances are all this data is internal and doesn’t need ANY
# up/down case.

def self.title(character)
   nobility = Hash.new
   nobility["B"] = { "f" => "Dame", "m" => "Knight" }
   nobility["C"] = { "f" => "Baroness", "m" => "Baron" }
   nobility["D"] = { "f" => "Marquesa", "m" => "Marquis" }
   nobility["E"] = { "f" => "Countess", "m" => "Count" }
   nobility["F"] = { "f" => "Duchess", "m" => "Duke" }

   soc = character.upp[5,1]

   if nobility.has_key?(soc)
     if character.gender == "female"
       title = nobility[soc]["f"]
       character.title = title
     else
       title = nobility[soc]["m"]
       character.title = title
     end
   end
   return title
end

# 2) nobility doesn't change. Pull it out and make it a constant:

NOBILITY = {
             "B" => { "f" => "Dame", "m" => "Knight" },
             "C" => { "f" => "Baroness", "m" => "Baron" },
             "D" => { "f" => "Marquesa", "m" => "Marquis" },
             "E" => { "f" => "Countess", "m" => "Count" },
             "F" => { "f" => "Duchess", "m" => "Duke" },
            }

def self.title(character)
   soc = character.upp[5,1]

   if NOBILITY.has_key?(soc)
     if character.gender == "female"
       title = NOBILITY[soc]["f"]
       character.title = title
     else
       title = NOBILITY[soc]["m"]
       character.title = title
     end
   end
   return title
end

# 3) You're using data to make a decision w/ soc, but then skip that
# concept for gender. And then duplicate code entirely:

NOBILITY = {
             "B" => { "f" => "Dame", "m" => "Knight" },
             "C" => { "f" => "Baroness", "m" => "Baron" },
             "D" => { "f" => "Marquesa", "m" => "Marquis" },
             "E" => { "f" => "Countess", "m" => "Count" },
             "F" => { "f" => "Duchess", "m" => "Duke" },
            }

def self.title(character)
   soc = character.upp[5,1]
   gender = character.gender[0]

   if NOBILITY.has_key?(soc)
     title = NOBILITY[soc][gender]
     character.title = title
   end
   return title
end

# 4) Don't use return when it isn't an early exit

NOBILITY = {
             "B" => { "f" => "Dame", "m" => "Knight" },
             "C" => { "f" => "Baroness", "m" => "Baron" },
             "D" => { "f" => "Marquesa", "m" => "Marquis" },
             "E" => { "f" => "Countess", "m" => "Count" },
             "F" => { "f" => "Duchess", "m" => "Duke" },
            }

def self.title(character)
   soc = character.upp[5,1]
   gender = character.gender[0]

   if NOBILITY.has_key?(soc)
     title = NOBILITY[soc][gender]
     character.title = title
   end

   title
end

# 5) Everything can be collapsed to the one real concept here:

NOBILITY = {
             "B" => { "f" => "Dame", "m" => "Knight" },
             "C" => { "f" => "Baroness", "m" => "Baron" },
             "D" => { "f" => "Marquesa", "m" => "Marquis" },
             "E" => { "f" => "Countess", "m" => "Count" },
             "F" => { "f" => "Duchess", "m" => "Duke" },
            }

def self.title(character)
   soc = character.upp[5,1]
   gender = character.gender[0]

   character.title = NOBILITY[soc][gender] if NOBILITY.has_key?(soc)
end

# 6) Which shows one real problem, this is called `title` but really
# sets it and returns it, every time. Either rename it or just
# return and set it somewhere else.

NOBILITY = {
             "B" => { "f" => "Dame", "m" => "Knight" },
             "C" => { "f" => "Baroness", "m" => "Baron" },
             "D" => { "f" => "Marquesa", "m" => "Marquis" },
             "E" => { "f" => "Countess", "m" => "Count" },
             "F" => { "f" => "Duchess", "m" => "Duke" },
            }

def self.calculate_title(character)
   soc = character.upp[5,1]
   gender = character.gender[0]

   character.title = NOBILITY[soc][gender] if NOBILITY.has_key?(soc)
end

# 7) The other real problem is that this is a class method on a module
# that is acting on Character. This isn't OO. This is procedural.
# It is the responsibility of a Character to know its title and
# shouldn't be externalized.

Unsubscribe: <mailto:ruby-talk-request@ruby-lang.org?subject=unsubscribe>
<http://lists.ruby-lang.org/cgi-bin/mailman/options/ruby-talk&gt;

--
Patrick Bayford

Hey Ryan and Patrick, thanks! I've collated some of the notes and tried to comment in-line.

# 0) Delete all trailing whitespace. Use your editor to help you code like a pro.

Just started using rubocop to help find places to clean up. My editor is vim. :wink:

# 1) upcase & downcase is showing that you’re not normalizing data
# when it comes in / gets stored. I haven’t looked at your code,
# but chances are all this data is internal and doesn’t need ANY
# up/down case.

Hmm...you're right. I have morphed how this class/module is used over time and haven't set an internal state expectation. The methods in CharacterTools used to be in Character itself, then I started trying to clean things up a bit. Issue #45.
https://github.com/LeamHall/CT_Character_Generator/issues/45

# 2) nobility doesn't change. Pull it out and make it a constant:

Issue #44. :slight_smile:
https://github.com/LeamHall/CT_Character_Generator/issues/44

# 6) Which shows one real problem, this is called `title` but really
# sets it and returns it, every time. Either rename it or just
# return and set it somewhere else.

The Social Status characteristic can change over the course of an instance's usage. That's why "title" is calculated each time, not set.

# 7) The other real problem is that this is a class method on a module
# that is acting on Character. This isn't OO. This is procedural.
# It is the responsibility of a Character to know its title and
# shouldn't be externalized.

I had to go re-read to make sure I understood this; I'm very much a shell programmer trying to learn OOP and Ruby. :slight_smile:

> Had a good look at your classes, and I can't see any obvious way of
> simplifying or improving them. However, please bear in mind that I am
> a relative newbie to Ruby too, although my primary interest is in
> RPG/Combat games. - maybe one of our members more skilled at
> re-factoring may be able to suggest a radical approach from
> an outsiders viewpoint.

Well, this is for an RPG, so feel free to jump in!

> My main concern is that your classes are quite large, and may
> prove difficult to maintain/document.

You should have seen them before! :slight_smile:

The Module CharacterTools and the Class Career are pretty large as they contain the methods used to modify the Character instances. The grouping is fairly logical, though large. It comes from the Traveller (tm) RPG

> Just as an aside, have you considered yet how to store and recover the
> data?

Several times! Early on the plan was JSON. However, as the need has grown data will be stored in SQLite, JSON (to go to MongoDB), and something else so things can be put into Neo4J. At the moment I'm trying to reduce the number of gems required to run the program and removing the JSON requirement. Next is SQLite; that's used to access the Names database. I'll probably wind up with YAML to stay with the Ruby Standard Library and to stay simple.

Part of the reason for separation between the Character class and the CharacterTools module was to simplify things and clean out cruft from organic growth. The Character class mostly just stores data and will be used when putting stuff into a database or pulling it out. CharacterTools has the methods to generate and modify the data of a Character instance. The use of class methods is mostly me still trying to figure things out.

For example, the Presenter class will be the way instances of Character are displayed. Eventually there will be at least one class that will fetch data from a datastore and pass it to Presenter or something else.

The original concepts came from my PHP code, Since I started learning Ruby this seems a good project to learn on.

Leam

···

On 04/26/17 16:39, Ryan Davis wrote:
On 04/26/17 15:15, Patrick Bayford wrote:

You might want to have a look at the vim-better-whitespace plugin
(https://github.com/ntpeters/vim-better-whitespace\), which will
highlight all trailing whitespace in eye-catching red.

Regards,
Marcus

···

Am 27.04.2017 um 11:56 schrieb Leam Hall:

# 0) Delete all trailing whitespace. Use your editor to help you code
like a pro.

Just started using rubocop to help find places to clean up. My editor is
vim. :wink:

--
GitHub: stomar (Marcus Stollsteimer) · GitHub
PGP: 0x6B3A101A