Eine Einführung in die objektorientierte Programmierung mit Ruby

Als Informatikstudent verbringe ich viel Zeit damit, neue Sprachen zu lernen und damit zu spielen. Jede neue Sprache hat etwas Einzigartiges zu bieten. Allerdings beginnen die meisten Anfänger ihre Programmierreise entweder mit prozeduralen Sprachen wie C oder mit objektorientierten Sprachen wie JavaScript und C ++.

Daher ist es sinnvoll, die Grundlagen der objektorientierten Programmierung durchzugehen, damit Sie die Konzepte verstehen und auf die Sprachen anwenden können, die Sie leicht lernen. Wir werden die Programmiersprache Ruby als Beispiel verwenden.

Sie fragen sich vielleicht, warum Ruby? Weil es „Programmierer glücklich machen soll“ und auch weil fast alles in Ruby ein Objekt ist.

Ein Gefühl für das objektorientierte Paradigma (OOP) bekommen

In OOP identifizieren wir die „Dinge“, mit denen unser Programm umgeht. Als Menschen betrachten wir Dinge als Objekte mit Attributen und Verhaltensweisen und interagieren mit Dingen, die auf diesen Attributen und Verhaltensweisen basieren. Eine Sache kann ein Auto sein, ein Buch und so weiter. Solche Dinge werden zu Klassen (den Blaupausen von Objekten), und wir erstellen Objekte aus diesen Klassen.

Jede Instanz (Objekt) enthält Instanzvariablen, die den Status des Objekts (Attribute) darstellen. Objektverhalten wird durch Methoden dargestellt.

Nehmen wir das Beispiel eines Autos. Ein Auto ist eine Sache, die es zu einer Klasse machen würde . Ein bestimmter Autotyp, beispielsweise BMW, ist ein Objekt der Klasse Auto . Die Attribute / Eigenschaften eines BMW wie Farbe und Modellnummer können in Instanzvariablen gespeichert werden. Wenn Sie eine Operation des Objekts ausführen möchten, z. B. Fahren, beschreibt „Fahren“ ein Verhalten, das als Methode definiert ist .

Eine kurze Syntaxstunde

  • Um eine Zeile in einem Ruby-Programm zu beenden, ist ein Semikolon (;) optional (wird jedoch im Allgemeinen nicht verwendet).
  • Einzug von 2 Leerzeichen für jede verschachtelte Ebene wird empfohlen (nicht erforderlich, wie in Python).
  • Es {}werden keine geschweiften Klammern verwendet, und das Schlüsselwort end wird verwendet, um das Ende eines Flusssteuerungsblocks zu markieren
  • Zum Kommentieren verwenden wir das #Symbol

Die Art und Weise, wie Objekte in Ruby erstellt werden, besteht darin, eine neue Methode für eine Klasse aufzurufen , wie im folgenden Beispiel:

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

Um zu verstehen, was im obigen Code vor sich geht:

  • Wir haben eine Klasse Carmit zwei Methoden initializeund get_info.
  • Instanzvariablen in Ruby beginnen mit @(zum Beispiel @name). Der interessante Teil ist, dass die Variablen zunächst nicht deklariert werden. Sie entstehen bei der ersten Verwendung und stehen danach allen Instanzmethoden der Klasse zur Verfügung.
  • Durch Aufrufen der newMethode wird die initializeMethode aufgerufen. initializeist eine spezielle Methode, die als Konstruktor verwendet wird.

Zugriff auf Daten

Instanzvariablen sind privat und können nicht von außerhalb der Klasse aufgerufen werden. Um auf sie zugreifen zu können, müssen wir Methoden erstellen. Instanzmethoden haben standardmäßig öffentlichen Zugriff. Wir können den Zugriff auf diese Instanzmethoden beschränken, wie wir später in diesem Artikel sehen werden.

Um die Daten abzurufen und zu ändern, benötigen wir die Methoden "getter" bzw. "setter". Schauen wir uns diese Methoden am selben Beispiel eines Autos an.

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

In Ruby werden der "Getter" und der "Setter" mit demselben Namen definiert wie die Instanzvariable, mit der wir uns befassen.

Wenn wir im obigen Beispiel sagen my_car.color, ruft es tatsächlich die colorMethode auf, die wiederum den Namen der Farbe zurückgibt.

Hinweis: Achten Sie darauf, wie Ruby bei Verwendung des Setters ein Leerzeichen zwischen und gleich zulässt , obwohl der Methodenname lautetcolorcolor=

Durch das Schreiben dieser Getter / Setter-Methoden haben wir mehr Kontrolle. In den meisten Fällen ist es jedoch einfach, den vorhandenen Wert abzurufen und einen neuen Wert festzulegen. Es sollte also einen einfacheren Weg geben, als tatsächlich Getter / Setter-Methoden zu definieren.

Der einfachere Weg

Wenn Sie stattdessen das attr_*Formular verwenden, können Sie den vorhandenen Wert abrufen und einen neuen Wert festlegen.

  • attr_accessor: für Getter und Setter beide
  • attr_reader: nur für Getter
  • attr_writer: nur für Setter

Schauen wir uns dieses Formular am selben Beispiel eines Autos an.

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

Auf diese Weise können wir die Getter / Setter-Definitionen insgesamt überspringen.

Über Best Practices sprechen

Im obigen Beispiel haben wir die Werte für die Variablen @nameund @colorinstance nicht initialisiert , was keine gute Vorgehensweise ist. Da die Instanzvariablen auf Null gesetzt sind, macht das Objekt car1keinen Sinn. Es wird immer empfohlen, Instanzvariablen mithilfe eines Konstruktors wie im folgenden Beispiel festzulegen.

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

Klassenmethoden und Klassenvariablen

Klassenmethoden werden also für eine Klasse aufgerufen, nicht für eine Instanz einer Klasse. Diese ähneln statischen Methoden in Java.

Hinweis: selfBezieht sich außerhalb der Methodendefinition auf das Klassenobjekt. Klassenvariablen beginnen mit@@

Nun gibt es drei Möglichkeiten, Klassenmethoden in Ruby zu definieren:

Innerhalb der Klassendefinition

  1. Verwenden des Schlüsselworts self mit dem Namen der Methode:
class MathFunctions def self.two_times(num) num * 2 endend
# No instance createdputs MathFunctions.two_times(10) # => 20

2. Verwenden von <<; selbst

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

Außerhalb der Klassendefinition

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.