mmcdevi1
mmcdevi1

Reputation: 21

Ruby OOP Class Responsibilities

I am still trying in wrap my head around OOP in Ruby. Let's say I'm trying to create a simple Hangman game and I want to select a random word from a text file. So far I have 2 examples in the codeblock. The first example shows a Word and Game class, where the Word class generates a random word and the Game class calls the Word class in the initialize method. The second example has only a Game class where the Game class itself generates the random word. My question is, is it the Game classes responsibility to generate the random word or use the Word class?

# First Example
module Hangman

  class Word
    def self.words
      File.readlines("../words.txt")
    end

    def self.random
      words.select { |word| word.length > 4 && word.length < 13 }.sample
    end
  end

  class Game
    attr_reader :random_word

    def initialize
      @random_word = Hangman::Word.random
    end
  end

end

# Second Example
module Hangman

  class Game
    attr_reader :words, :random_word

    def initialize
      @words = File.readlines("../words.txt")
      @random_word = @words.select { |word| word.length > 4 && word.length < 13 }.sample
    end
  end

end

Upvotes: 0

Views: 137

Answers (2)

Tim Heilman
Tim Heilman

Reputation: 410

Sandi Metz has a great example of how to answer this question in Practical Object Oriented Design in Ruby. Sadly, since it is copyrighted I can't link directly to the passage.

In her example, although a bicycle seems like a good candidate for a class due to its obvious existence in the problem domain, at the point in the development of her application that she needs to compute a gear ratio, she realizes that the inputs to that calculation are all related only to gears: the number of teeth on each of two instances of Gear, and thus places the functionality on Gear, deferring the creation of the Bicycle class until a later time.

So the general answer is: look to the input values for the required computation and place that computation's definition on the class with the greatest number of those input values as fields already.

In your specific case:

is it the Game classes responsibility to generate the random word or use the Word class?

Well first off, it seems like your Word class is more of a WordList class, although depending on your future direction, it could remain a Word class but in embodiment of the composite pattern . If you do keep it as a WordList class, it has no instance methods, so discussing responsibilities of the class becomes very difficult. Effectively the class itself has methods, but the class is always expected to be at "singleton instantiation scope" or a constant. Ruby class names are constants, so defining methods only at the level of constants is effectively procedural, not object-oriented, code.

To make WordList object-oriented, you can pass an IO instance (File is a subclass, but why depend on a subclass whose additionally defined methods are not needed by your code?) to WordList#initialize , potentially providing singleton access with a

def self.singleton_instance
  @singleton_instance ||= new(File.open("../words.txt"))
end

This allows other clients to reuse the WordList class in other contexts by providing any kind of IO, including a StringIO, and separates and makes explicit that loading the default, singleton WordList is only one way this class expects to be used, requires the constant-scope level file from the parent directory, and allows the instance-level behavior of a WordList to be defined.

So far it looks like that instance-level behavior you need is a random selection from all the words. Getting back to Sandi Metz's advice, WordList does seem like a good place to put the computation of the random selection, because WordList will have a field:

attr_reader :words

def initialize(io)
  @words = io.readlines
end

and it's exactly the words field that the filtration is to be performed upon, so this class is a good candidate for that functionality:

def random # notice no self. prefix
  words.select { |word| word.length > 4 && word.length < 13 }.sample
end

and later, to actually-use,

@random_word = Hangman::WordList.singleton_instance.random

This also gives you a place to swap-out the singleton instance for a different one if you need to later, without changing the WordList class. That should score points for complying with the Open Closed Principle too.

(An aside: it seems that "random" may be a poor choice for method name -- it's not just random, but also constrained to a length of between 4 and 13 exclusive. Perhaps "random_suitable_length_word"?)

Upvotes: 2

Mitch VanDuyn
Mitch VanDuyn

Reputation: 2878

In general it depends.

For this specific case I think most people would agree that splitting the structure between Word and the Game is a good idea.

Word is a nice small testable piece, and so it does deserve its own class.

It also could be reusable in a number of games that need a random word.

I think this becomes clearer if you rewrite word so it has an initialize method. Then the game is simply calling Word.new(...) to get a new random word.

Imagine if there was a gem called "words" that already did all this. You would be happy add the gem and say done deal. Well that is an easy way to tell you have made a good division of labor, even no such gem exists.

By the way once you think this should be a separate class, you might want to check to see if somebody already did it for you. In this case there is a gem random-word.

What would the parameters be to words initialize? Well the length of the word, skill level, etc etc.

class Word
  def self.words
    @words ||= File.readlines("../words.txt")
  end

  def initialize(min_length, max_length)
    Word.words.select do |word| 
      word.length > length && word.length < max_length 
    end.sample
  end
end

Upvotes: 0

Related Questions