Une introduction à la programmation orientée objet avec Ruby

En tant qu'étudiant en informatique, je passe beaucoup de temps à apprendre et à jouer avec de nouvelles langues. Chaque nouvelle langue a quelque chose d'unique à offrir. Cela dit, la plupart des débutants commencent leur parcours de programmation avec des langages procéduraux comme C ou avec des langages orientés objet comme JavaScript et C ++.

Par conséquent, il est logique de passer par les bases de la programmation orientée objet afin de pouvoir comprendre les concepts et les appliquer aux langages que vous apprenez facilement. Nous utiliserons le langage de programmation Ruby comme exemple.

Vous vous demandez peut-être pourquoi Ruby? Parce qu'il est «conçu pour rendre les programmeurs heureux» et aussi parce que presque tout dans Ruby est un objet.

Se faire une idée du paradigme orienté objet (POO)

En POO, nous identifions les «choses» que notre programme gère. En tant qu'humains, nous considérons les choses comme des objets avec des attributs et des comportements, et nous interagissons avec les choses en fonction de ces attributs et comportements. Une chose peut être une voiture, un livre, etc. De telles choses deviennent des classes (les plans des objets), et nous créons des objets à partir de ces classes.

Chaque instance (objet) contient des variables d'instance qui sont l'état de l'objet (attributs). Les comportements des objets sont représentés par des méthodes.

Prenons l'exemple d'une voiture. Une voiture est une chose qui en ferait une classe . Un type spécifique de voiture, disons BMW, est un objet de la classe Car . Les attributs / propriétés d'une BMW tels que la couleur et le numéro de modèle peuvent être stockés dans des variables d'instance. Et si vous souhaitez effectuer une opération sur l'objet, comme conduire, alors «conduire» décrit un comportement qui est défini comme une méthode .

Une leçon de syntaxe rapide

  • Pour terminer une ligne dans un programme Ruby, un point-virgule (;) est facultatif (mais n'est généralement pas utilisé)
  • L'indentation à 2 espaces pour chaque niveau imbriqué est encouragée (non obligatoire, comme c'est le cas en Python)
  • Aucune accolade {}n'est utilisée et le mot-clé end est utilisé pour marquer la fin d'un bloc de contrôle de flux
  • Pour commenter, nous utilisons le #symbole

La façon dont les objets sont créés dans Ruby consiste à appeler une nouvelle méthode sur une classe, comme dans l'exemple ci-dessous:

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

Pour comprendre ce qui se passe dans le code ci-dessus:

  • Nous avons une classe nommée Caravec deux méthodes, initializeet get_info.
  • Les variables d'instance dans Ruby commencent par @(Par exemple @name). La partie intéressante est que les variables ne sont pas initialement déclarées. Ils prennent naissance lors de leur première utilisation et sont ensuite disponibles pour toutes les méthodes d'instance de la classe.
  • L'appel de la newméthode provoque l' appel de la méthode initialize. initializeest une méthode spéciale qui est utilisée comme constructeur.

Accéder aux données

Les variables d'instance sont privées et ne sont pas accessibles depuis l'extérieur de la classe. Pour y accéder, nous devons créer des méthodes. Les méthodes d'instance ont un accès public par défaut. Nous pouvons limiter l'accès à ces méthodes d'instance comme nous le verrons plus loin dans cet article.

Afin d'obtenir et de modifier les données, nous avons besoin des méthodes «getter» et «setter», respectivement. Regardons ces méthodes en prenant le même exemple d'une voiture.

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

Dans Ruby, le «getter» et le «setter» sont définis avec le même nom que la variable d'instance que nous traitons.

Dans l'exemple ci-dessus, quand nous disons my_car.color, il appelle en fait la colorméthode qui à son tour renvoie le nom de la couleur.

Remarque: faites attention à la façon dont Ruby permet à un espace entre le coloret est égal à signer lors de l'utilisation du setter, même si le nom de la méthode estcolor=

L'écriture de ces méthodes getter / setter nous permet d'avoir plus de contrôle. Mais la plupart du temps, obtenir la valeur existante et définir une nouvelle valeur est simple. Donc, il devrait y avoir un moyen plus simple au lieu de définir les méthodes getter / setter.

Le moyen le plus simple

En utilisant le attr_*formulaire à la place, nous pouvons obtenir la valeur existante et définir une nouvelle valeur.

  • attr_accessor: pour getter et setter à la fois
  • attr_reader: pour getter uniquement
  • attr_writer: pour setter uniquement

Regardons cette forme en prenant le même exemple d'une voiture.

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 cette façon, nous pouvons ignorer complètement les définitions getter / setter.

Parler des meilleures pratiques

Dans l'exemple ci-dessus, nous n'avons pas initialisé les valeurs des variables d'instance @nameet @color, ce qui n'est pas une bonne pratique. De plus, comme les variables d'instance sont définies sur nil, l'objet car1n'a aucun sens. C'est toujours une bonne pratique de définir des variables d'instance à l'aide d'un constructeur comme dans l'exemple ci-dessous.

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éthodes de classe et variables de classe

Les méthodes de classe sont donc appelées sur une classe, pas sur une instance d'une classe. Celles-ci sont similaires aux méthodes statiques en Java.

Remarque: en selfdehors de la définition de méthode, se réfère à l'objet de classe. Les variables de classe commencent par@@

Maintenant, il existe en fait trois façons de définir des méthodes de classe dans Ruby:

À l'intérieur de la définition de classe

  1. Utilisation du mot-clé self avec le nom de la méthode:
class MathFunctions def self.two_times(num) num * 2 endend
# No instance createdputs MathFunctions.two_times(10) # => 20

2. Utilisation <<; soi

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

En dehors de la définition de classe

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.