Advertisement
Guest User

Untitled

a guest
Feb 7th, 2016
70
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 25.79 KB | None | 0 0
  1. # Classical Inheritance in Ruby
  2.  
  3. For programmers who are new to Object Oriented Programming, the ideas behind classical inheritance can take some
  4. getting used to. This article walks you through the syntax of defining class inheritance in Ruby with explanations of
  5. each OOP feature along the way. You will have created many inheriting classes and used them in Ruby code by the end of
  6. this exercise.
  7.  
  8. 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
  9. read the output. Try writing your own driver code to try things out new concepts as they're introduced.
  10.  
  11. ## A simple class
  12.  
  13. In Ruby, all objects are instances of classes. There are a lot of built-in classes, but you can define your own if none
  14. of the built-in ones are sufficient for the problem you're trying to solve. This is often the case when you are working
  15. in a very specific problem domain. For example, if you want to program a traffic simulation, you probably need objects
  16. representing vehicles, which don't come pre-packaged with Ruby.
  17.  
  18. Let's define a `Vehicle` class to see how that would look.
  19.  
  20. ```ruby
  21. class Vehicle
  22. end
  23. ```
  24.  
  25. Every class in Ruby starts off like this. The keyword `class` is followed by the name of the kind of object in
  26. UpperCamelCase, and the keyword `end` on a separate line. The specifics of the class come between those lines.
  27.  
  28. To use this class in a program, you invoke the `.new` class method. Class methods are invoked by writing the class name
  29. followed by a dot and then the name of the method. In many cases, you can tell when documentation is referring to a
  30. class method because it precedes the method name with a dot (`.`) character. Let's try it.
  31.  
  32. ```ruby
  33. my_vehicle = Vehicle.new
  34. puts my_vehicle
  35. ```
  36.  
  37. If you run the above code after defining the `Vehicle` class, you should see a representation of a `Vehicle` object
  38. printed to the console. Presently `Vehicle` objects aren't very useful, but we can improve that by adding
  39. **attributes**.
  40.  
  41. ## Instance attributes
  42.  
  43. Custom objects become more useful when they are given attributes. Attributes are a combination of values that are
  44. unique to each instance of a class and methods for reading or writing those values. For example, every vehicle instance
  45. might have a value representing the kind of terrain that it can travel on, or it might have a value representing how
  46. it's propelled. Let's add those to our `Vehicle` class.
  47.  
  48. The first step is to initialize the data. It doesn't make sense for a `Vehicle` to not have values for those two
  49. attributes, so we should force the programmer to specify values for each when they are creating a new instance. We do
  50. this by creating an instance method named `#initialize`, which is a special method name. It gets called automatically on
  51. new instances of our class when the class' `.new` method is called. The number sign (`#`) is just notation to indicate
  52. that it is an instance method, the same way the dot indicates a class method.
  53.  
  54. ```ruby
  55. class Vehicle
  56.  
  57. def initialize(terrain, propulsion)
  58. @terrain = terrain
  59. @propulsion = propulsion
  60. end
  61.  
  62. end
  63. ```
  64.  
  65. Adding an `#initialize` method is often the second thing you do when defining a new class. The parameters that the
  66. method takes represent the values that vary from one instance of the class to another. In this case, each `Vehicle` can
  67. travel on different terrain and have a different method of propulsion. The values of the passed parameters are stored
  68. inside of instance variables so that they can be accessed later on from within the class.
  69.  
  70. Accessing the values from within the class is useful, but it is also useful to be able to ask an instance of `Vehicle`
  71. what its values for these attributes are, so we need to expose them. This is done by using the `attr_reader` method.
  72. When `attr_reader` is called from inside a class declaration, it creates "reader" methods (methods that just return the
  73. value of an instance variable) for each instance variable that matches the names of the symbols that are passed in as
  74. parameters. For example:
  75.  
  76. ```ruby
  77. class Vehicle
  78.  
  79. attr_reader :terrain, :propulsion
  80.  
  81. def initialize(terrain, propulsion)
  82. @terrain = terrain
  83. @propulsion = propulsion
  84. end
  85.  
  86. end
  87. ```
  88.  
  89. Adding the `attr_reader` statement has generated two methods, one for each parameter. This is the same as:
  90.  
  91. ```ruby
  92. class Vehicle
  93.  
  94. def initialize(terrain, propulsion)
  95. @terrain = terrain
  96. @propulsion = propulsion
  97. end
  98.  
  99. # Manually defined reader methods. It's better to use `attr_reader` for these.
  100.  
  101. def terrain
  102. @terrain
  103. end
  104.  
  105. def propulsion
  106. @propulsion
  107. end
  108.  
  109. end
  110. ```
  111.  
  112. You can define reader methods manually like this, but it's far more concise to use `attr_reader`. Now you can create
  113. `Vehicle` objects that can report their values for these attributes later on, like this:
  114.  
  115. ```ruby
  116. steam_train = Vehicle.new('rails', 'steam')
  117. puts steam_train.terrain
  118. puts steam_train.propulsion
  119. skateboard = Vehicle.new('road', 'manual')
  120. puts skateboard.terrain
  121. puts skateboard.propulsion
  122. ```
  123.  
  124. Notice that the `.new` class method now requires the two `#initialize` parameters to be passed into it. Under the hood,
  125. `.new` passes each parameter it receives along to the `#initialize` method of the new instance.
  126.  
  127. Now we're getting to a point where we have object that are meaningful to a real problem domain. Often you have to create
  128. many instances of a class to solve a problem, and several of those instances will likely have the same values for some
  129. of their attributes. We can use inheritance to simplify this process.
  130.  
  131. ## Class inheritance
  132.  
  133. Let's say we need a bunch of `Vehicle` objects that represent boats. We could define each one separately and specify
  134. that the terrain they travel on is `'water'`, but it would be much simpler to define a `Boat` class that inherits from
  135. `Vehicle` that automatically specifies that for us.
  136.  
  137. We can start the new `Boat` class the same way we start every class.
  138.  
  139. ```ruby
  140. class Boat
  141. end
  142. ```
  143.  
  144. The next step is to define the class that we want to inherit functionality from. The notation for that is to put a
  145. less-than sign (`<`) and then the name of the other class after the name of the new class.
  146.  
  147. ```ruby
  148. class Boat < Vehicle
  149. end
  150. ```
  151.  
  152. Now, the `Boat` class has a reference to the `Vehicle` class as its "superclass". This can even be seen by asking the
  153. `Boat` class object what its superclass is.
  154.  
  155. ```ruby
  156. # Should print out `Vehicle`.
  157. puts Boat.superclass
  158. ```
  159.  
  160. Notice the capital "B" on `Boat` in that snippet. Each class can be referred to as an object and it has its own
  161. attributes, such as its superclass. Take a look at the superclass of the `Vehicle` class.
  162.  
  163. ```ruby
  164. # Should print out `Object`.
  165. puts Vehicle.superclass
  166. ```
  167.  
  168. If you don't specify another class to inherit from, the default superclass for a new class in Ruby is the `Object`
  169. class.
  170.  
  171. So, the new `Boat` class inherits from the `Vehicle` class, and therefore can be used just like a `Vehicle`, but when
  172. we try to initialize a new `Boat`, it still requires two parameters. This is because there is no `#initialize` method
  173. defined in the `Boat` class, so when creating a new `Boat` it goes to the superclass to find an `#initialize` method to
  174. call. Ruby will keep going from one superclass to another to find the `#initialize` method until it gets to a class
  175. where one is defined, and then it will call that one. This is the same for every instance method that you call on an
  176. object.
  177.  
  178. To specify all boats use the `'water'` terrain, we can hard-code that into the `#initialize` method of `Boat`.
  179.  
  180. ```ruby
  181. class Boat < Vehicle
  182.  
  183. def initialize(propulsion)
  184. super('water', propulsion)
  185. end
  186.  
  187. end
  188. ```
  189.  
  190. There are a few things going on in the above class definition. The first is that we defined an instance method named
  191. `#initialize` that takes one argument: `propulsion`. This is the method that will be invoked when we call `Boat.new`.
  192. It takes this argument because different `Boat` objects can have different methods of propulsion. However, all boats
  193. travel on water, so we don't need to take a parameter for it.
  194.  
  195. The next thing is the use of the `super` keyword. The way the `super` keyword works is just like any other method call.
  196. It calls the method of the same name as the one that the program is currently inside of, but in the superclass. In this
  197. case, the keyword `super` is inside of the `#initialize` method in the `Boat` class, therefore it will call the method
  198. of the same name, `#initialize`, in the superclass, which is `Vehicle`. The `#initialize` method of `Vehicle` takes two
  199. parameters, `terrain` and `propulsion`, so they both need to be passed.
  200.  
  201. This is the completed `Boat` class. All it does is extend the `Vehicle` class by defaulting the `terrain` attribute to
  202. `'water'`. All the other instance methods, including the ones defined by `attr_reader`, are inherited and can be used
  203. on instances of `Boat`.
  204.  
  205. ```ruby
  206. speed_boat = Boat.new('internal combustion')
  207. puts speed_boat.terrain
  208. puts speed_boat.propulsion
  209. ```
  210.  
  211. `Boat.new` takes one parameter because the `#initialize` method in the `Boat` class takes one parameter. The `#terrain`
  212. and `#propulsion` instance methods work as expected because they are inherited from `Vehicle` and `super` was called
  213. inside of `Boat#initialize`, passing the parameter values, and the code inside of `Vehicle#initialize` set those values
  214. to instance variables.
  215.  
  216. ## Querying inheritance and polymorphism
  217.  
  218. With these two classes, there is now a new, interesting dynamic to the way you can write a program. If you have some
  219. code that only works when it has an instance of `Vehicle`, it will work for instances of `Vehicle` _and_ instances of
  220. `Boat`. Boats _are_ vehicles, after all. You can prove it by using the `#is_a?` method, which is inherited from `Object`
  221. by all the classes you define.
  222.  
  223. ```ruby
  224. # Prints out details about the passed `vehicle` parameter.
  225. # Raises an ArgumentError if `vehicle` is not a `Vehicle`.
  226. def display_vehicle(vehicle)
  227.  
  228. # Raises an error if `vehicle.is_a?(Vehicle)` returns false.
  229. # Note the use of capital "V" `Vehicle` here - the class object.
  230. raise ArgumentError, 'vehicle parameter must be a Vehicle' unless vehicle.is_a?(Vehicle)
  231.  
  232. # Displays information about the passed `Vehicle`.
  233. puts "The vehicle can travel on #{vehicle.terrain} and is propelled by #{vehicle.propulsion}."
  234. end
  235.  
  236. steam_boat = Boat.new('steam')
  237. display_vehicle(steam_boat)
  238. ```
  239.  
  240. In the above example, `steam_boat.is_a?(Vehicle)` returns `true`, because `#is_a?` returns `true` if the class object
  241. that is passed into it is the same class or any of the superclasses of the object.
  242.  
  243. The ability to use `Boat` instances when `Vehicle` instances are needed is what's known as "polymorphism". A subclass
  244. can be used anywhere its parent classes are needed.
  245.  
  246. ## New attributes
  247.  
  248. Sometimes classes that inherit from other classes will have new properties that their superclasses don't have. For
  249. example, a sail boat is always propelled by wind, so it makes sense to define a `SailBoat` class, but it may also have
  250. an attribute that holds the number of sails that the boat has. Let's define this as a new class. The first step, as
  251. usual, is the basic class declaration.
  252.  
  253. ```ruby
  254. class SailBoat
  255. end
  256. ```
  257.  
  258. After that, specify the superclass.
  259.  
  260. ```ruby
  261. class SailBoat < Boat
  262. end
  263. ```
  264.  
  265. In this case, we're inheriting from `Boat` because a sail boat _is a_ boat. However, we need to specify that all
  266. `SailBoat` objects have `'wind'` as their propulsion.
  267.  
  268. ```ruby
  269. class SailBoat < Boat
  270.  
  271. def initialize
  272. super('wind')
  273. end
  274.  
  275. end
  276. ```
  277.  
  278. The superclass `#initialize` method in the `Boat` class only takes one parameter, the propulsion, so we only pass it
  279. one. Now we're ready to add a new attribute to this class, the number of sails.
  280.  
  281. ```ruby
  282. class SailBoat < Boat
  283.  
  284. def initialize(number_of_sails)
  285. super('wind')
  286. @number_of_sails = number_of_sails
  287. end
  288.  
  289. end
  290. ```
  291.  
  292. Just like the attributes we defined earlier, we add a parameter to the `#initialize` method because the number of sails
  293. can vary from one `SailBoat` to another. Then we save the value that was passed in to an instance variable so that it
  294. can be used later.
  295.  
  296. Right now, there's no way to ask a `SailBoat` how many sails it has after it's instantiated, so we can add an
  297. `attr_reader` method for that instance variable.
  298.  
  299. ```ruby
  300. class SailBoat < Boat
  301.  
  302. attr_reader :number_of_sails
  303.  
  304. def initialize(number_of_sails)
  305. super('wind')
  306. @number_of_sails = number_of_sails
  307. end
  308.  
  309. end
  310. ```
  311.  
  312. Now we have a useful `SailBoat` class that can be initialized with a number of sails. Let's give it a try.
  313.  
  314. ```ruby
  315. schooner = SailBoat.new(7)
  316. puts schooner.terrain
  317. puts schooner.propulsion
  318. puts schooner.number_of_sails
  319. ```
  320.  
  321. Instances of `SailBoat` have inherited methods as well as methods of its own, such as `#number_of_sails`. When they are
  322. invoked on an instance, Ruby looks up which one to use by first looking at the class of the object itself, then the
  323. superclass if it couldn't find the method, then the superclass of the superclass, and so on.
  324.  
  325. ## Class inheritance practice
  326.  
  327. Let practice declaring classes using inheritance. This time we'll define a `Bicycle` class that is propelled manually,
  328. and a `Fixie` class that can travel on `'road'` and a `MountainBike` class that can travel `'off road'`.
  329.  
  330. First, declare the `Bicycle` class.
  331.  
  332. ```ruby
  333. class Bicycle
  334. end
  335. ```
  336.  
  337. Then, inherit from `Vehicle`.
  338.  
  339. ```ruby
  340. class Bicycle < Vehicle
  341. end
  342. ```
  343.  
  344. Then, default the `propulsion` parameter to `'manual'`.
  345.  
  346. ```ruby
  347. class Bicycle < Vehicle
  348.  
  349. def initialize(terrain)
  350. super(terrain, 'manual')
  351. end
  352.  
  353. end
  354. ```
  355.  
  356. The `terrain` parameter is still necessary and is passed along to the `Vehicle#initialize` method to be saved as an
  357. instance variable. Next, define the `Fixie` class.
  358.  
  359. ```ruby
  360. class Fixie
  361. end
  362. ```
  363.  
  364. Then, inherit from `Bicycle`.
  365.  
  366. ```ruby
  367. class Fixie < Bicycle
  368. end
  369. ```
  370.  
  371. Then, default the `terrain` parameter to `'road'`.
  372.  
  373.  
  374. ```ruby
  375. class Fixie < Bicycle
  376.  
  377. def initialize
  378. super('road')
  379. end
  380.  
  381. end
  382. ```
  383.  
  384. Only one parameter has to be passed to `super` because the superclass `Bicycle` only takes one parameter to its
  385. `#initialize` method. Next, define the `MountainBike` class.
  386.  
  387. ```ruby
  388. class MountainBike
  389. end
  390. ```
  391.  
  392. Then, inherit from `Bicycle`.
  393.  
  394. ```ruby
  395. class MountainBike < Bicycle
  396. end
  397. ```
  398.  
  399. Finally, default the `terrain` parameter to `'off road'`.
  400.  
  401. ```ruby
  402. class MountainBike < Bicycle
  403.  
  404. def initialize
  405. super('off road')
  406. end
  407.  
  408. end
  409. ```
  410.  
  411. Now you can make two different kinds of bikes and neither of the `Fixie` or `MountainBike` `#initialize` methods take
  412. any parameters, so they're super easy to create! Try them out by writing your own driver code that produces output to
  413. the console.
  414.  
  415. ## Default and writable attributes
  416.  
  417. So far all the attributes of these classes been initialized to values specified as parameters to the `#initialize`
  418. method. Sometimes, each new instance of a class will have a starting value for an attribute. For example, each new
  419. `MountainBike` might start with its brakes released. It's easy to add these kinds of attributes. You just set them to
  420. their starting value inside the `#initialize` method.
  421.  
  422. Let's add a `braking` attribute to the `MountainBike` class that represents whether the brakes are being held, but
  423. let's give it a default value instead of taking its initial value as a parameter.
  424.  
  425. ```ruby
  426. class MountainBike < Bicycle
  427.  
  428. def initialize
  429. super('off road')
  430. @braking = false
  431. end
  432.  
  433. end
  434. ```
  435.  
  436. Now every new `MountainBike` instance starts with the brakes released. Of course, we can't read whether an instance of
  437. `MountainBike` is braking, so we should add an `attr_reader` for it.
  438.  
  439. ```ruby
  440. class MountainBike < Bicycle
  441.  
  442. attr_reader :braking
  443.  
  444. def initialize
  445. super('off road')
  446. @braking = false
  447. end
  448.  
  449. end
  450. ```
  451.  
  452. Now we can access the value representing whether the `MountainBike` is braking. Let's try it out.
  453.  
  454. ```ruby
  455. a_mountain_bike = MountainBike.new
  456. puts a_mountain_bike.terrain
  457. puts a_mountain_bike.propulsion
  458. puts a_mountain_bike.braking
  459. ```
  460.  
  461. Ok, its brakes are off, but how do we change that? We did the right thing by defining the braking attribute as read
  462. only first. All attributes should start as `attr_reader`s until you know for a fact that you want to expose the ability
  463. to update them. Exposing a "writer" method as well is as easy as using the `attr_accessor` method instead of
  464. `attr_reader`. This will also expose a method for updating the value of the instance variable using the equals sign
  465. (`=`). This is how it looks.
  466.  
  467. ```ruby
  468. class MountainBike < Bicycle
  469.  
  470. attr_accessor :braking
  471.  
  472. def initialize
  473. super('off road')
  474. @braking = false
  475. end
  476.  
  477. end
  478. ```
  479.  
  480. It's a very simple change. The `attr_accessor` method creates two instance methods that are equivalent to the following.
  481.  
  482. ```ruby
  483. class MountainBike < Bicycle
  484.  
  485. def initialize
  486. super('off road')
  487. @braking = false
  488. end
  489.  
  490. # Manually defined accessor methods. It's better to use `attr_accessor` for these.
  491.  
  492. def braking
  493. @braking
  494. end
  495.  
  496. def braking=(braking)
  497. @braking = braking
  498. end
  499.  
  500. end
  501. ```
  502.  
  503. In Ruby, defining a method whose name ends with the equals sign, including automatically generated "writer" methods, is
  504. special. You can then use the following syntax for updating the value of an attribute on an object.
  505.  
  506. ```ruby
  507. a_mountain_bike = MountainBike.new
  508. a_mountain_bike.braking = true
  509. puts a_mountain_bike.braking
  510. ```
  511.  
  512. Now instances of `MountainBike` can break. As mentioned above, not all attributes should have "writer" methods.
  513. Generally you will define your own instance methods on a class that update the values of instance variables.
  514.  
  515. ## Custom instance methods
  516.  
  517. Most of the functionality of the classes you create will be inside of instance methods. The instance methods you define
  518. will typically change the values of instance variables that were initialized when the object was created. To see this,
  519. let's add front and rear gears to the `MountainBike` class.
  520.  
  521. ```ruby
  522. class MountainBike < Bicycle
  523.  
  524. attr_accessor :braking
  525.  
  526. def initialize
  527. super('off road')
  528. @braking = false
  529. @front_gear = 1
  530. @rear_gear = 1
  531. end
  532.  
  533. end
  534. ```
  535.  
  536. When a new `MountainBike` is created, its front and rear gears are both in position `1`. Let's expose these using
  537. `attr_reader`.
  538.  
  539. ```ruby
  540. class MountainBike < Bicycle
  541.  
  542. attr_reader :front_gear, :rear_gear
  543.  
  544. attr_accessor :braking
  545.  
  546. def initialize
  547. super('off road')
  548. @braking = false
  549. @front_gear = 1
  550. @rear_gear = 1
  551. end
  552.  
  553. end
  554. ```
  555.  
  556. Because they are only readers, we can't just add them to the `attr_accessor` call for the braking attribute, so we
  557. add a separate line for the `attr_reader`s.
  558.  
  559. 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
  560. that gets the actual gear that the bike is in.
  561.  
  562. ```ruby
  563. class MountainBike < Bicycle
  564.  
  565. attr_reader :front_gear, :rear_gear
  566.  
  567. attr_accessor :braking
  568.  
  569. def initialize
  570. super('off road')
  571. @braking = false
  572. @front_gear = 1
  573. @rear_gear = 1
  574. end
  575.  
  576. def gear
  577. (front_gear - 1) * 6 + rear_gear
  578. end
  579.  
  580. end
  581. ```
  582.  
  583. The maximum rear gear is `6`, so the full gear number can be calculated as in the example above. The `#gear` instance
  584. method should return `1` for each new `MountainBike` until the gears are changed.
  585.  
  586. ```ruby
  587. a_mountain_bike = MountainBike.new
  588. puts a_mountain_bike.gear
  589. ```
  590.  
  591. Did you notice that inside the `#gear` method the `attr_reader` methods `#front_gear` and `#rear_gear` are being used
  592. instead of directly accessing the instance variables `@front_gear` and `@rear_gear`? This is a useful convention to
  593. follow because your class behaves in a predictable way to users (programmers using your class.) Some code that has an
  594. instance of `MountainBike` can access all the `#front_gear`, `#rear_gear`, and `#gear` methods, and their values are
  595. linked to one another in a predetermined way. This keeps the class' internal code and external, or public, API
  596. consistent. By using existing instance methods inside your new instance methods, the code is more resilient against
  597. future changes, too. The way instance variables are updated can change or the instance variables can be removed an
  598. replaced by computed values. If that happens, then you don't have to fix any code that was using the instance variables
  599. directly.
  600.  
  601. Be careful when following this convention, though. If the method you're using is a "writer", that is, the name ends with
  602. an equals sign, then you **must** prefix the method name with `self.`. The `self` keyword refers to the current object
  603. instance, and you can access its methods using dot-notation just like you can outside the method. For example, if you
  604. wanted to implement a `#stop` instance method on `MountainBike` that turns on the brakes, it would have to be written
  605. the following way.
  606.  
  607.  
  608. ```ruby
  609. class MountainBike < Bicycle
  610.  
  611. attr_reader :front_gear, :rear_gear
  612.  
  613. attr_accessor :braking
  614.  
  615. def initialize
  616. super('off road')
  617. @braking = false
  618. @front_gear = 1
  619. @rear_gear = 1
  620. end
  621.  
  622. def gear
  623. (front_gear - 1) * 6 + rear_gear
  624. end
  625.  
  626. def stop
  627. self.braking = true
  628. end
  629.  
  630. end
  631. ```
  632.  
  633. Note the use of `self.` in the `#stop` method. If it was omitted, then Ruby would just think you are defining a new
  634. local variable named `braking` instead of invoking the `#braking=` instance method.
  635.  
  636. There is still no way to change the gear that the `MountainBike` is in. `MountainBike` objects should only be able to
  637. increase or decrease their gears by one at a time. Let's implement an instance method that increases the rear gear by
  638. `1`.
  639.  
  640. ```ruby
  641. class MountainBike < Bicycle
  642.  
  643. attr_reader :front_gear, :rear_gear
  644.  
  645. attr_accessor :braking
  646.  
  647. def initialize
  648. super('off road')
  649. @braking = false
  650. @front_gear = 1
  651. @rear_gear = 1
  652. end
  653.  
  654. def gear
  655. (front_gear - 1) * 6 + rear_gear
  656. end
  657.  
  658. def stop
  659. self.braking = true
  660. end
  661.  
  662. def increase_rear_gear
  663. @rear_gear += 1
  664. end
  665.  
  666. end
  667. ```
  668.  
  669. The new `#increase_rear_gear` method can't use a "writer" method like the `#stop` method did because there is no
  670. writer method for the `@rear_gear` instance variable. Let's try this method out.
  671.  
  672. ```ruby
  673. a_mountain_bike = MountainBike.new
  674. puts a_mountain_bike.gear
  675. a_mountain_bike.increase_rear_gear
  676. puts a_mountain_bike.gear
  677. ```
  678.  
  679. Alright, now we can increase the rear gear and the total gear goes up.
  680.  
  681. ## Class constants
  682.  
  683. The rear gear of a `MountainBike` as previously mentioned, can't go above `6`. We need to prevent the `@rear_gear`
  684. instance variable from going above this amount.
  685.  
  686. The value `6` has special meaning in the context of a `MountainBike`. In cases like these, instead of hard-coding the
  687. value `6` into multiple methods in a class, it should be defined as a constant. Constants are defined inside a class
  688. declaration, are named in ALL_CAPS, and can't be changed. They can be referred to inside instance methods of the class
  689. they're defined in by name, but from outside the class they must be prefixed with the scope resolution operator, which
  690. is two colons, (`::`). For example:
  691.  
  692. ```ruby
  693. class MountainBike < Bicycle
  694.  
  695. MAX_REAR_GEAR = 6
  696.  
  697. attr_reader :front_gear, :rear_gear
  698.  
  699. attr_accessor :braking
  700.  
  701. def initialize
  702. super('off road')
  703. @braking = false
  704. @front_gear = 1
  705. @rear_gear = 1
  706. end
  707.  
  708. def gear
  709. (front_gear - 1) * MAX_REAR_GEAR + rear_gear
  710. end
  711.  
  712. def stop
  713. self.braking = true
  714. end
  715.  
  716. def increase_rear_gear
  717. @rear_gear += 1
  718. end
  719.  
  720. end
  721. ```
  722.  
  723. The `6` that was previously being used in `#gear` has been replaced by the new constant `MAX_REAR_GEAR`. You can get
  724. the value of this constant from outside the `MountainBike` class like so:
  725.  
  726. ```
  727. puts MountainBike::MAX_REAR_GEAR
  728. ```
  729.  
  730. Note the use of the scope resolution operator, `::`. This can be useful if you need to access the value of the constant
  731. from another class.
  732.  
  733. Now we can prevent `@rear_gear` from going over `MAX_REAR_GEAR` like this:
  734.  
  735. ```ruby
  736. class MountainBike < Bicycle
  737.  
  738. MAX_REAR_GEAR = 6
  739.  
  740. attr_reader :front_gear, :rear_gear
  741.  
  742. attr_accessor :braking
  743.  
  744. def initialize
  745. super('off road')
  746. @braking = false
  747. @front_gear = 1
  748. @rear_gear = 1
  749. end
  750.  
  751. def gear
  752. (front_gear - 1) * MAX_REAR_GEAR + rear_gear
  753. end
  754.  
  755. def stop
  756. self.braking = true
  757. end
  758.  
  759. def increase_rear_gear
  760. @rear_gear += 1 if rear_gear < MAX_REAR_GEAR
  761. end
  762.  
  763. end
  764. ```
  765.  
  766. Note the use of the `#rear_gear` method again. The instance variable is just being read here, so we can use the
  767. `attr_reader` that is defined.
  768.  
  769. ## Class methods
  770.  
  771. Sometimes there are actions that can be thought of as being done by the class of objects as opposed to individual
  772. instances. In these cases, class methods are used. For example, the `.new` method is an action of the `MountainBike`
  773. class. We could add a new action to the `MountainBike` class if we wanted to. Here's an example of adding a
  774. `.repair_kit` class method thar returns an array of tools for fixing `MountainBike`s.
  775.  
  776. ```ruby
  777. class MountainBike < Bicycle
  778.  
  779. MAX_REAR_GEAR = 6
  780.  
  781. attr_reader :front_gear, :rear_gear
  782.  
  783. attr_accessor :braking
  784.  
  785. def initialize
  786. super('off road')
  787. @braking = false
  788. @front_gear = 1
  789. @rear_gear = 1
  790. end
  791.  
  792. def gear
  793. (front_gear - 1) * MAX_REAR_GEAR + rear_gear
  794. end
  795.  
  796. def stop
  797. self.braking = true
  798. end
  799.  
  800. def increase_rear_gear
  801. @rear_gear += 1 if rear_gear < MAX_REAR_GEAR
  802. end
  803.  
  804. def self.repair_kit
  805. ['wrench', 'pliers', 'pump']
  806. end
  807.  
  808. end
  809. ```
  810.  
  811. As you can see, the method name starts with `self.`. This is different from the `self.` earlier, which was _inside_ an
  812. instance method and therefore referred to the instance. When `self.` appears after `def` or inside a class method, it
  813. refers to the class object itself, in this case `MountainBike`. Now, the `MountainBike` class object has a new method
  814. that can be called on it.
  815.  
  816. ```ruby
  817. puts MountainBike.repair_kit
  818. ```
  819.  
  820. As you can see, no instance of the class had to be created. The method is used on the class object itself.
  821.  
  822. ## More practice
  823.  
  824. To practice working with classes and inheritance in Ruby, try some of the following.
  825.  
  826. - Finish the `MountainBike` methods for increasing and decreasing the front and rear gears, making sure that the gears
  827. can't go below `1` and the front gear can't go above `3`.
  828. - Think of another kind of vehicle that wasn't discussed and come up with ways to define classes that take advantage of
  829. inheritance to simplify working with the subclasses.
  830.  
  831. Good luck!
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement