Monday, January 9, 2012
Exercise 42: Gothons Are Getting Classy: Learn Ruby the Hard Way: Practicum
In the last example, we saw that we could put functions inside of hashes. While that will work, there's another approach that serves the same purpose and really works better to encapsulate code that we want to duplicate for various purposes, and is actually at the heart of so called "object oriented programming". That heart is the structure called the "class".
Ruby utilizes classes to do a lot of things, specifically it allows us to make entirely new commands with their own methods that we can call on, and make copies of them that are unique and encapsulated within themselves (we could look at the idea of designing a car and have two separate cars with many similar components, but one's an SUV and the other is a Lamborghini. Similar in a lot of ways, different in key areas. we reuse the parts that are similar, and change or create components that are different for each instance. Zed makes the point that Classes are a huge area in programming, and to explore them completely goes way beyond the focus and point of this course and book.
We've actually been using classes quite a bit so far without realizing it. Examples are as follows:
stuff = ['Test', 'This', 'Out']
puts stuff.join(' ')
stuff is actually an Array class.
stuff.join(' ') has us using the Array class we called "stuff" and calling on the Array function (method) 'join', passing ' ' (an empty space). That's an example of using classes. Cool, huh?
Here's an example class. They are easier to make compared to hashes, but they have their own syntactic details to remember.
The @ symbol before the @number variable is part of that special syntax. This defines it as an "instance variable". This means that every instance of TheThing that we create will have its own value for @number. Instance variables are specific to the instance of the object that we create. We can't get at the name simply by typing a.number unless we explicitly make that data readable to the outside world.
We do that by including the attr_reader :number line.
If we wanted to make @number write-only, we could change the line to "attr_writer :number".
To make it read/write we could change the line to "attr_accessor :number".
This is all part of what is referred to as "encapsulating data" within an object-oriented context.
Next, there is an initialize method. This is how Ruby classes can be set up with internal variables (using the @ symbol). The variables can also be used in the add_me_up() functions, where we add to the @number that we created.
The project for today is to recreate the game we made for Exercise 41, but in this case, we are going to make it as a class, and call the class to set up the game and run through the rooms. I took the liberty of modifying the program structure to use "here documents" just for fun. If you want to try it out using the program as Zed originally created it, check the LRTHW site and get that version and copy it out:
What You Should See
As you can see, the code output is almost identical to the last version. Two differences, we see two calls at the top to show that we are creating an instance of the class to start the game, and we are also seeing the effect of the Here Document usage: anything that appears between the opening and closing tag are treated as literal text, meaning the formatting spaces are preserved. That's why the text appears indented in the output. If I wanted to clean that up, I could move the text to the beginning of each line, but again, that kind of offends my sensibilities of formatted code. Still, if it was needed, I'd do it :).
The key details we need to be aware of when we are looking at classes and how they are used are as follows:
- We made a class called Game and put functions inside it.
- Initialize is a special initialization method that sets up important variables.
- We can add functions to a class by nesting their definitions under the class keyword.
- We can nest the contents of the functions under their names.
- We can see how "@" is used with initialize, play, and death.
- We created a "Game" instance at the end and then told it to "play()". That's how everything got started.