Introducción a la programación orientada a objetos con Ruby

Como estudiante de informática, paso mucho tiempo aprendiendo y jugando con nuevos idiomas. Cada nuevo idioma tiene algo único que ofrecer. Dicho esto, la mayoría de los principiantes comienzan su viaje de programación con lenguajes procedimentales como C o con lenguajes orientados a objetos como JavaScript y C ++.

Por lo tanto, tiene sentido repasar los conceptos básicos de la programación orientada a objetos para que pueda comprender los conceptos y aplicarlos a los lenguajes que aprende fácilmente. Usaremos el lenguaje de programación Ruby como ejemplo.

Quizás te preguntes, ¿por qué Ruby? Porque está “diseñado para hacer felices a los programadores” y también porque casi todo en Ruby es un objeto.

Tener una idea del paradigma orientado a objetos (OOP)

En OOP, identificamos las "cosas" que maneja nuestro programa. Como seres humanos, pensamos en las cosas como objetos con atributos y comportamientos, e interactuamos con cosas en función de estos atributos y comportamientos. Una cosa puede ser un automóvil, un libro, etc. Tales cosas se convierten en clases (los planos de los objetos) y creamos objetos a partir de estas clases.

Cada instancia (objeto) contiene variables de instancia que son el estado del objeto (atributos). Los comportamientos de los objetos están representados por métodos.

Tomemos el ejemplo de un automóvil. Un automóvil es algo que lo convertiría en una clase . Un tipo específico de automóvil, digamos BMW, es un objeto de la clase Automóvil . Los atributos / propiedades de un BMW, como el color y el número de modelo, se pueden almacenar en variables de instancia. Y si desea realizar una operación del objeto, como conducir, entonces "conducir" describe un comportamiento que se define como un método .

Una lección de sintaxis rápida

  • Para terminar una línea en un programa Ruby, un punto y coma (;) es opcional (pero generalmente no se usa)
  • Se recomienda la sangría de 2 espacios para cada nivel anidado (no es obligatorio, como en Python)
  • No {}se utilizan llaves y la palabra clave end se utiliza para marcar el final de un bloque de control de flujo
  • Para comentar usamos el #símbolo

La forma en que se crean los objetos en Ruby es llamando a un nuevo método en una clase, como en el siguiente ejemplo:

class Car def initialize(name, color) @name = name @color = color end
 def get_info "Name: #{@name}, and Color: #{@color}" endend
my_car = Car.new("Fiat", "Red")puts my_car.get_info

Para comprender lo que sucede en el código anterior:

  • Tenemos una clase nombrada Carcon dos métodos initializey get_info.
  • Las variables de instancia en Ruby comienzan con @(por ejemplo @name). Lo interesante es que las variables no se declaran inicialmente. Surgen cuando se utilizan por primera vez y, después, están disponibles para todos los métodos de instancia de la clase.
  • Llamar al newmétodo hace initializeque se invoque. initializees un método especial que se utiliza como constructor.

Acceso a datos

Las variables de instancia son privadas y no se puede acceder a ellas desde fuera de la clase. Para acceder a ellos, necesitamos crear métodos. Los métodos de instancia tienen acceso público de forma predeterminada. Podemos limitar el acceso a estos métodos de instancia como veremos más adelante en este artículo.

Para obtener y modificar los datos, necesitamos los métodos "getter" y "setter", respectivamente. Veamos estos métodos tomando el mismo ejemplo de un automóvil.

class Car def initialize(name, color) # "Constructor" @name = name @color = color end
 def color @color end
 def color= (new_color) @color = new_color endend
my_car = Car.new("Fiat", "Red")puts my_car.color # Red
my_car.color = "White"puts my_car.color # White

En Ruby, el "getter" y el "setter" se definen con el mismo nombre que la variable de instancia con la que estamos tratando.

En el ejemplo anterior, cuando decimos my_car.color, en realidad llama al colormétodo que a su vez devuelve el nombre del color.

Nota: Preste atención a cómo Ruby permite un espacio entre colory es igual para firmar mientras usa el establecedor, aunque el nombre del métodocolor=

Escribir estos métodos getter / setter nos permite tener más control. Pero la mayoría de las veces, obtener el valor existente y establecer un nuevo valor es simple. Por lo tanto, debería haber una manera más fácil en lugar de definir métodos getter / setter.

La forma mas facil

Al usar el attr_*formulario en su lugar, podemos obtener el valor existente y establecer un nuevo valor.

  • attr_accessor: para getter y setter tanto
  • attr_reader: solo para getter
  • attr_writer: solo para colocador

Veamos esta forma tomando el mismo ejemplo de un automóvil.

class Car attr_accessor :name, :colorend
car1 = Car.newputs car1.name # => nil
car1.name = "Suzuki"car1.color = "Gray"puts car1.color # => Gray
car1.name = "Fiat"puts car1.name # => Fiat

De esta manera podemos omitir las definiciones de getter / setter por completo.

Hablar de las mejores prácticas

En el ejemplo anterior, no inicializamos los valores para las variables de instancia @namey @color, lo cual no es una buena práctica. Además, como las variables de instancia se establecen en nil, el objeto car1no tiene ningún sentido. Siempre es una buena práctica establecer variables de instancia usando un constructor como en el siguiente ejemplo.

class Car attr_accessor :name, :color def initialize(name, color) @name = name @color = color endend
car1 = Car.new("Suzuki", "Gray")puts car1.color # => Gray
car1.name = "Fiat"puts car1.name # => Fiat

Métodos de clase y variables de clase

Entonces, los métodos de clase se invocan en una clase, no en una instancia de una clase. Estos son similares a los métodos estáticos en Java.

Nota: selffuera de la definición del método se refiere al objeto de clase. Las variables de clase comienzan con@@

Ahora, en realidad, hay tres formas de definir métodos de clase en Ruby:

Dentro de la definición de clase

  1. Usando la palabra clave self con el nombre del método:
class MathFunctions def self.two_times(num) num * 2 endend
# No instance createdputs MathFunctions.two_times(10) # => 20

2. Usando <<; yo

class MathFunctions class << self def two_times(num) num * 2 end endend
# No instance createdputs MathFunctions.two_times(10) # => 20

Fuera de la definición de clase

3. Using class name with the method name

class MathFunctionsend
def MathFunctions.two_times(num) num * 2end
# No instance createdputs MathFunctions.two_times(10) # => 20

Class Inheritance

In Ruby, every class implicitly inherits from the Object class. Let’s look at an example.

class Car def to_s "Car" end
 def speed "Top speed 100" endend
class SuperCar < Car def speed # Override "Top speed 200" endend
car = Car.newfast_car = SuperCar.new
puts "#{car}1 #{car.speed}" # => Car1 Top speed 100puts "#{fast_car}2 #{fast_car.speed}" # => Car2 Top speed 200

In the above example, the SuperCar class overrides the speed method which is inherited from the Car class. The symbol &lt; denotes inheritance.

Note: Ruby doesn’t support multiple inheritance, and so mix-ins are used instead. We will discuss them later in this article.

Modules in Ruby

A Ruby module is an important part of the Ruby programming language. It’s a major object-oriented feature of the language and supports multiple inheritance indirectly.

A module is a container for classes, methods, constants, or even other modules. Like a class, a module cannot be instantiated, but serves two main purposes:

  • Namespace
  • Mix-in

Modules as Namespace

A lot of languages like Java have the idea of the package structure, just to avoid collision between two classes. Let’s look into an example to understand how it works.

module Patterns class Match attr_accessor :matched endend
module Sports class Match attr_accessor :score endend
match1 = Patterns::Match.newmatch1.matched = "true"
match2 = Sports::Match.newmatch2.score = 210

In the example above, as we have two classes named Match, we can differentiate between them and prevent collision by simply encapsulating them into different modules.

Modules as Mix-in

In the object-oriented paradigm, we have the concept of Interfaces. Mix-in provides a way to share code between multiple classes. Not only that, we can also include the built-in modules like Enumerable and make our task much easier. Let’s see an example.

module PrintName attr_accessor :name def print_it puts "Name: #{@name}" endend
class Person include PrintNameend
class Organization include PrintNameend
person = Person.newperson.name = "Nishant"puts person.print_it # => Name: Nishant
organization = Organization.neworganization.name = "freeCodeCamp"puts organization.print_it # => Name: freeCodeCamp 

Mix-ins are extremely powerful, as we only write the code once and can then include them anywhere as required.

Scope in Ruby

We will see how scope works for:

  • variables
  • constants
  • blocks

Scope of variables

Methods and classes define a new scope for variables, and outer scope variables are not carried over to the inner scope. Let’s see what this means.

name = "Nishant"
class MyClass def my_fun name = "John" puts name # => John end
puts name # => Nishant

The outer name variable and the inner name variable are not the same. The outer name variable doesn’t get carried over to the inner scope. That means if you try to print it in the inner scope without again defining it, an exception would be thrown — no such variable exists

Scope of constants

An inner scope can see constants defined in the outer scope and can also override the outer constants. But it’s important to remember that even after overriding the constant value in the inner scope, the value in the outer scope remains unchanged. Let’s see it in action.

module MyModule PI = 3.14 class MyClass def value_of_pi puts PI # => 3.14 PI = "3.144444" puts PI # => 3.144444 end end puts PI # => 3.14end

Scope of blocks

Blocks inherit the outer scope. Let’s understand it using a fantastic example I found on the internet.

class BankAccount attr_accessor :id, :amount def initialize(id, amount) @id = id @amount = amount endend
acct1 = BankAccount.new(213, 300)acct2 = BankAccount.new(22, 100)acct3 = BankAccount.new(222, 500)
accts = [acct1, acct2, acct3]
total_sum = 0accts.each do |eachAcct| total_sum = total_sum + eachAcct.amountend
puts total_sum # => 900

In the above example, if we use a method to calculate the total_sum, the total_sum variable would be a totally different variable inside the method. That’s why sometimes using blocks can save us a lot of time.

Having said that, a variable created inside the block is only available to the block.

Access Control

When designing a class, it is important to think about how much of it you’ll be exposing to the world. This is known as Encapsulation, and typically means hiding the internal representation of the object.

There are three levels of access control in Ruby:

  • Public - no access control is enforced. Anybody can call these methods.
  • Protected - can be invoked by objects of the defining classes or its sub classes.
  • Private - cannot be invoked except with an explicit receiver.

Let’s see an example of Encapsulation in action:

class Car def initialize(speed, fuel_eco) @rating = speed * comfort end
 def rating @rating endend
puts Car.new(100, 5).rating # => 500

Now, as the details of how the rating is calculated are kept inside the class, we can change it at any point in time without any other change. Also, we cannot set the rating from outside.

Talking about the ways to specify access control, there are two of them:

  1. Specifying public, protected, or private and everything until the next access control keyword will have that access control level.
  2. Define the method regularly, and then specify public, private, and protected access levels and list the comma(,) separated methods under those levels using method symbols.

Example of the first way:

class MyClass private def func1 "private" end protected def func2 "protected" end public def func3 "Public" endend

Example of the second way:

class MyClass def func1 "private" end def func2 "protected" end def func3 "Public" end private :func1 protected :func2 public :func3end

Note: The public and private access controls are used the most.

Conclusion

These are the very basics of Object Oriented Programming in Ruby. Now, knowing these concepts you can go deeper and learn them by building cool stuff.

Don’t forget to clap and follow if you enjoyed! Keep up with me here.