Objects

At this moment just have two classes that do nothing and have no difference at all, and a Person class that can only learn programming languages. Let's add some properties that would describe an instance of each class. For example, all Person objects will need a name, age, job properties. Dog and Cats objects may have size, color, smartness properties.

The easiest way to define some property is to add attr_accessor method and specify which variables of an object should be accessible. For example, Person class:

        class Person
          attr_accessor :name, :age, :job
        end
      

This code creates @name, @age, @job variables for each Person object, and creates "getter" and "setter" methods (or "reader" and "writer") - one to get a variable value of objects, and another one to set it. It means that with attr_accessor we can do next:

        john = Person.new
        john.name = "John"
      

Without attr_accessor code, our app would crash:

        john = Person.new
        john.name = "John"
        # => NoMethodError: undefined method `name='
      

This name= is a setter method. Its logic is simple, it accepts some value, and sets it to our @name variable.

attr_accessor :some_property is just a shorthand for a

        attr_reader :some_property
        attr_writer :some_property
      

As you may see from methods name, the reader will allow you to read a property, writer will allow you to write a property, and accessor allows you to do both: read and write to a property.

Technically, attr_reader and attr_writer just create a simple method that will write into a given instance variable, for example:

        # attr_reader :prop is a shorthand for:
        def prop
          @prop
        end

        # attr_writer :prop is a shorthand for:
        def prop=(value)
          @prop = value
        end
      

Obviously, attr_accessor will add these two methods automatically.

Let's try to create some instance of our Person class, and read its name and age:

        # check how getter methods work:
        p = Person.new
        p.age # Ruby will return us nil
        p.name # Ruby will return us nil
      

Cool, now we can also remove our learned_languages method because it can be replaced with attr_reader:

        class Person
          attr_accessor :name, :job, :age
          attr_reader :learned_languages
          # rest of the code
          # ...
        end
      

By default, Ruby creates an object with all values equal nil. However, the average user will expect a new person to have age predefined to 0 (people usually born with age == 0), or 18, if we want to create adult people. So, how could we preset some values of a new object?

When we call Person.new Ruby is looking for a initialize method in a class, and if it exists, it runs it. Otherwise, it just creates an empty object. Let's use this initialize method to preset person's age to 18:

        class Person
          attr_accessor :name, :job, :age
          attr_reader :learned_languages

          def initialize
            @age = 18
          end
        end
      

Now, each time we create a new person, initialize method will be called, and @age variable will be set to 18. Let's try it:

        p = Person.new
        p.age
        # => 18
      

It worked! But there is one additional hidden feature of initialize method - we can pass it some data, that will be used to create new objects, for example:

        p = Person.new({ name: "John", age: 21, job: "Manager" })
      

This way of creating an object and setting its values is shorter, and may save us some time in the future. To make it work, we just need to inform Person class that it will accept some data - in our case it is a Hash:

        def initialize(opts={})
          @name = opts[:name]
          @job = opts[:job]
          @age = opts[:age] || 18
          @learned_languages = []
        end
      

Here we wrote that initialize method accepts a single variable opts . In cases when it has not been specified, it will be automatically set to an empty hash: opts={} . Then we just assign each hash value to a corresponding instance variable. Age is a little bit different; you can read it as "age should be equal to age from opts hash, and if this value does not exist just set it to 18". Remember when we were talking about logical expressions? opts[:age] || 18 will return a value that is not false or nil. If opts[:age] exists, Ruby will choose it. Otherwise, value 18 will be chosen, since it is the only one here that is not false or nil .

Let's try it now:

        p = Person.new({ name: "John", age: 21, job: "Manager" })
        p.name
        # => "John"

        p.age
        # => 21

        p.job
        # => "Manager"
      

The only output of our current program is just some values of a person object. Let's make a new method for Person objects that would print some kind of resume for them. When we would call john.resume, it should show following data:

First, let's remove "Programming Language Learned" message when learning a new language. Then, we will create a new method that would only print data in a given earlier format:

        def resume
          p "Hi, my name is #{@name}, I'm #{@age}."
          return if !@job
          if @learned_languages.empty?
            p "I'm a #{job}"
          else
            p "I'm a #{@job}, and I know next languages:"
            @learned_languages.each do |lang|
              p "  - " + lang
            end
          end
        end
      

There is only one string that may not be familiar at this point:

        return if !@job
      

You can read it as "stop if a job is not set". As you remember, character ! is logical "not". !true equals false, and !false equals true. In our case it is !@job, which returns true if the value is empty, and false otherwise.

Here is the source code:

        class Person
          attr_accessor :name, :job, :age
          attr_reader :learned_languages

          def initialize(opts={})
            @name = opts[:name]
            @job = opts[:job]
            @age = opts[:age] || 18
            @learned_languages = []
          end

          def self.default_planet
            "Earth"
          end

          def learn(language)
            if @learned_languages.include? language
              p "This language has been learned earlier."
            else
              @learned_languages << language
            end
          end

          def resume
            p "Hi, my name is #{@name}, I'm #{@age}."
            return if !@job
            if @learned_languages.empty?
              p "I'm a #{job}"
            else
              p "I'm a #{@job}, and I know next languages:"
              @learned_languages.each do |lang|
                p "  - " + lang
              end
            end
          end
        end

        class Dog
        end

        class Cat
        end

        john = Person.new name: "John", age: 21, job: "Software Developer"
        john.learn "Ruby"
        john.learn "Python"
        john.learn "JavaScript"

        john.resume
      

Book Index | Next