Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- # Classical Inheritance in Ruby
- For programmers who are new to Object Oriented Programming, the ideas behind classical inheritance can take some
- getting used to. This article walks you through the syntax of defining class inheritance in Ruby with explanations of
- each OOP feature along the way. You will have created many inheriting classes and used them in Ruby code by the end of
- this exercise.
- Create a new Ruby code file and copy the code examples into it as you go. Run the file with the provided driver code and
- read the output. Try writing your own driver code to try things out new concepts as they're introduced.
- ## A simple class
- In Ruby, all objects are instances of classes. There are a lot of built-in classes, but you can define your own if none
- of the built-in ones are sufficient for the problem you're trying to solve. This is often the case when you are working
- in a very specific problem domain. For example, if you want to program a traffic simulation, you probably need objects
- representing vehicles, which don't come pre-packaged with Ruby.
- Let's define a `Vehicle` class to see how that would look.
- ```ruby
- class Vehicle
- end
- ```
- Every class in Ruby starts off like this. The keyword `class` is followed by the name of the kind of object in
- UpperCamelCase, and the keyword `end` on a separate line. The specifics of the class come between those lines.
- To use this class in a program, you invoke the `.new` class method. Class methods are invoked by writing the class name
- followed by a dot and then the name of the method. In many cases, you can tell when documentation is referring to a
- class method because it precedes the method name with a dot (`.`) character. Let's try it.
- ```ruby
- my_vehicle = Vehicle.new
- puts my_vehicle
- ```
- If you run the above code after defining the `Vehicle` class, you should see a representation of a `Vehicle` object
- printed to the console. Presently `Vehicle` objects aren't very useful, but we can improve that by adding
- **attributes**.
- ## Instance attributes
- Custom objects become more useful when they are given attributes. Attributes are a combination of values that are
- unique to each instance of a class and methods for reading or writing those values. For example, every vehicle instance
- might have a value representing the kind of terrain that it can travel on, or it might have a value representing how
- it's propelled. Let's add those to our `Vehicle` class.
- The first step is to initialize the data. It doesn't make sense for a `Vehicle` to not have values for those two
- attributes, so we should force the programmer to specify values for each when they are creating a new instance. We do
- this by creating an instance method named `#initialize`, which is a special method name. It gets called automatically on
- new instances of our class when the class' `.new` method is called. The number sign (`#`) is just notation to indicate
- that it is an instance method, the same way the dot indicates a class method.
- ```ruby
- class Vehicle
- def initialize(terrain, propulsion)
- @terrain = terrain
- @propulsion = propulsion
- end
- end
- ```
- Adding an `#initialize` method is often the second thing you do when defining a new class. The parameters that the
- method takes represent the values that vary from one instance of the class to another. In this case, each `Vehicle` can
- travel on different terrain and have a different method of propulsion. The values of the passed parameters are stored
- inside of instance variables so that they can be accessed later on from within the class.
- Accessing the values from within the class is useful, but it is also useful to be able to ask an instance of `Vehicle`
- what its values for these attributes are, so we need to expose them. This is done by using the `attr_reader` method.
- When `attr_reader` is called from inside a class declaration, it creates "reader" methods (methods that just return the
- value of an instance variable) for each instance variable that matches the names of the symbols that are passed in as
- parameters. For example:
- ```ruby
- class Vehicle
- attr_reader :terrain, :propulsion
- def initialize(terrain, propulsion)
- @terrain = terrain
- @propulsion = propulsion
- end
- end
- ```
- Adding the `attr_reader` statement has generated two methods, one for each parameter. This is the same as:
- ```ruby
- class Vehicle
- def initialize(terrain, propulsion)
- @terrain = terrain
- @propulsion = propulsion
- end
- # Manually defined reader methods. It's better to use `attr_reader` for these.
- def terrain
- @terrain
- end
- def propulsion
- @propulsion
- end
- end
- ```
- You can define reader methods manually like this, but it's far more concise to use `attr_reader`. Now you can create
- `Vehicle` objects that can report their values for these attributes later on, like this:
- ```ruby
- steam_train = Vehicle.new('rails', 'steam')
- puts steam_train.terrain
- puts steam_train.propulsion
- skateboard = Vehicle.new('road', 'manual')
- puts skateboard.terrain
- puts skateboard.propulsion
- ```
- Notice that the `.new` class method now requires the two `#initialize` parameters to be passed into it. Under the hood,
- `.new` passes each parameter it receives along to the `#initialize` method of the new instance.
- Now we're getting to a point where we have object that are meaningful to a real problem domain. Often you have to create
- many instances of a class to solve a problem, and several of those instances will likely have the same values for some
- of their attributes. We can use inheritance to simplify this process.
- ## Class inheritance
- Let's say we need a bunch of `Vehicle` objects that represent boats. We could define each one separately and specify
- that the terrain they travel on is `'water'`, but it would be much simpler to define a `Boat` class that inherits from
- `Vehicle` that automatically specifies that for us.
- We can start the new `Boat` class the same way we start every class.
- ```ruby
- class Boat
- end
- ```
- The next step is to define the class that we want to inherit functionality from. The notation for that is to put a
- less-than sign (`<`) and then the name of the other class after the name of the new class.
- ```ruby
- class Boat < Vehicle
- end
- ```
- Now, the `Boat` class has a reference to the `Vehicle` class as its "superclass". This can even be seen by asking the
- `Boat` class object what its superclass is.
- ```ruby
- # Should print out `Vehicle`.
- puts Boat.superclass
- ```
- Notice the capital "B" on `Boat` in that snippet. Each class can be referred to as an object and it has its own
- attributes, such as its superclass. Take a look at the superclass of the `Vehicle` class.
- ```ruby
- # Should print out `Object`.
- puts Vehicle.superclass
- ```
- If you don't specify another class to inherit from, the default superclass for a new class in Ruby is the `Object`
- class.
- So, the new `Boat` class inherits from the `Vehicle` class, and therefore can be used just like a `Vehicle`, but when
- we try to initialize a new `Boat`, it still requires two parameters. This is because there is no `#initialize` method
- defined in the `Boat` class, so when creating a new `Boat` it goes to the superclass to find an `#initialize` method to
- call. Ruby will keep going from one superclass to another to find the `#initialize` method until it gets to a class
- where one is defined, and then it will call that one. This is the same for every instance method that you call on an
- object.
- To specify all boats use the `'water'` terrain, we can hard-code that into the `#initialize` method of `Boat`.
- ```ruby
- class Boat < Vehicle
- def initialize(propulsion)
- super('water', propulsion)
- end
- end
- ```
- There are a few things going on in the above class definition. The first is that we defined an instance method named
- `#initialize` that takes one argument: `propulsion`. This is the method that will be invoked when we call `Boat.new`.
- It takes this argument because different `Boat` objects can have different methods of propulsion. However, all boats
- travel on water, so we don't need to take a parameter for it.
- The next thing is the use of the `super` keyword. The way the `super` keyword works is just like any other method call.
- It calls the method of the same name as the one that the program is currently inside of, but in the superclass. In this
- case, the keyword `super` is inside of the `#initialize` method in the `Boat` class, therefore it will call the method
- of the same name, `#initialize`, in the superclass, which is `Vehicle`. The `#initialize` method of `Vehicle` takes two
- parameters, `terrain` and `propulsion`, so they both need to be passed.
- This is the completed `Boat` class. All it does is extend the `Vehicle` class by defaulting the `terrain` attribute to
- `'water'`. All the other instance methods, including the ones defined by `attr_reader`, are inherited and can be used
- on instances of `Boat`.
- ```ruby
- speed_boat = Boat.new('internal combustion')
- puts speed_boat.terrain
- puts speed_boat.propulsion
- ```
- `Boat.new` takes one parameter because the `#initialize` method in the `Boat` class takes one parameter. The `#terrain`
- and `#propulsion` instance methods work as expected because they are inherited from `Vehicle` and `super` was called
- inside of `Boat#initialize`, passing the parameter values, and the code inside of `Vehicle#initialize` set those values
- to instance variables.
- ## Querying inheritance and polymorphism
- With these two classes, there is now a new, interesting dynamic to the way you can write a program. If you have some
- code that only works when it has an instance of `Vehicle`, it will work for instances of `Vehicle` _and_ instances of
- `Boat`. Boats _are_ vehicles, after all. You can prove it by using the `#is_a?` method, which is inherited from `Object`
- by all the classes you define.
- ```ruby
- # Prints out details about the passed `vehicle` parameter.
- # Raises an ArgumentError if `vehicle` is not a `Vehicle`.
- def display_vehicle(vehicle)
- # Raises an error if `vehicle.is_a?(Vehicle)` returns false.
- # Note the use of capital "V" `Vehicle` here - the class object.
- raise ArgumentError, 'vehicle parameter must be a Vehicle' unless vehicle.is_a?(Vehicle)
- # Displays information about the passed `Vehicle`.
- puts "The vehicle can travel on #{vehicle.terrain} and is propelled by #{vehicle.propulsion}."
- end
- steam_boat = Boat.new('steam')
- display_vehicle(steam_boat)
- ```
- In the above example, `steam_boat.is_a?(Vehicle)` returns `true`, because `#is_a?` returns `true` if the class object
- that is passed into it is the same class or any of the superclasses of the object.
- The ability to use `Boat` instances when `Vehicle` instances are needed is what's known as "polymorphism". A subclass
- can be used anywhere its parent classes are needed.
- ## New attributes
- Sometimes classes that inherit from other classes will have new properties that their superclasses don't have. For
- example, a sail boat is always propelled by wind, so it makes sense to define a `SailBoat` class, but it may also have
- an attribute that holds the number of sails that the boat has. Let's define this as a new class. The first step, as
- usual, is the basic class declaration.
- ```ruby
- class SailBoat
- end
- ```
- After that, specify the superclass.
- ```ruby
- class SailBoat < Boat
- end
- ```
- In this case, we're inheriting from `Boat` because a sail boat _is a_ boat. However, we need to specify that all
- `SailBoat` objects have `'wind'` as their propulsion.
- ```ruby
- class SailBoat < Boat
- def initialize
- super('wind')
- end
- end
- ```
- The superclass `#initialize` method in the `Boat` class only takes one parameter, the propulsion, so we only pass it
- one. Now we're ready to add a new attribute to this class, the number of sails.
- ```ruby
- class SailBoat < Boat
- def initialize(number_of_sails)
- super('wind')
- @number_of_sails = number_of_sails
- end
- end
- ```
- Just like the attributes we defined earlier, we add a parameter to the `#initialize` method because the number of sails
- can vary from one `SailBoat` to another. Then we save the value that was passed in to an instance variable so that it
- can be used later.
- Right now, there's no way to ask a `SailBoat` how many sails it has after it's instantiated, so we can add an
- `attr_reader` method for that instance variable.
- ```ruby
- class SailBoat < Boat
- attr_reader :number_of_sails
- def initialize(number_of_sails)
- super('wind')
- @number_of_sails = number_of_sails
- end
- end
- ```
- Now we have a useful `SailBoat` class that can be initialized with a number of sails. Let's give it a try.
- ```ruby
- schooner = SailBoat.new(7)
- puts schooner.terrain
- puts schooner.propulsion
- puts schooner.number_of_sails
- ```
- Instances of `SailBoat` have inherited methods as well as methods of its own, such as `#number_of_sails`. When they are
- invoked on an instance, Ruby looks up which one to use by first looking at the class of the object itself, then the
- superclass if it couldn't find the method, then the superclass of the superclass, and so on.
- ## Class inheritance practice
- Let practice declaring classes using inheritance. This time we'll define a `Bicycle` class that is propelled manually,
- and a `Fixie` class that can travel on `'road'` and a `MountainBike` class that can travel `'off road'`.
- First, declare the `Bicycle` class.
- ```ruby
- class Bicycle
- end
- ```
- Then, inherit from `Vehicle`.
- ```ruby
- class Bicycle < Vehicle
- end
- ```
- Then, default the `propulsion` parameter to `'manual'`.
- ```ruby
- class Bicycle < Vehicle
- def initialize(terrain)
- super(terrain, 'manual')
- end
- end
- ```
- The `terrain` parameter is still necessary and is passed along to the `Vehicle#initialize` method to be saved as an
- instance variable. Next, define the `Fixie` class.
- ```ruby
- class Fixie
- end
- ```
- Then, inherit from `Bicycle`.
- ```ruby
- class Fixie < Bicycle
- end
- ```
- Then, default the `terrain` parameter to `'road'`.
- ```ruby
- class Fixie < Bicycle
- def initialize
- super('road')
- end
- end
- ```
- Only one parameter has to be passed to `super` because the superclass `Bicycle` only takes one parameter to its
- `#initialize` method. Next, define the `MountainBike` class.
- ```ruby
- class MountainBike
- end
- ```
- Then, inherit from `Bicycle`.
- ```ruby
- class MountainBike < Bicycle
- end
- ```
- Finally, default the `terrain` parameter to `'off road'`.
- ```ruby
- class MountainBike < Bicycle
- def initialize
- super('off road')
- end
- end
- ```
- Now you can make two different kinds of bikes and neither of the `Fixie` or `MountainBike` `#initialize` methods take
- any parameters, so they're super easy to create! Try them out by writing your own driver code that produces output to
- the console.
- ## Default and writable attributes
- So far all the attributes of these classes been initialized to values specified as parameters to the `#initialize`
- method. Sometimes, each new instance of a class will have a starting value for an attribute. For example, each new
- `MountainBike` might start with its brakes released. It's easy to add these kinds of attributes. You just set them to
- their starting value inside the `#initialize` method.
- Let's add a `braking` attribute to the `MountainBike` class that represents whether the brakes are being held, but
- let's give it a default value instead of taking its initial value as a parameter.
- ```ruby
- class MountainBike < Bicycle
- def initialize
- super('off road')
- @braking = false
- end
- end
- ```
- Now every new `MountainBike` instance starts with the brakes released. Of course, we can't read whether an instance of
- `MountainBike` is braking, so we should add an `attr_reader` for it.
- ```ruby
- class MountainBike < Bicycle
- attr_reader :braking
- def initialize
- super('off road')
- @braking = false
- end
- end
- ```
- Now we can access the value representing whether the `MountainBike` is braking. Let's try it out.
- ```ruby
- a_mountain_bike = MountainBike.new
- puts a_mountain_bike.terrain
- puts a_mountain_bike.propulsion
- puts a_mountain_bike.braking
- ```
- Ok, its brakes are off, but how do we change that? We did the right thing by defining the braking attribute as read
- only first. All attributes should start as `attr_reader`s until you know for a fact that you want to expose the ability
- to update them. Exposing a "writer" method as well is as easy as using the `attr_accessor` method instead of
- `attr_reader`. This will also expose a method for updating the value of the instance variable using the equals sign
- (`=`). This is how it looks.
- ```ruby
- class MountainBike < Bicycle
- attr_accessor :braking
- def initialize
- super('off road')
- @braking = false
- end
- end
- ```
- It's a very simple change. The `attr_accessor` method creates two instance methods that are equivalent to the following.
- ```ruby
- class MountainBike < Bicycle
- def initialize
- super('off road')
- @braking = false
- end
- # Manually defined accessor methods. It's better to use `attr_accessor` for these.
- def braking
- @braking
- end
- def braking=(braking)
- @braking = braking
- end
- end
- ```
- In Ruby, defining a method whose name ends with the equals sign, including automatically generated "writer" methods, is
- special. You can then use the following syntax for updating the value of an attribute on an object.
- ```ruby
- a_mountain_bike = MountainBike.new
- a_mountain_bike.braking = true
- puts a_mountain_bike.braking
- ```
- Now instances of `MountainBike` can break. As mentioned above, not all attributes should have "writer" methods.
- Generally you will define your own instance methods on a class that update the values of instance variables.
- ## Custom instance methods
- Most of the functionality of the classes you create will be inside of instance methods. The instance methods you define
- will typically change the values of instance variables that were initialized when the object was created. To see this,
- let's add front and rear gears to the `MountainBike` class.
- ```ruby
- class MountainBike < Bicycle
- attr_accessor :braking
- def initialize
- super('off road')
- @braking = false
- @front_gear = 1
- @rear_gear = 1
- end
- end
- ```
- When a new `MountainBike` is created, its front and rear gears are both in position `1`. Let's expose these using
- `attr_reader`.
- ```ruby
- class MountainBike < Bicycle
- attr_reader :front_gear, :rear_gear
- attr_accessor :braking
- def initialize
- super('off road')
- @braking = false
- @front_gear = 1
- @rear_gear = 1
- end
- end
- ```
- Because they are only readers, we can't just add them to the `attr_accessor` call for the braking attribute, so we
- add a separate line for the `attr_reader`s.
- The full gear that the bike is in can be calculated using the gears on the front and rear. Let's add an instance method
- that gets the actual gear that the bike is in.
- ```ruby
- class MountainBike < Bicycle
- attr_reader :front_gear, :rear_gear
- attr_accessor :braking
- def initialize
- super('off road')
- @braking = false
- @front_gear = 1
- @rear_gear = 1
- end
- def gear
- (front_gear - 1) * 6 + rear_gear
- end
- end
- ```
- The maximum rear gear is `6`, so the full gear number can be calculated as in the example above. The `#gear` instance
- method should return `1` for each new `MountainBike` until the gears are changed.
- ```ruby
- a_mountain_bike = MountainBike.new
- puts a_mountain_bike.gear
- ```
- Did you notice that inside the `#gear` method the `attr_reader` methods `#front_gear` and `#rear_gear` are being used
- instead of directly accessing the instance variables `@front_gear` and `@rear_gear`? This is a useful convention to
- follow because your class behaves in a predictable way to users (programmers using your class.) Some code that has an
- instance of `MountainBike` can access all the `#front_gear`, `#rear_gear`, and `#gear` methods, and their values are
- linked to one another in a predetermined way. This keeps the class' internal code and external, or public, API
- consistent. By using existing instance methods inside your new instance methods, the code is more resilient against
- future changes, too. The way instance variables are updated can change or the instance variables can be removed an
- replaced by computed values. If that happens, then you don't have to fix any code that was using the instance variables
- directly.
- Be careful when following this convention, though. If the method you're using is a "writer", that is, the name ends with
- an equals sign, then you **must** prefix the method name with `self.`. The `self` keyword refers to the current object
- instance, and you can access its methods using dot-notation just like you can outside the method. For example, if you
- wanted to implement a `#stop` instance method on `MountainBike` that turns on the brakes, it would have to be written
- the following way.
- ```ruby
- class MountainBike < Bicycle
- attr_reader :front_gear, :rear_gear
- attr_accessor :braking
- def initialize
- super('off road')
- @braking = false
- @front_gear = 1
- @rear_gear = 1
- end
- def gear
- (front_gear - 1) * 6 + rear_gear
- end
- def stop
- self.braking = true
- end
- end
- ```
- Note the use of `self.` in the `#stop` method. If it was omitted, then Ruby would just think you are defining a new
- local variable named `braking` instead of invoking the `#braking=` instance method.
- There is still no way to change the gear that the `MountainBike` is in. `MountainBike` objects should only be able to
- increase or decrease their gears by one at a time. Let's implement an instance method that increases the rear gear by
- `1`.
- ```ruby
- class MountainBike < Bicycle
- attr_reader :front_gear, :rear_gear
- attr_accessor :braking
- def initialize
- super('off road')
- @braking = false
- @front_gear = 1
- @rear_gear = 1
- end
- def gear
- (front_gear - 1) * 6 + rear_gear
- end
- def stop
- self.braking = true
- end
- def increase_rear_gear
- @rear_gear += 1
- end
- end
- ```
- The new `#increase_rear_gear` method can't use a "writer" method like the `#stop` method did because there is no
- writer method for the `@rear_gear` instance variable. Let's try this method out.
- ```ruby
- a_mountain_bike = MountainBike.new
- puts a_mountain_bike.gear
- a_mountain_bike.increase_rear_gear
- puts a_mountain_bike.gear
- ```
- Alright, now we can increase the rear gear and the total gear goes up.
- ## Class constants
- The rear gear of a `MountainBike` as previously mentioned, can't go above `6`. We need to prevent the `@rear_gear`
- instance variable from going above this amount.
- The value `6` has special meaning in the context of a `MountainBike`. In cases like these, instead of hard-coding the
- value `6` into multiple methods in a class, it should be defined as a constant. Constants are defined inside a class
- declaration, are named in ALL_CAPS, and can't be changed. They can be referred to inside instance methods of the class
- they're defined in by name, but from outside the class they must be prefixed with the scope resolution operator, which
- is two colons, (`::`). For example:
- ```ruby
- class MountainBike < Bicycle
- MAX_REAR_GEAR = 6
- attr_reader :front_gear, :rear_gear
- attr_accessor :braking
- def initialize
- super('off road')
- @braking = false
- @front_gear = 1
- @rear_gear = 1
- end
- def gear
- (front_gear - 1) * MAX_REAR_GEAR + rear_gear
- end
- def stop
- self.braking = true
- end
- def increase_rear_gear
- @rear_gear += 1
- end
- end
- ```
- The `6` that was previously being used in `#gear` has been replaced by the new constant `MAX_REAR_GEAR`. You can get
- the value of this constant from outside the `MountainBike` class like so:
- ```
- puts MountainBike::MAX_REAR_GEAR
- ```
- Note the use of the scope resolution operator, `::`. This can be useful if you need to access the value of the constant
- from another class.
- Now we can prevent `@rear_gear` from going over `MAX_REAR_GEAR` like this:
- ```ruby
- class MountainBike < Bicycle
- MAX_REAR_GEAR = 6
- attr_reader :front_gear, :rear_gear
- attr_accessor :braking
- def initialize
- super('off road')
- @braking = false
- @front_gear = 1
- @rear_gear = 1
- end
- def gear
- (front_gear - 1) * MAX_REAR_GEAR + rear_gear
- end
- def stop
- self.braking = true
- end
- def increase_rear_gear
- @rear_gear += 1 if rear_gear < MAX_REAR_GEAR
- end
- end
- ```
- Note the use of the `#rear_gear` method again. The instance variable is just being read here, so we can use the
- `attr_reader` that is defined.
- ## Class methods
- Sometimes there are actions that can be thought of as being done by the class of objects as opposed to individual
- instances. In these cases, class methods are used. For example, the `.new` method is an action of the `MountainBike`
- class. We could add a new action to the `MountainBike` class if we wanted to. Here's an example of adding a
- `.repair_kit` class method thar returns an array of tools for fixing `MountainBike`s.
- ```ruby
- class MountainBike < Bicycle
- MAX_REAR_GEAR = 6
- attr_reader :front_gear, :rear_gear
- attr_accessor :braking
- def initialize
- super('off road')
- @braking = false
- @front_gear = 1
- @rear_gear = 1
- end
- def gear
- (front_gear - 1) * MAX_REAR_GEAR + rear_gear
- end
- def stop
- self.braking = true
- end
- def increase_rear_gear
- @rear_gear += 1 if rear_gear < MAX_REAR_GEAR
- end
- def self.repair_kit
- ['wrench', 'pliers', 'pump']
- end
- end
- ```
- As you can see, the method name starts with `self.`. This is different from the `self.` earlier, which was _inside_ an
- instance method and therefore referred to the instance. When `self.` appears after `def` or inside a class method, it
- refers to the class object itself, in this case `MountainBike`. Now, the `MountainBike` class object has a new method
- that can be called on it.
- ```ruby
- puts MountainBike.repair_kit
- ```
- As you can see, no instance of the class had to be created. The method is used on the class object itself.
- ## More practice
- To practice working with classes and inheritance in Ruby, try some of the following.
- - Finish the `MountainBike` methods for increasing and decreasing the front and rear gears, making sure that the gears
- can't go below `1` and the front gear can't go above `3`.
- - Think of another kind of vehicle that wasn't discussed and come up with ways to define classes that take advantage of
- inheritance to simplify working with the subclasses.
- Good luck!
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement