We've been talking throughout this class about how everything in Ruby is an object, but it can be difficult to visualize what that means when it comes to building a program. Let's take a moment with your Car challenge to take apart how Ruby's object-oriented behavior actually impacts you as a programmer.
Take a look at your Vehicle class. It probably looks like a list of class attributes, followed by a list of methods. The class attributes -- things like @wheels and @model_year -- are what make your object what it is: the class Vehicle (also a noun). The methods you've written are things you can do with Vehicle and its attributes: for example, print the Vehicle information or create a new vehicle. All Ruby classes have this structure.
In your Car challenge, you have at minimum a Vehicle class and a Car class. There can be as many Cars as you want to make, and they will share a lot of the same attributes as Vehicle. Any time you identify this kind of relationship forming, whether it's one to many or one to one, it's a strong indication that there should be inheritance between the two classes. This will allow you to share methods across related classes.
Creating a program in Ruby is a little bit like playing Jenga; all the bits and pieces rely on each other to stand up, and removing the wrong piece can wreak havoc. Because of their relationships, objects impact each other and you have to remember those relationships as you're writing your code. Take the example of the Vitamin class from this morning's presentation. Vitamin and Apple have a many to many relationship. There can be as many Apples with Vitamins as you want to create, and you can create them using the method you already wrote for creating items in the Fruit class, which is pretty cool. But what happens when you write a method in the Fruit class to grab all Fruit with Vitamins, and it grabs an instance of the Fruit class that doesn't inherit from Vitamin? You'll get an error. One solution to this is creating a separate array for storing items with Vitamins in Fruit, so the two types don't clash. Remembering how your objects impact each other is very important when writing applications in Ruby.
Class can be an abstract concept when you first come across it. Let's try to clear up what class is and how it behaves. To start, "class definition" describes the structure and behavior of a set of objects.
Inheritance is a relationship between two classes. For instance, an Apple "is a" Fruit. Apple and Fruit have an inheritance relationship based on their shared features.
Fruit is the Superclass,"super"/bigger, because it encompasses more things (like other classes or Fruits) and is the broader class definition.
Apple would be a good example of a class further down the inheritance hierarchy. Classes further down from the superclass have more features specific to what they are -- like Apple has more specific features and characteristics than the broad umbrella Fruit -- making them more specialized.
Inheritance means that classes inherit the external interface of the superclasses. That is a slightly complicated way of saying that, in Ruby, classes can access and use their superclass' methods. The "private parts" of those methods -- the instance variables -- are not inherited (only the methods in a class can access them).
Aggregation/composition/contains, has-a (uses): one class uses another class
Has-a: singular; have-many: plural, implying a collection (for instance, array).
Classes are related through storage or operation, but not more/less.
Objects, meaning instances of a class, are realizations of a class that can be interacted with and store data. Objects are independent of each other. With a class definition, we have the blueprint for what a Fruit class or an Apple class is and can do. Objects allow us to interact with those classes directly in our code; it's the difference between reading about an apple and being handed one.
This style of programming is called object oriented, even if the programming to some extent focuses on classes.
Since inheritance is a relation between two classes, to create the relation we use
<. This allows the new class to get the features of the higher up class, but now you can add specific features. For example:
class Fruit def initialize color @color = color end def color @color end def is_sweet true end end
Now we will create a new class called Apple and have it inherit from Fruit.
And since we are inheriting from the Fruit class, which initializes an instance variable, and requires a parameter, we have to do the same with our Apple class. So we create an initialize method, and pass in the parameter as super. Using super will call the original method from Fruit and pass in the parameter.
class Apple < Fruit def initialize color super color + " Apple" end def spoils "Spoils in 7 days" end end apple_one = Apple.new("Red") apple_one.color => "Red Apple" apple_one.is_sweet => true apple_one.spoils => "Spoils in 7 days"
In Ruby, all values are objects. Anything you have any way of referring to is an object.
All objects in Ruby belong to a class. The class defines what it means to be that kind of thing, which is to say what the thing does.
classes are a bit different.
You can find out the class of something by calling its
> 1.class => Fixnum > 1.0.class => Float 'hello'.class => String
The nice thing about Ruby's OOP capabilities is that they're deep and subtle and powerful and you can spend a lot of time getting really good at OOP and being able to do many amazing things. Yet for most purposes, you can stick to simple OOP features and get nearly anything done that you might want.
Let's say you want to keep track of people. You would create a Person class (capitalized, and usually singular). This can be created in IRB.
class Person def set_given_name(name) @given_name = name end def get_given_name() @given_name end end
Then you can create a new instance of the
Person class. You create a new instance of a class with
a_person = Person.new() a_person.set_given_name('Bilbo')
Then you can call the methods that have stored the names:
a_person.get_given_name() => 'Bilbo'
The variable starting with
@ is an instance variable, meaning it belongs to the instance of
And each instance of
Person has it's own set of instance variables independent of other
> a_person.given_name NoMethodError: undefined method `given_name' for #<Person:0x007fec7287d9b0> > [email protected]_name SyntaxError: (irb):60: syntax error, unexpected tIVAR, expecting '('
This is why we have written accessor methods - the get... and set... methods in my class. And remember that Ruby methods return the value of whatever they last did, which is how my get... methods work.
a_person.given_name = 'Bilbo'
And in fact we can do that. Without restarting your irb, enter this:
class Person def given_name=(name) @given_name = name end def given_name @given_name end end
This lets you write
a_person.given_name = 'Baggins', which is interpreted as
> a_person.given_name = 'Bracegirdle' > a_person.given_name => 'Bracegirdle'
Finally, you will often want to do something at the time an object is created. Perhaps it has a property that is an array, and you want to ensure that it always starts as an empty array.
If you give your class an initialize method, it will get executed immediately when an instance of the class is created. And if it takes any arguments, you'll be required to give those when creating an instance.
class with initialize:
class Jedi def initialize @title = "Knight" end def title @title end end
Creating a new instance of the Jedi class, which immediately initializes an instance variable with an empty array, which can than have elements pushed to it:
> chosen_one = Jedi.new => #<Jedi:0x007fb532a24d30 @title="Knight"> > chosen_one.title => "Knight"
Another example with initialize for a one-to-many/has-many relationship:
class Fruit def initialize @vitamins =  end def vitamins @vitamins end end
New instance of the class:
generic_fruit = Fruit.new => #<Fruit:0x007fb532acc120 @vitamins=> generic_fruit.vitamins =>  # No vitamins generic_fruit.vitamins << "C" generic_fruit.vitamins << "D" generic_fruit.vitamins => ["C", "D"]
One more Example, but this one is looking for an argument to be passed into it when it is initialized. So it gives an error of
wrong number of arguments if one is not passed in.
class Animal def initialize att @attitude = att end def attitude @attitude end end
New instance of the class, which gives error.
> kitten = Animal.new ArgumentError: wrong number of arguments (0 for 1) from (irb):77:in `initialize' from (irb):81:in `new'
Correct instance of the class, with passing in 1 argument.
> kitten = Animal.new :sweet => #<Baz:0x007fb5331de210 @attitude=:sweet> > kitten.attitude => :sweet
Before starting the implemention, copy each story into the editor as a comment, and use the function recipe to guide the implementation of each method
Story: As a programmer, I can make a vehicle.
Hint: Create a class called
Vehicle, and create a variable called
my_vehicle which contains an object of class
Story: As a programmer, I can make a car.
Hint: Create a class called
Car, and create a variable called
my_car which contains an object of class
Story: As a programmer, I can tell how many wheels a car has; default is four.
initialize the car to have four wheels, then create a method to return the number of wheels.
Story: As a programmer, I can make a Tesla car.
Hint: Create an variable called
my_tesla which is of class
Tesla which inherits from class
Story: As a programmer, I can make a Tata car.
Story: As a programmer, I can make a Toyota car.
Story: As a programmer, I can tell which model year a vehicle is from. Model years never change.
Hint: Make model year part of the initialization.
Story: As a programmer, I can turn on and off the lights on a given Vehicle.
Hint: Create method(s) to allow programmer to turn lights on and off. Which class are the methods in?
Story: As a programmer, I can determine if the lights are on or off. Lights start in the off position.
You should be able to test the car now: vehicle = Vehicle.new(...) vehicle.lights_on # should return false because they start false vehicle.lights_on = true # this should change the lights to true vehicle.lights_on # should now return true vehicle.lights_on = false # this should change the lights to false vehicle.lights_on? # should return false
Story: As a programmer, I can signal left and right. Turn signals starts off.
Story: As a programmer, I can determine the speed of a car. Speed starts at 0 km/h.
Story: As a programmer, I can speed my Teslas up by 10 per acceleration.
Story: As a programmer, I can slow my Teslas down by 7 per braking.
Story: As a programmer, I can speed my Tatas up by 2 per acceleration.
Story: As a programmer, I can slow my Tatas down by 1.25 per braking.
Story: As a programmer, I can speed my Toyotas up by 7 per acceleration.
Story: As a programmer, I can slow my Toyotas down by 5 per braking.
Story: As a programmer, I can call upon a car to tell me all it's information.
to_s on one or more classes. You can call a super class's
to_s with super.
Story: As a programmer, I can keep a collection of two of each kind of vehicle, all from different years.
Hint: Create two of each vehicles, all from different model years, and put them into an Array.
Story: As a programmer, I can sort my collection of cars based on model year.
Story: As a programmer, I can sort my collection of cars based on model.
Hint: Sort based on class name.
Story: As a programmer, I can sort my collection of cars based on model and then year.
Hint: Find out how the spaceship operator can help you with an array.
Today's Tentative Schedule
9:15am - Stand Up
9:30am - Introduction to Ruby: Classes & Inheritance followed by challenges
12:00 noon - Lunch
1:00pm - Continue challenges
4.30pm - Review
5:00pm - Class Ends