Inheritance, Privacy and Scope

How to DRY up your code with good usage of inheritance, hide your important methods, and encapsulate everything in a proper scope.

Scroll down...

Content

Resources

Comments

We've shown you how to organize your scripts into methods and how to put those methods into classes but in this lesson we'll show you how classes help you DRY up your code with inheritance. In a similar vein, you'll also learn how Ruby determines which variables are accessible where and how you can shield certain methods from being usable from outside of a class.

We touched on these issues when we used attr_accessor to make instance variables "visible" from outside your instance in the previous lesson, but we'll dive deeper here and give you more tools to very explicitly decide what you do and don't want to show to the world.

We have two goals for this lesson:

  1. That you to learn how to use inheritance to extend the usefulness of your classes.
  2. To help you clarify your understanding of how Ruby determines who is "allowed" to see, use, and modify variables and methods.

Class Inheritance

Inheritance is the ability of one class to be a "child" of another class and therefore inherit all its characteristics, including methods and variables.

We saw inheritance early on when we demonstrated using the superclass method to see what a particular class inherits from. For instance the number 1 is...

  1. class FixNum...
  2. which inherits from Integer...
  3. which inherits from Numeric...
  4. which inherits from Object...
  5. which inherits from BasicObject.
> 1.class.superclass.superclass.superclass.superclass
#=> BasicObject

Why is Inheritance Useful?

Inheritance keeps our code DRY. It lets us not have to repeat a bunch of methods (say, to_s, which is implemented in the Object class) for every different subclass.

How Inheritance Works

In Ruby, a class inherits from another class using the < notation. Unlike some other languages, a class can only have ONE parent.

class Viking < Person

Now Viking has access to all of Person's methods. You say that Viking extends Person.

If you're thinking of inheriting purely to get access to the methods of the parent, consider using a module instead (since you can include as many modules as you want). Remember, you only get ONE parent (though you will also get all of that parent's ancestors as well) so make it count!

Overwriting Inherited Methods

You've previously seen us add methods to another existing class, like we did several times with Array, via "monkey patching" the class:

class Array
    def monkey_patcher
        puts "Swinging from the trees..."
    end
end

# IRB
> [1,2,3].monkey_patcher
Swinging from the trees...
#=> nil

You don't have to add new methods... you can use the same technique to completely overwrite existing methods. It would cause all kinds of problems here, but we could do:

class Array
    def each
        puts "HAHA no each method here!"
    end
end

> [1,2,3].each {|item| puts item }
HAHA no each method here!
#=> nil

If Viking extends Person, you similarly have the option to overwrite any of Person's methods. Maybe Vikings heal twice as fast as normal people. You could write:

class Person
    MAX_HEALTH = 120
    ...
    def heal
        self.health = [self.health + 1, MAX_HEALTH].min
    end
end

class Viking < Person
    ...
    def heal
        self.health = [self.health + 2, MAX_HEALTH].min
        puts "Ready for battle!"
    end
end

Super

You could fully overwrite a parent's method in the child like we saw above but that doesn't seem very DRY if you really just want to extend its functionality. To DRY up our overwritten method, we might rewrite it so that it calls the parent's heal method directly a couple of times by using the special super method.

super lets a class call its superclass's version of a method:

class Viking < Person
    ...
    def heal
        2.times { super }
        puts "Ready for battle!"
    end
end

You will often use that in your initialize method when you want to use the parent's initialize but just add a tweak or two of your own. You can pass in parameters as needed:

class Viking < Person
    def initialize(name, health, age, strength, weapon)
        super(name, health, age, strength)
        @weapon = weapon
    end
end

Again, it saves you the trouble of having to rewrite (and overwrite!) all those lines of code that were already taken care of by your parent class.

Scope

Scope is the formal term that represents when you can access a variable or method and when you can't. It's nothing explicit in the code (you're never calling a method named scope or anything like that); it's just a concept. If your variable is "in scope" then it's available for use, otherwise it's "out of scope".

Think of scope like a container of one-way glass around certain chunks of code. Inside that container, your variable or method can see (or use) anything in the world outside the container but the outside world can't see in.

Creating Scopes

A new scope is created when you first define a variable. That variable is then accessible by anything "downstream" of it in the code, until the current scope is exited (by leaving the containing method or loop, for instance):

def launch_longships(longships)

    # Here we can't yet use `longship`, `longships_count` 
    # or `longship_name`.  We CAN use `longships` because 
    # it was passed in above this point.
    launched_ships = 0

    # Now launched_ships is in scope so we can use it
    longships.each do |longship|
        # Now `longship` is in scope, so we can use it
        longship_name = "#{longship.owner.name}'s Reaver"

        # Now `longship_name` is in scope so we can use it
        longship.launch

        # We can edit `launched_ships` even though it wasn't
        # declared inside this block because it's in scope
        launched_ships += 1
        puts "#{longship_name} successfully launched!"
    end

    # Now we've exited the loop so `longship` and `longship_name` are no longer in scope so we cannot use them.
    puts "Excellent news! We've launched #{launched_ships} ships!"
end

A good rule of thumb for scope is that you create a new scope any time you should indent your code and any time within that indent that a new variable is defined.

Global Variables

You can, however, create variables in the global scope using the $ syntax. You can then access these variables from anywhere in your program. Be careful with these... you want to avoid polluting your global scope with a bunch of meaningless or, worse, conflicting values.

# Several variable declaration methods
some_var = some_thing       # local
var some_var = some_thing   # local
var $some_var = some_thing  # global

You should also avoid the habit of using global variables to store the state of your script or as a proxy for passing information between methods.

Method Scope aka Privacy

Method scope is similar but has some important differences because it deals much more explicitly with the notion of privacy.

As we said in the previous lesson on Classes vs Modules, instance methods can be called by any instance of a class (e.g. oleg.sleep) and class methods can be called directly on the class itself (e.g. Viking.new).

But what if we've got a really sensitive method like die that we don't want anyone else to be able to call directly... it should ONLY be available to other methods from within THAT PARTICULAR INSTANCE of a Viking. So we don't want to be able to say oleg.die from the command line, but we do want to be able to kill off oleg if he loses all his health.

To do this, we put the method inside a chunk of code marked private:

class Viking < Person
    ...
    def take_damage(damage)
        @health -= damage
        die if @health <= 0
    end

    private  # Everything below this is private

    def die
        puts "#{self.name} has been killed :("
        self.dead = true    # assume we've defined `dead`
    end
end

> oleg = Viking.create_warrior("Oleg")
> oleg.die
# NoMethodError: private method `die' called for #<Viking:0x007ffd4c041e50>
> oleg.take_damage(200)
Oleg has been killed :(
#=> true

Don't Reach Outside Your Scope

A method should not reach outside its scope to modify variables unless that is the stated intention of the method.

A method should take inputs and provide a return without needing to modify anything else. We could call this the "look but don't touch" principle... you can use variables from outside your method but don't modify them unless you must!

When to Use Private and Protected Methods

You should change the default thought in your head from :

"Everything is accessible, what do I need to hide?"

...to:

"Everything should be hidden, what do I absolutely need to make externally available?"

That principle will take you far, especially when designing things like APIs that will be used by other programs. The more you make available to people, the harder it will be later on to hide it again.

Privacy Reduces Dependencies

"Why should I hide my methods from myself?" is a valid question but it's not addressing the point -- "Privacy" has nothing to do with being secretive (or even "secure"). It directly addresses the larger principle we've covered before of reducing dependencies (coupling) between parts of your program and it signals intention to other programmers that a particular method is only an implementation detail and should not be used by any other code.

There's a saying in development that "If it's visible, it'll get used". At some point down the line, you or someone else will build some other part of your program and see that there's a method they can use because it's public. They'll begin using it and now you've got another dependency -- now if you change that method, you'll break another part of the program.

So keep everything as private as possible to help enforce good behavior and good design.

Implementing Privacy

If you create methods that should only be accessible by other methods within your instance, make them private. This is the default setting for instance variables unless you expose them using the afore-mentioned attr_accessor.

How do we put this stuff to good use?

In our above example, there's really no reason for us to be able to directly call take_damage on our Viking instance either... it's an implementation detail. Why would a user ever need to say oleg.take_damage(10) directly? So we should figure out how to hide that function behind privacy.

To do so, we might provide an even more high-level method to abstract take_damage away from our user, for instance a new method called attack. Then attack could figure out how damage is handled and call the take_damage method for us.

But we can't make our take_damage method private because otherwise it could only be called by the specific Viking who is DOING the attacking. We want to call it on the RECIPIENT of the attack (remember, private methods can only be called from within the same instance).

Since we don't want take_damage to be visible to anyone on the command line but we DO want it to be visible to the methods inside OTHER instances of Viking, we call that protected. protected provides most of the privacy of private but lets the methods inside other instances of the same class or its descendants also access it:

class Viking < Person
    ...
    def attack(recipient)
        if recipient.dead
            puts "#{recipient.name} is already dead!"
            return false
        end
        damage = (rand * 10 + 10).round(0)
        recipient.take_damage(damage)  # `take_damage` called on `recipient`!
    end

    protected
    def take_damage(damage)
        self.health -= damage
        puts "Ouch! #{self.name} took #{damage} damage and has #{self.health} health left"
        die if @health <= 0  
        # `die` called from within the same object as take_damage was (the `recipient` as well!)
    end

    private
    def die
        puts "#{self.name} has been killed :("
        self.dead = true  # assume we've defined a `dead` instance variable
    end
end

# IRB
> oleg = Viking.create_warrior("Oleg")
#=> #<Viking:0x007ffd4b8b5588 @age=24.58111251562904, @name="Oleg", @health=120, @strength=10, @dead=false> 
> sten = Viking.create_warrior("Sten")
#=> #<Viking:0x007ffd4b8e1700 @age=28.80998656037281, @name="Sten", @health=120, @strength=10, @dead=false> 
> 10.times { oleg.attack(sten) }
Ouch! Sten took 19 damage and has 101 health left
Ouch! Sten took 10 damage and has 91 health left
Ouch! Sten took 13 damage and has 78 health left
Ouch! Sten took 17 damage and has 61 health left
Ouch! Sten took 15 damage and has 46 health left
Ouch! Sten took 11 damage and has 35 health left
Ouch! Sten took 14 damage and has 21 health left
Ouch! Sten took 14 damage and has 7 health left
Ouch! Sten took 18 damage and has -11 health left
Sten has been killed :(
Sten is already dead!
#=> 10 
> sten
#=> #<Viking:0x007ffd4c048840 @age=25.601709008134428, @name="Sten", @health=-11, @strength=10, @dead=true>

Wrapping Up

Privacy can be summed up by saying:

"Don't let anything access any methods or variables that it doesn't need to. It's on a 'need-to-know' basis, and it doesn't need to know."

Privacy may not be a big issue while you're building toy projects, but becomes more important when you're interfacing with the real world and you want to zip up your classes tightly to prevent any malicious or unintended shenanigans from occurring.

Scope is a concept you'll see again and again in every programming language you learn. Inheritance is another very common feature of programming languages, though it's often implemented in slightly different ways. You'll get another taste of it in JavaScript soon enough.



Sign up to track your progress for free

There are ( ) additional resources for this lesson. Check them out!

Sorry, comments aren't active just yet!

Next Lesson: Best Practices for Working With Classes