Ruby on Rails 網站程式設計基礎班

第3.5課:深入物件導向

Ruby 風格指南

何時一定會用到 return?

# Early return 的狀況
def add_two(arr)
  # 確保每一個 hash 的 age 屬性都有值,若沒有,就迴傳錯誤訊息,不執行下面的程式碼
  return "missing age value..." if arr.any? { |x| x[:age] == nil }
  
  arr.map { |x| x[:age] = x[:age] + 2}
  arr
end

hash1 = { :age => 19}

hash2 = { :age => 21}

# hash3 = { :age => 31}
hash3 = { :age => nil}

arr = [hash1,hash2,hash3]

puts add_two(arr)

Self 的使用

class Person
  attr_accessor :name, :age

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

  def greet
    puts "Hello, my name is #{name}"
  end

  def get_name
    name # 若已使用 attr_accessor,可省略 self
  end

  def set_name1=(result)
    name = result # Ruby中若偵測到name=,會建立一個新的區域變數name,而不是呼叫 name 方法
  end
end

bob = Person.new("bob", 17)
puts bob.get_name
# => bob

bob.set_name1 = "Bob"
puts bob.get_name
# => bob 沒有改變

Self

class Person
  attr_accessor :name, :age

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

  def greet
    puts "Hello, my name is #{name}"
  end

  def get_name
    name
  end

  def set_name2=(result)
    # 使用 setter method 時,為了區別區域變數,這種情況下必須使用self明確指定
    self.name = result
  end
end

bob = Person.new("bob", 17)
puts bob.get_name
# => bob

bob.set_name2 = "Bob"
puts bob.get_name
# => Bob

Self

class Person
  attr_accessor :name, :age

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

  def greet
    puts "Hello, my name is #{name}"
  end

  def get_name
    name
  end

  def set_name3=(name)
    # 當然,我也可以直接設定實例變數
    @name = name
  end
end

bob = Person.new("bob", 17)
puts bob.get_name
# => bob

bob.set_name3 = "Bob"
puts bob.get_name
# => Bob

@ vs method

# 把 instance variable 包在方法裡有許多好處
# 假設今天我的帳務系統有一個應收帳務的 class

class AccountReceivable # 應收帳務
  def initialize(title, amount)
    @title = title
    @amount = amount
  end
   
  def sales_tax_amount #計算營業稅金額
    @amount * 0.05
  end

  def invoice_amount #計算加上稅的帳面金額
    @amount * 1.05
  end
end

@ vs method

# 若今天收到新的需求:以後系統的營業稅金額與帳面金額
# 必須轉成用美金計價(輸入依然是台幣)...
# 這時就需要修改多處地方...
class AccountReceivable # 應收帳務
  def initialize(title, amount)
    @title = title
    @amount = amount
  end
   
  def sales_tax_amount #計算營業稅金額
    #@amount * 0.05
    @amount / 32 * 0.05
  end

  def invoice_amount #計算加上稅的帳面金額
    #@amount * 1.05
    @amount / 32 * 1.05
  end
end

@ vs method

# 若我一開始就把 @amount 包在 accessor method 裡面...

class AccountReceivable # 應收帳務
  attr_accessor :title, :amount
  
  def initialize(title, amount)
    @title = title
    @amount = amount
  end
  # 把 amount 的邏輯集中到程式一處
  # 之後若要修改,我只需要修改 amount 的 getter method 一處即可
  def amount 
    @amonut / 32
  end
   
  def sales_tax_amount
    amount * 0.05
  end

  def invoice_amount
    amount * 1.05
  end
end

@ vs method

class AccountReceivable # 應收帳務
  attr_accessor :title, :amount
  def initialize(title, amount)
    @title = title
    @amount = amount
  end

  def amount
    @amount / 32
  end
   
  def sales_tax_amount #計算營業稅金額
    amount * 0.05
  end

  def invoice_amount #計算加上稅的帳面金額
    amount * 1.05
  end
end
# 若是把 @amount 包在方法裡,就可以在 class 以外的地方被使用

class Rebate # 現金回饋
  attr_accessor :account_receivable, :rate

  def initialize(account_receivable, rate)
    @account_receivable = account_receivable
    @rate = rate
  end

  def calculate_rebate 
    # 使用應收帳款的 amount 方法計算現金回饋金額
    account_receivable.amount * rate
  end
end
receivable1 = AccountReceivable.new("應收帳款1", 10000)
puts receivable1.amount
#=> 312

# 計算回扣金額
rebate = Rebate.new(receivable1, 0.02)
puts rebate.calculate_rebate
#=> 6.24

# 計算實際收入的金額
puts receivable1.invoice_amount - rebate.calculate_rebate
#=> 321.36

@ vs method

結論:盡量把實例變數包在 method 裡面

 

1. 較高的可見度 (可在 class 外被使用)

2. 比較靈活的寫法 (更能適應改變)

 

 


 

用 hash 傳參數

# 若今天一個方法我需要傳進許多參數...
class Person
  # 宣告方法的介面時,要寫的 code 就會變得又臭又長...
  def initialize(name, age, hair_color, favorite_book, occupation)
    @name = name
    @age = age
    @hair_color = hair_color
    @favorite_book = favorite_book
    @occupation = occupation
  end
end

# 更麻煩的是,在使用上還必須依照順序傳入參數...
bob = Person.new("bob", 17, "black", "1984", "Salesman")

用 hash 傳參數

class Person
  attr_accessor :name, :age, :occupation
  # 可以把方法改成用 hash 傳遞參數
  def initialize(options ={})
    @name = options[:name] # 到hash內取值
    @age = options[:age]
    @hair_color = options[:hair_color]
    @favorite_book = options[:favorite_book]
    @occupation = options[:occupation]
  end
end

hash = { 
  hair_color: "black", 
  name: "bob", 
  age: "1984", 
  occupation: "Salesman", 
  favorite_book: "1984" 
}

# 使用時就丟 hash...
bob = Person.new(hash)

Constants (常數)

# constant 就是有些時候會遇上一些在程式裡不應改變的變數
# 像是圓周率、法定成年的年紀 etc.
class Person  
  # 宣告 constant,全部大寫,命名用尖叫蛇底式大寫 SCREAMING_SNAKE_CASE
  LEGAL_AGE_TO_DRIVE = 18

  attr_accessor :name, :age

  def initialize(name, age)
    @name = name
    @age = age
  end
  # 看這個人是否已到可以考駕照的年齡
  def can_drive?
    if age >= LEGAL_AGE_TO_DRIVE 
      puts "yes #{name} can drive"
    else 
      puts "no #{name} can't drive"
    end
  end
end

bob = Person.new("Bob", 17)
bob.can_drive?
#=> no Bob can't drive

Module (模組)

# 可把 module 想像成工具箱,本身並不是 class
# 沒有實例、不可 new,只能被 include 到其他 class 裡面使用
# 一個 class 使用模組在 ruby 界是叫做 mixin module

module Knowledge
  def program
    puts "I know how to program"
  end
end

class Person  
  attr_accessor :name, :age

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

class Engineer < Person
  include Knowledge
end

bob = Engineer.new("Bob", 17)
bob.program
# => I know how to program

Module (模組)

Ruby 使用 Module來解決多重繼承的問題。不同類別但是要擁有相同的方法,就可以改放在 Module 裡面,然後 include 它即可

Inheritance (繼承)

# 我們希望在建立新的 class 時,新的 class 也包含了舊的 class 的方法和特性
# 這時就可以使用 inheritance(繼承),讓我不用重新寫很多東西
class Person  
  attr_accessor :name, :age

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

  def greet
    puts "hello, my name is #{name}"
  end
end
# 使用 < 來代表繼承
class Engineer < Person
end

class Salesman < Person
end

bob = Engineer.new("Bob", 17)
sam = Salesman.new("Sam", 32)
bob.greet
# => hello, my name is Bob
sam.greet
# => hello, my name is Sam
# Engineer 和 Salesman 繼承了 Person 的 greet 方法

Inheritance vs Module

1. You can only subclass from one class. But you can mix in as many modules as you'd like.
   你只能為一個 class 建立子類別,但卻可以 mix 很多 module

2. 如果今天你是需要 "是某個..." 的關係,就用 inheritance,如過需要 "擁有某項行為" 就用 module
   Ex. 蝙蝠是屬於一種哺乳動物,就讓 Bat 繼承 Mammal
       蝙蝠有飛行的能力,但並非所有哺乳動物都會飛,所以讓 Bat include 擁有 fly 這個 method 的 module

3. Module 不能使用 new,他只能被其他 class 使用  

Method Override (覆寫)

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

  def greet
    puts "hello, my name is #{name}"
  end
end
# 如果我希望在改變 Engineer 的 greet
class Engineer < Person
  # override (覆寫) 原本 Person 的 greet
  def greet
    puts "I am an Engineer, my name is #{name}"
  end
end
# 創立一個叫 German 的子類別
class German < Person
  # override (覆寫) 原本 Person 的 greet
  def greet
    puts "Hallo, mein name ist #{name}"
  end
end
bob = Engineer.new("Bob", 17)
martin = German.new("Martin", 20)
bob.greet
martin.greet
#=> I am an Engineer, my name is Bob
#=> Hallo, mein name ist Martin

Super

class Person  
  attr_accessor :name, :age

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

  def greet
    puts "hello, my name is #{name}"
  end
end

class Engineer < Person
  attr_accessor :lang

  def initialize(name, age, lang) # 若我今天想改寫 Engineer 的建構式,可以傳入三個參數
    @lang = lang
    super(name, age)
    # super 會呼叫 parent class 裡面同樣名字的 method
    # 注意 super 只能在 method 裡面使用
  end
end

bob = Engineer.new("Bob", 17, "Ruby")
puts bob.name
puts bob.age
puts bob.lang
#=> Bob
#=> 17
#=> Ruby

Method Lookup 

module Knowledge
  def program
    puts "I know how to program"
  end
end

class Person  
  attr_accessor :name, :age

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

class Engineer < Person
  include Knowledge
end

# 尋找 Engineer 的祖先們
puts Engineer.ancestors

# Engineer
# Knowledge
# Person
# Object
# Kernel
# BasicObject

# 一旦 object 呼叫了一個它的 Class 裡面沒有的 method
# 他就會依序找他所有的祖先,若找到了有這個 method 的祖先,就使用這個祖先類別的 method

Public Method

# method 有三種不同的權限
# public 就是任何屬於這個 class 的物件都能使用

class Person 
  attr_accessor :name, :age

  def initialize(name, age)
    @name = name
    @age = age
  end
  # greet 就是 public method
  def greet
    puts "hello, my name is #{name}"
  end
end

bob = Person.new("Bob", 30)
bob.greet

#可以在程式的任何地方被使用

Private Method

# private 是只有在 class 內部才可以存取
# 適用於有些時候,你希望一些 method 不會被程式的其他地方使用到(像是金流、身分證字號 etc.)
class Person  
  attr_accessor :name, :age
  def initialize(name, age)
    @name = name
    @age = age
  end

  def identity
    #只能在 class 裡面被其他 instance method 呼叫,前面不能加 self
    puts "secretly, #{secret_method}"
  end

  private # 先寫 private 關鍵字
  
  def secret_method
    "I am Iron Man!"
  end
end

bob = Person.new("bob", 17)
bob.secret_method
#=> private method `secret_method' called
#=> for #<Person:0x007ff182033498 @name="bob", @age=17> (NoMethodError)
# private method 不可直接被物件呼叫
bob.identity
# secretly, I am Iron Man!

Private Method

# 今天我們的帳務系統有新需求:若是尊榮級的合作對象,就加 1% 的現金回饋

class Rebate # 現金回饋
  attr_accessor :account_receivable, :rate, :company

  def initialize(account_receivable, rate, company)
    @account_receivable = account_receivable
    @rate = rate
    @company = company
  end

  def rebate_amount
    calculate_rebate
  end

  private

  def calculate_rebate 
    # 但是我們知道現金回饋的算法會經常改變
    # 寫成 private method,再由一個 public method 呼叫,會是比較
    # 能夠適應改變的寫法
    if company.partnership == "premium"
      account_receivable.amount * (rate + 0.01)
    else
      account_receivable.amount * rate
    end  
  end
end
# 代表公司(合作廠商)的 class
class Company
  attr_accessor :name, :partnership
  
  def initialize(name, partnership)
    @name = name
    @partnership = partnership
  end
end

Private Method

結論:盡量把預期會經常改變的程式碼寫成 private methods,不常改變 / 穩定的方法寫成 public methods,避免修改 method 的時候,所有依靠該 method 的程式碼也要一起修改

1. 

OOP Design

*越容易維護或擴充功能的程式碼(越能適應改變的程式碼),就是越高明的設計

Ruby 程式碼風格

雖然你已經看了 Ruby 風格指南

但還是經常發生不知道自己寫的 Code 風格是否正確

Rubocop Gem

Ruby 的程式碼風格分析器

 

Rubocop Gem

Rubocop Gem

$ gem install rubocop

接下來就可以在終端機裡直接輸入 Rubocop 即可:

$ rubocop 你的ruby程式碼檔案路徑

安裝 Rubocop gem

SQLite Browser

如果你是用 Mac:

http://sqlitebrowser.org/

 

如果你是用 Ubuntu:

https://apps.ubuntu.com/cat/applications/sqlitebrowser/

 

 

Relational Database (關聯式資料庫)

關聯式資料庫教學:

 

 

Relational Database (關聯式資料庫)

看資料庫是如何透過關聯把多個資料表結合起來:

http://sqlbolt.com/lesson/select_queries_with_joins

 

 

Homework


安裝環境

用物件導向重構剪刀石頭布遊戲

https://github.com/yuyueugene84/lesson4/blob/master/RPS_OOP.rb



Extra

HTML & CSS 練習

https://www.codecademy.com/learn/web

http://www.w3schools.com/

http://www.w3school.com.cn/​

 

Ruby 語言練習

https://www.codecademy.com/learn/ruby

 

SQL 語言練習

https://www.codecademy.com/learn/learn-sql

http://sqlbolt.com/

Made with Slides.com