← Writing

Ruby: Class & Module

在 Viiisit Ruby Ruby 的物件導向程式語言基礎概念! 有提到 Ruby 是一個物件導向的程式語言,在 Ruby 中,所有的東西幾乎都是物件,為什麼說是幾乎? 因為像之前有一篇 Viiisit Ruby on Rails Scope 前情提要 Block、Proc 和 Lambda!...

interviewruby

Viiisit Ruby - Ruby 的物件導向程式語言基礎概念! 有提到 Ruby 是一個物件導向的程式語言,在 Ruby 中,所有的東西幾乎都是物件,為什麼說是幾乎? 因為像之前有一篇 Viiisit Ruby on Rails - Scope 前情提要 Block、Proc 和 Lambda! 裡頭有說明 Block 就不是物件!

那麼在物件導向程式裡的特色就是物件,什麼是物件?

物件(Object)是什麼?

物件(object) = 狀態(state) + 行為(behavior)

在現實生活中,你我他,看得到、摸得到的都可通稱為之物件(Object)。 物件會有狀態跟行為,例如我有「黑色頭髮」、「黃色皮膚」等狀態,也有「吃飯」、「睡覺」等行為。

而,我們要如何在物件導向程式語言定義這些狀態與行為?

就要來開啟認識類別(Class)之路了!

類別(Class)是什麼?

類別是一個程式碼的模板,用來建立具體的物件(實例)。

可以透過關鍵字 class 來定義類別!

初始化

初始化通常是透過一個特殊的方法稱為 initialize 來執行的, 這個方法會在建立該類別的實例時被呼叫。 當一個新的實例被創建時,initialize 方法用於設置實例的初始狀態。

class Person
  def initialize(name, age)
    @name = name
    @age = age
  end
end

# 建立一個 Person 實例
person1 = Person.new("Alice", 30)
person2 = Person.new("Bob", 25)

initialize 方法接受兩個參數 nameage, 並使用這些參數來設置 Person 實例的初始狀態。 當我們建立 Person 實例時,我們傳遞相應的參數給 new 方法,這將觸發 initialize 方法。

一旦實例被初始化,我們可以使用實體變數(instance variables)來存儲和訪問實例的狀態。 在 Ruby 中,實體變數以 @ 符號開頭,並且可以在整個類別的方法中訪問。

如何使用實體變數?

Ruby 的實體變數沒辦法直接從外部取用, 因為實例變數的封裝性以及 Ruby 並沒有「屬性」(property/attribute)這樣的設計, 因此只允許通過方法來取用實體變數!

定義兩個方法 get_nameget_age, 允許我們從 Person 實例中訪問 @name@age 實體變數。

class Person
  def initialize(name, age)
    @name = name
    @age = age
  end

  def get_name
    @name
  end

  def get_age
    @age
  end
end

person = Person.new("Alice", 30)

puts "Name: #{person.get_name}"  # 輸出:Name: Alice
puts "Age: #{person.get_age}"    # 輸出:Age: 30

attr_ 系列讓你輕鬆建立屬性!

前面提及每次要這樣取用或設定實體變數的值都要建立方法,好像有點麻煩?

別擔心!Ruby 的 attr_ 系列方法可以簡化實例變數的訪問和設置, 這些方法可以自動創建 getter 和 setter 方法,以允許對實例變數的訪問!

  • attr_reader :用於建立 getter 方法,允許訪問實例變數的值。

    class Person
      attr_reader :name
    
      def initialize(name)
        @name = name
      end
    end
    
    person = Person.new("Alice")
    puts person.name # 可以訪問實例變數 @name
    
  • attr_writer :用於建立 setter 方法,允許設置實例變數的值。

    class Person
      attr_writer :name
    
      def initialize(name)
        @name = name
      end
    end
    
    person = Person.new("Alice")
    person.name = "Bob" # 可以設置實例變數 @name 的值
    
  • attr_accessor :用於同時建立 getter 和 setter 方法。

    class Person
      attr_accessor :name
    
      def initialize(name)
        @name = name
      end
    end
    
    person = Person.new("Alice")
    puts person.name    # 可以訪問實例變數 @name
    person.name = "Bob" # 可以設置實例變數 @name 的值
    

實體方法與類別方法

  • 實體方法(Instance Methods)

    • 實體方法是與類別的實例(物件)關聯的方法,可以訪問和操作實例變數,並且通常用於對特定實例執行操作或訪問其狀態。
    • 實體方法通常定義在類別中,並且通過類別的實例來呼叫。這些方法在操作實例時具有存取實例變數的能力,因為有關聯到特定的實例。
    • 在定義實體方法時,你可以使用 def 關鍵字,而不需要特殊的修飾詞。
    class Person
      def initialize(name)
        @name = name
      end
    
      def greet
        "Hello, my name is #{@name}."
      end
    end
    
    person = Person.new("Alice")
    puts person.greet # 輸出:Hello, my name is Alice.
    
  • 類別方法(Class Methods)

    • 類別方法是與類別本身關聯的方法,而不是與實例相關聯。通常用於執行與整個類別相關的操作,而不是與單個實例相關。
    • 類別方法在類別級別上定義,通常使用 self. 關鍵字,或者類別名稱來定義。
    • 類別方法可以在不需要實例的情況下呼叫,因為它們與類別本身關聯,而不是與實例變數關聯。
    class MathUtility
      def self.square(x)
        x * x
      end
    
      def self.cube(x)
        x * x * x
      end
    end
    
    puts MathUtility.square(2) # 輸出:4
    puts MathUtility.cube(3)   # 輸出:27
    

方法的存取控制

類別的方法存取限制常見的主要有三種:publicprotected 以及 private

  • public 方法:在類別內部和外部都可訪問,通常用於定義對外部世界可見的介面,以便外部程式碼可以直接訪問這些方法。

  • private 方法:只能在定義他們的類別內部訪問,通常用於實現類別的內部邏輯,以避免外部程式碼干擾或誤用這些方法。

  • protected 方法:類似於私有方法,但可以在同一個類別的不同實例之間訪問,通常用於實現特定的類別內部協作,例如當不同實例需要互相訪問彼此的某些方法時。

class Animal
  def initialize(name)
    @name = name
  end

  # 公開方法
  def speak
    puts "#{@name} 發出聲音。"
  end

  # 保護方法
  protected

  def sleep
    puts "#{@name} 正在休息。"
  end

  # 私有方法
  private

  def eat
    puts "#{@name} 正在進食。"
  end
end

class Dog < Animal
  def wag_tail
    # 子類別可以訪問保護方法
    sleep
    puts "#{@name} 開心地搖尾巴。"
  end

  def feed
    # 子類別可以訪問私有方法
    eat
    puts "#{@name} 吃得很開心。"
  end
end

animal = Animal.new("動物")
animal.speak # 動物 發出聲音。

dog = Dog.new("狗")
dog.speak     # 狗 發出聲音。
dog.wag_tail  # 狗 開心地搖尾巴。
dog.feed      # 狗 吃得很開心。

定義一個 Animal 類別, 有一個公開方法 speak、一個保護方法 sleep 和一個私有方法 eat。 然後,建立一個 Dog 類別, 他是繼承自 Animal 類別的子類別,並且可以訪問父類別中的保護和私有方法。

公開方法 speak 可以從任何地方訪問, 保護方法 sleep 可以在 Dog 類別中訪問,而私有方法 eat 也可以在 Dog 類別中訪問。

模組(Module)是什麼?

在 Ruby 中,模組(Modules)在某種程度上類似於類別(Classes), 他們都可以包含方法,就像類別一樣。

然而,模組無法被實例化。也就是說,無法從模組建立物件。 因此,模組不像類別一樣具有 new 方法。 除此之外,模組也無法像類別有繼承的效果,也無法建立子類別。

那,模組實際上的作用到底是什麼?

模組的作用

透過 module 關鍵字來定義模組!

module Greetable
  
  A = "Hi!"

  def greet
    "Hello!"
  end
end

class Person
  include  Greetable
end

puts Greetable::A # Hi!

amy = Person.new
puts amy.greet # Hello!

模組(module) 可以透過雙冒號運算符 (::) 來訪問模組中常數的值

puts Greetable::A # Hi!

模組(module)使用 def 關鍵字定義一個方法,即 def method_name,則該方法為實例方法。 我們無法直接使用點運算符(.)呼叫實例方法,因為無法建立模組(module)的實例。

為了呼叫模組(module)內定義的實例方法, 必須將該模組(module)包含(include)在一個類別(class)中, 然後使用類別(class)實例來訪問該方法。

amy = Person.new
puts amy.greet # Hello!

使用 include 關鍵字在類別(class)中包含模組(module), 這種情況下,模組(module)的作用類似於一個 namespace

class Person
  include  Greetable
end

amy = Person.new
puts amy.greet # Hello!

補充:Namespace 用意

Namespace 是一個在程式設計中常用的概念,用於區分和組織變數、函數、類別和其他命名實體, 以避免命名衝突,確保代碼的結構化和可讀性。

當在類別(class)中使用 include 關鍵字將一個模組(module)包含在其中時, 這個模組(module)的內容變成該類別(class)的一部分, 可以在該類別(class)中訪問模組(module)中的方法和變數,同時避免了命名衝突。 這就是命名空間的一個實際應用場景。

include vs extend

在 Ruby 中,includeextend 是用於引入模組(module)的兩種不同方法。

  • include

    • 使用 include 關鍵字時,模組(module)的方法被引入到類別(class)的實例中。
    • 引入模組(module)的方法可以直接在類別(class)的實例上調用, 並且這些方法對該實例的每個對象都有效。
  • extend

    • 使用 extend 關鍵字時,模組(module)的方法被引入為類別方法,而不是實例方法。
    • 引入模組(module)的方法只能在類別(class)本身上調用, 而無法在該類別(class)的實例上調用。

簡而言之,include 用於向類別(class)加上的實例方法, 而 extend 用於向類別(class)本身添加方法,這些方法在類別(class)級別上可用。

透過簡單的舉例,來看看 includeextend 的實際應用吧!

  • 使用 include
module Greeting
  def say_hello
    puts "Hello!"
  end
end

class Person
  include Greeting
  attr_reader :name

  def initialize(name)
    @name = name
  end
end

person = Person.new("Alice")
puts person.say_hello # 可以在實例上調用 Greeting 模組中的方法 # Hello!

include GreetingGreeting 模組的方法加到 Person 類的實例中。 因此,person 實例可以調用 say_hello 方法。

  • 使用 extend
module MathFunctions
  def square(num)
    num * num
  end
end

class Calculator
  extend MathFunctions
end

result = Calculator.square(5) # 可以在類別級別調用 MathFunctions 模組中的方法
puts result # 25

extend MathFunctionsMathFunctions 模組的方法 加上 Calculator 類別的類別方法。 因此,我們可以直接在類別調用 Calculator.square 方法,而不需要建立類別的實例。

Brief Summary

  • 物件(Object)

    • Ruby 是一個物件導向的程式語言,其中幾乎所有東西都是物件,但 Block 不是物件。
    • 物件(Object)在 Ruby 中被定義為擁有狀態(state)和行為(behavior)的實體, 就像生活中的事物一樣。
  • 類別(Class)

    • 類別(Class)是一個建立具體的物件(實例)模板。 可以透過 class 關鍵字來定義類別,並使用 initialize 方法來初始化物件的狀態。
    • 在 Ruby 中,有不同的方法來訪問和設置實例變數: attr_readerattr_writerattr_accessor
    • 方法可分為實體方法和類別方法,實體方法是與類別的實例相關聯的, 而類別方法是與類別本身相關聯的。
    • 方法具有不同的存取控制,包括 publicprotectedprivate, 以控制它們的可見性和存取權限。
  • 模組(Module)

    • 模組(Module)類似於類別,但不能被實例化, 並且通常用於將方法和常數組織在一個命名空間(namespace)中,以避免命名衝突。
    • include 用於將模組的方法添加到類別的實例中, 而 extend 用於將模組的方法添加為類別方法。

參考資料

Ruby | Module 類別(Class)與模組(Module)