🔁 ทวนก่อนเริ่ม — คำจากบทก่อนที่บทนี้ใช้
- object กับการเรียก method ด้วยจุด (บท 91) —
"hi".upcaseคือสั่งให้ object"hi"ทำงานupcase #{}(บท 91) — แทรกค่าลงใน string ที่ใช้ฟันหนูคู่"…"- symbol (บท 92) — ชื่อที่ขึ้นต้นด้วย
:เช่น:titleใช้เป็น "ชื่อเรียก" ของอะไรสักอย่าง defและ return อัตโนมัติ (บท 93) — บรรทัดสุดท้ายของ method คือค่าที่ส่งกลับnil(บท 94) — แปลว่า "ไม่มีค่า"
🧰 เวอร์ชันที่ใช้ตรวจ: Ruby 4.0.9 · Rails 8.1.4 · Go 1.26.5 (สไลด์ใช้ Ruby 4.0.6 · Rails 8.1.3 — ต่างกันแค่รุ่นย่อย เนื้อหาบทนี้ใช้ได้เหมือนกัน) · ผลรันทุกกล่องก็อปมาจากเครื่องจริง
1 · ปัญหา: บทความหลายชิ้น หน้าตาต้องเหมือนกัน
ในบท 92 เราเก็บบทความหนึ่งชิ้นไว้ใน Hash ทำนองนี้ (จำลองบทความ ยังไม่ใช่ Rails):
post = { title: "ต้มยำกุ้ง", body: "วิธีทำ..." }
ใช้ได้ แต่ไม่มีใครบังคับว่าบทความทุกชิ้นต้องมีช่อง title ·
ถ้าเพื่อนพิมพ์ titel: ผิด ก็ได้บทความหน้าตาเพี้ยนมาหนึ่งชิ้น ·
และงานที่บทความ "ทำได้" (เช่น ทำคำทักทาย) ก็ต้องเขียนกระจายไว้ที่อื่น
class คือทางแก้: เราเขียน "แบบ" ของบทความไว้ที่เดียว ว่าบทความหนึ่งชิ้นมีข้อมูลอะไร และทำอะไรได้บ้าง · จากนั้นจะสร้างบทความกี่ชิ้นก็ได้ ทุกชิ้นออกมาจากแบบเดียวกัน · ของแต่ละชิ้นที่สร้างออกมาเรียกว่า object
class เหมือนแม่พิมพ์ขนมไข่ — แม่พิมพ์มีอันเดียว กำหนดรูปร่างไว้แล้ว
object คือขนมแต่ละชิ้นที่หยอดแป้งออกมาจากแม่พิมพ์ · ทุกชิ้นรูปร่างเหมือนกัน
แต่แต่ละชิ้นมีของของตัวเอง ชิ้นหนึ่งโรยงา อีกชิ้นโรยลูกเกด
Post คือแม่พิมพ์ · บทความ "ต้มยำกุ้ง" กับบทความ "ผัดกะเพรา" คือขนมสองชิ้น
ที่ไม่เหมือนกัน: ขนมอบเสร็จแล้วแก้ไม่ได้ แต่ object แก้ข้อมูลข้างในทีหลังได้เรื่อย ๆ · และแม่พิมพ์ขนมทำได้แค่กำหนดรูปร่าง ส่วน class ยังเขียนไว้ด้วยว่า object "ทำอะไรได้บ้าง" (method)
ถ้าเทียบกับ Go — type Post struct { ... } ก็คือแม่พิมพ์เหมือนกัน
และ Post{Title: "Hi"} คือขนมหนึ่งชิ้น · แนวคิดเดียวกันเป๊ะ ต่างแค่วิธีเขียน
- class
- แบบที่บอกว่าข้อมูลชนิดหนึ่งมีอะไรและทำอะไรได้ · ชื่อขึ้นต้นด้วยตัวใหญ่เสมอ เช่น
Post· ถาม AI ได้ว่า "class Post ใน Ruby เทียบกับ struct ของ Go ตรงไหนบ้าง" - object / instance
- ของหนึ่งชิ้นที่สร้างจาก class · สองคำนี้ใช้แทนกันได้ ("post เป็น instance ของ Post") · ถาม senior ได้ว่า "ตัวแปร post ตรงนี้เป็น instance ของ class ไหนครับ"
2 · เขียน class Post ตัวแรก — เทียบ Go ทีละบรรทัด
มาเขียนแม่พิมพ์บทความกัน · ซ้ายคือ Go ที่คุณเคยเขียน · ขวาคือ Ruby ที่ทำเรื่องเดียวกัน (ทั้งสองฝั่งรันจริงแล้ว):
Go
type Post struct {
Title string
}
func NewPost(title string) Post {
return Post{Title: title}
}
post := NewPost("Hi")
Ruby
# post.rb
class Post
def initialize(title)
@title = title
end
end
post = Post.new("Hi")
p post
ทวนฝั่ง Go ก่อนครึ่งประโยค: Go ไม่มี method สร้าง object แบบพิเศษ (เขียน Post{Title: "Hi"} ตรง ๆ ก็ได้) · NewPost เป็นแค่ func ธรรมดาที่เราตั้งชื่อขึ้นต้นด้วย New ตามธรรมเนียม ·
ส่วน Ruby มีที่ทางให้ตายตัว คือ method ชื่อ initialize เท่านั้น
อ่านฝั่ง Ruby ทีละบรรทัด:
| Ruby | แปลว่า | ใน Go คือ |
|---|---|---|
class Post … end |
เริ่มเขียนแม่พิมพ์ชื่อ Post · ทุกอย่างจนถึง end ตัวสุดท้ายเป็นของ class นี้ |
type Post struct |
def initialize(title) |
method ชื่อพิเศษ รันอัตโนมัติทุกครั้งที่สร้างบทความใหม่ · รับชื่อเรื่องเข้ามาหนึ่งตัว | func NewPost(title string) |
@title = title |
เก็บชื่อเรื่องไว้ ติดกับบทความชิ้นนี้ · @ ข้างหน้าแปลว่า "ของ object ตัวนี้" ไม่ใช่ตัวแปรชั่วคราว |
Post{Title: title} |
Post.new("Hi") |
สั่งแม่พิมพ์ให้หยอดขนมหนึ่งชิ้น · new จะไปเรียก initialize("Hi") ให้เอง เราไม่ได้เรียก initialize ตรง ๆ |
NewPost("Hi") |
p post |
พิมพ์ object ออกมาดูแบบละเอียด (สำหรับ debug) | fmt.Printf("%+v", post) |
ผลรันจริง:
$ ruby post.rb
#<Post:0x0000000121f0beb0 @title="Hi">
อ่านว่า "นี่คือ object ของ class Post · ข้างในมี @title เป็น "Hi"" ·
ตัวเลข 0x0000… คือที่อยู่ใน memory ของคุณจะไม่ตรงกับนี้ และเปลี่ยนทุกครั้งที่รัน เป็นเรื่องปกติ
ทีนี้ลองหยอดขนมสองชิ้นจากแม่พิมพ์เดียว · แต่ละชิ้นมี @title เป็นของตัวเอง ไม่ปนกัน:
- initialize / constructor
- method ที่
Post.new(...)เรียกให้อัตโนมัติตอนสร้าง object · ภาษาอื่นเรียกว่า constructor · ถาม senior ได้ว่า "ค่าเริ่มต้นของบทความควรตั้งใน initialize เลยไหมครับ" - instance variable (
@title) - ตัวแปรที่ขึ้นต้นด้วย
@· ติดอยู่กับ object แต่ละตัว อยู่ได้ตลอดอายุของ object · พิมพ์บอก AI ได้ว่า "อธิบาย instance variable ใน Ruby เทียบกับ field ใน struct ของ Go"
3 · post.title — เอาข้อมูลออกมาดู
ใน Go ถ้าชื่อ field ขึ้นต้นตัวใหญ่ (Title) คนข้างนอกก็อ่าน post.Title ได้เลย ·
Ruby ไม่ได้ทำแบบนั้น — ลองทายดู:
# post2.rb — class เดิมจากหัวข้อ 2 ทุกตัวอักษร
class Post
def initialize(title)
@title = title
end
end
post = Post.new("Hi")
puts post.title
ใน object มี @title = "Hi" อยู่แล้ว · บรรทัดสุดท้ายจะพิมพ์อะไร?
$ ruby post2.rb
post2.rb:9:in '<main>': undefined method 'title' for an instance of Post (NoMethodError)
puts post.title
^^^^^^
พัง · error แปลว่า "object ของ Post ไม่มี method ชื่อ title" ·
ใน Ruby post.title ไม่ใช่การหยิบ field แต่เป็นการเรียก method ชื่อ title ·
ข้อมูล @title อยู่ข้างใน object จริง แต่คนข้างนอกแตะตรง ๆ ไม่ได้ ต้องมี method ไว้ให้เรียก
ทางแก้ตรงไปตรงมา คือเขียน method ที่ส่ง @title ออกมาเอง:
class Post
def initialize(title)
@title = title
end
def title # อ่านค่า — คืน @title ออกไป (return อัตโนมัติ)
@title
end
def title=(value) # แก้ค่า — ชื่อ method คือ "title=" มีเครื่องหมาย = ติดท้ายจริง ๆ
@title = value
end
end
พอมีสอง method นี้ post.title ก็อ่านได้ · ส่วน post.title = "ใหม่"
ที่ดูเหมือนการเอาค่าไปใส่ จริง ๆ คือการเรียก method ชื่อ title= แล้วส่ง "ใหม่" เข้าไป ·
Ruby ยอมให้เว้นวรรคระหว่าง title กับ = ได้ เพื่อให้อ่านเหมือนการตั้งค่าตัวแปร
แต่ต้องเขียน 6 บรรทัดต่อข้อมูลหนึ่งช่อง ซึ่งน่าเบื่อมาก · Ruby เลยมีทางลัด บรรทัดเดียว:
# post3.rb
class Post
attr_accessor :title # สร้าง def title กับ def title= ให้เอง
def initialize(title)
@title = title
end
end
post = Post.new("Hi")
puts post.title
post.title = "Hello Ruby"
puts post.title
$ ruby post3.rb
Hi
Hello Ruby
attr_accessor— คำสั่งที่เขียนไว้ในตัว class แล้วมันเขียน method สองตัวให้แทนเรา · ตัวมันเองก็เป็น method ที่เรียกแบบไม่ใส่วงเล็บ (เขียนเต็ม ๆ คือattr_accessor(:title)— เรื่องวงเล็บที่ละได้อยู่บท 97):title— symbol จากบท 92 · ที่นี่ใช้บอกว่า "ข้อมูลช่องชื่อ title" → ได้ methodtitleกับtitle=ที่อ่าน/เขียน@title- มีหลายช่องก็เขียนต่อกันได้:
attr_accessor :title, :body
ถ้าอยากให้คนนอก อ่านได้อย่างเดียว ให้ใช้ attr_reader :title แทน (สร้างแค่ title) ·
แล้วถ้าใครสั่งแก้ค่า จะพังแบบนี้:
# reader.rb
class Post
attr_reader :title # อ่านได้อย่างเดียว
def initialize(title)
@title = title
end
end
post = Post.new("Hi")
puts post.title # อ่าน — ทำได้
post.title = "Hello Ruby" # แก้ — พังตรงนี้
$ ruby reader.rb
Hi
reader.rb:12:in '<main>': undefined method 'title=' for an instance of Post (NoMethodError)
บรรทัด Hi คือการอ่านที่ทำได้ · ส่วน error บอกว่าหา method ชื่อ 'title=' ไม่เจอ
เพราะ attr_reader สร้างให้แค่ title ตัวเดียว
object เหมือนกล่องข้าวที่ปิดฝาไว้ · @title คือกับข้าวที่อยู่ในกล่อง คนข้างนอกล้วงเองไม่ได้
attr_reader = เปลี่ยนเป็นฝาใส มองเห็นว่ามีอะไร แต่เปลี่ยนกับข้าวไม่ได้
attr_accessor = ฝาที่เปิดได้ ทั้งดูและเปลี่ยนกับข้าวได้
ที่ไม่เหมือนกัน: กล่องข้าวจริงปิดฝาแล้ว ใครแอบเปิดก็ได้ ·
ส่วนใน Ruby ไม่มี method ก็คือเรียกไม่ได้ ได้ NoMethodError ทันที ·
และฝาใสมีฝาเดียวต่อกล่อง แต่ attr_reader เลือกได้ทีละช่องข้อมูล (title ฝาใส · body ฝาเปิดได้)
- getter / setter
- method ที่อ่านค่า (getter เช่น
title) และ method ที่ตั้งค่า (setter เช่นtitle=) · พิมพ์บอก AI ได้ว่า "ทำไม error บอกว่าไม่มี method title= ผมแค่จะตั้งค่าเอง" attr_accessor/attr_reader/attr_writer- ทางลัดสร้าง getter+setter / getter อย่างเดียว / setter อย่างเดียว · ถาม senior ได้ว่า "title ควรเป็น attr_reader ไหมครับ ไม่อยากให้ข้างนอกแก้ได้"
4 · ให้บทความ "ทำอะไรได้" — method ใน class
นอกจากเก็บข้อมูล class ยังเขียนได้ว่า object ทำอะไรได้บ้าง · ตัวอย่างจากสไลด์หน้า 28 คือ method greeting
ที่คืนคำทักทาย · ฝั่ง Go คือ method ที่มี receiver (p Post)
(ทวน: (p Post) หน้าชื่อ func แปลว่า "func นี้เป็นของ Post · ข้างในเรียกบทความชิ้นที่ถูกเรียกว่า p"):
Go
func (p Post) Greeting() string {
return "Hello, " + p.Title
}
Ruby (เขียนไว้ใน class Post)
def greeting
"Hello, #{@title}"
end
- Ruby ไม่ต้องเขียน receiver แบบ
(p Post)· method ที่อยู่ในclass Postเป็นของ Post อัตโนมัติ @titleตรงนี้ทำหน้าที่เหมือนp.Titleของ Go — "ชื่อเรื่องของบทความชิ้นที่ถูกเรียก"- ไม่มี
returnเพราะบรรทัดสุดท้ายคือค่าที่คืน (บท 93)
เติมลงใน class Post แล้วเรียก puts Post.new("Hi").greeting ได้ Hello, Hi (ฝั่ง Go ก็ได้ Hello, Hi เหมือนกัน)
- instance method
- method ที่เขียนใน class แล้วเรียกผ่าน object แต่ละชิ้น เช่น
post.greeting· ผลขึ้นกับข้อมูลของชิ้นนั้น · ถาม senior ได้ว่า "logic ทำคำทักทายนี้ควรเป็น instance method ของ Post ไหมครับ"
⚠️ พิมพ์ชื่อ @ตัวแปรผิด Ruby ไม่เตือน — ถ้าเผลอเขียน "Hello, #{@titel}"
จะไม่มี error ไม่มี warning (แม้รันด้วย ruby -w ซึ่งสั่งให้ Ruby เตือนเรื่องน่าสงสัย ก็ยังเงียบ) เพราะ @ตัวแปรที่ยังไม่เคยตั้งค่าจะได้ nil เฉย ๆ:
$ ruby typo.rb # greeting เขียน @titel แทน @title · บรรทัดท้าย p post.greeting
"Hello, "
Go จะฟ้องตั้งแต่ compile ว่าไม่มี field Titel · ใน Ruby ถ้าผลออกมาว่างแบบแปลก ๆ ให้สงสัยตัวสะกดของ @ตัวแปรก่อน
5 · สืบทอดด้วย < — class ลูกที่ต่อยอดจาก class แม่
แอปบล็อกของเราอยากมีบทความชนิดพิเศษ: สูตรอาหาร ที่มีทุกอย่างเหมือนบทความ (ชื่อเรื่อง · คำทักทาย)
แต่มี "เวลาที่ใช้ทำ" เพิ่มมาหนึ่งช่อง · แทนที่จะก็อป Post มาทั้งก้อน Ruby ให้เขียนว่า
"Recipe เป็นลูกของ Post" ด้วยเครื่องหมาย <
เหมือนสูตรขนมที่ต่อยอดจากสูตรแม่ — สูตรแม่คือขนมไข่ธรรมดา · สูตรลูกเขียนว่า "ทำตามสูตรแม่ทุกขั้น แล้วเพิ่มไส้สังขยา" · ไม่ต้องคัดลอกขั้นตอนของแม่ลงมาใหม่ แค่เขียนส่วนที่เพิ่ม
ที่ไม่เหมือนกัน: สูตรลูกในครัวจริงจดไว้แล้วก็จบ ถ้าแม่แก้สูตรทีหลัง สูตรลูกที่จดไว้ไม่เปลี่ยนตาม · แต่ใน Ruby ถ้าแก้ class แม่ class ลูกเปลี่ยนตามทันที เพราะลูกไม่ได้ก็อปโค้ดของแม่ไว้ แค่ชี้ไปหาแม่
# inherit.rb — ข้างบนคือ class Post ที่มี attr_accessor :title, initialize และ greeting (หัวข้อ 3–4)
class Recipe < Post
attr_accessor :minutes
def initialize(title, minutes)
super(title) # ให้ initialize ของ Post ตั้ง @title ให้ก่อน
@minutes = minutes # แล้วค่อยเก็บส่วนที่ Recipe เพิ่มเอง
end
end
r = Recipe.new("ต้มยำกุ้ง", 15)
puts r.greeting
puts r.title
puts r.minutes
p r.is_a?(Post)
$ ruby inherit.rb
Hello, ต้มยำกุ้ง
ต้มยำกุ้ง
15
true
class Recipe < Post— อ่านว่า "Recipe เป็นลูกของ Post" ·<ตรงนี้ไม่ใช่น้อยกว่าsuper(title)— "เรียก method ชื่อเดียวกันของแม่" · ในที่นี้คือinitializeของ Post · ใส่วงเล็บและส่งเฉพาะของที่แม่รับเสมอ (ถ้าลืม ดู error ในภารกิจท้ายบท)greetingกับtitleไม่ได้เขียนใน Recipe เลย แต่เรียกได้ เพราะได้มาจากแม่r.is_a?(Post)— ถามว่า "r เป็น Post ด้วยไหม" · ได้trueเพราะสูตรอาหารก็เป็นบทความชนิดหนึ่ง
Go ไม่มีการสืบทอดแบบนี้ (สไลด์หน้า 28 ก็เขียนไว้) · Go ใช้การฝัง struct ไว้ข้างใน (embedding) แทน ซึ่งจะเห็นในหัวข้อ 7
- inheritance (การสืบทอด)
- ให้ class หนึ่งได้ทุกอย่างของอีก class ไปใช้ · class แม่เรียก superclass · class ลูกเรียก subclass · ถาม senior ได้ว่า "Recipe ควรเป็น subclass ของ Post ไหมครับ หรือแยกเป็นคนละ class ดีกว่า"
super- เรียก method ชื่อเดียวกันของแม่ · ใช้บ่อยใน
initializeของลูก · พิมพ์บอก AI ได้ว่า "ใน initialize ของ class ลูก ควรเรียก super แบบไหน ถึงไม่ส่ง argument เกิน"
6 · สะพานไป Rails — Post ในโปรเจกต์จริงมาจากไหน
ถึงตรงนี้ตอบคำถามต้นบทได้แล้ว · ในโปรเจกต์ Rails จะมีไฟล์ app/models/post.rb ที่หน้าตาแบบนี้
(Rails สร้างไฟล์นี้ให้ด้วยคำสั่ง bin/rails generate — เจอในบท 99):
# app/models/post.rb — ทั้งไฟล์มีแค่นี้จริง ๆ
class Post < ApplicationRecord
end
อ่านออกแล้วใช่ไหม — Post คือ class ธรรมดา ที่เป็นลูกของ ApplicationRecord ·
แต่แปลกตรงที่ว่างเปล่า ไม่มี attr_accessor ไม่มี initialize ·
แล้ว post.title มาจากไหน?
คำตอบ: Rails เปิดดูตาราง posts ในฐานข้อมูลตอนรัน แล้วสร้าง method ตามชื่อคอลัมน์ให้เอง
(title · title= · body · …)
สังเกตชื่อสองแบบ: Post (ตัวใหญ่ เอกพจน์) คือแม่พิมพ์ของบทความหนึ่งชิ้น ·
posts (ตัวเล็ก พหูพจน์) คือตารางที่เก็บบทความทุกชิ้น (ตาราง = ที่เก็บข้อมูลเป็นแถว ๆ ในฐานข้อมูล แบบที่เคยใช้กับ GORM) ·
Rails จับคู่สองชื่อนี้ให้เองตามกฎตั้งชื่อ — ละเอียดในบท 99
ผมสร้างตาราง posts ที่มีคอลัมน์ title กับ body ไว้ก่อน
(ด้วย bin/rails generate model Post title:string body:text แล้ว bin/rails db:prepare — เรื่องของ Day 2 ตอนนี้แค่ดู) ·
แล้วรันสคริปต์นี้ในโปรเจกต์:
# try.rb — วางไว้ในโฟลเดอร์โปรเจกต์ wongnok_blog
p Post.column_names
post = Post.new(title: "Hi", body: "My first post")
puts post.title
puts post.body
p post.respond_to?(:title)
p post.respond_to?(:author)
$ bin/rails runner try.rb
["id", "title", "body", "created_at", "updated_at"]
Hi
My first post
true
false
bin/rails runner try.rb— รันไฟล์ Ruby โดยโหลดทั้งโปรเจกต์ Rails ให้ก่อน (เลยรู้จักPostโดยไม่ต้อง import — เรื่องนี้ละเอียดในบท 99)Post.column_names— ขอรายชื่อคอลัมน์ของตารางposts· เห็นtitleกับbodyที่เราสร้าง และอีกสามช่องที่ Rails เติมให้Post.new(title: "Hi", body: "…")— ส่งค่าแบบระบุชื่อช่อง คล้าย keyword argument ในบท 93 (เบื้องหลังเป็น hash — บท 97 อธิบาย) · ไม่ใช่Post.new("Hi")แบบ class ของเราเองrespond_to?(:title)— ถามว่า "object นี้มี method ชื่อนี้ไหม" ·titleมี (true) เพราะเป็นคอลัมน์ ·authorไม่มี (false) เพราะตารางไม่มีคอลัมน์นี้
model Post คือห้องเก็บวัตถุดิบ + คนดูแลสต็อก · ฐานข้อมูลทั้งก้อนคือตู้เก็บแฟ้ม ·
ตาราง posts คือลิ้นชักบทความ · บทความหนึ่งแถวคือแฟ้มหนึ่งแผ่น
คนดูแลสต็อกของ Rails เดินไปเปิดลิ้นชักดูเองว่าแฟ้มมีช่องอะไรบ้าง (title · body) แล้วก็พร้อมหยิบช่องนั้นให้ทันที
ส่วน Go ที่เคยเรียน = เปิดร้านเองตั้งแต่ศูนย์ ต้องจดรายการช่องของแฟ้มเองทุกช่องใน struct ·
Rails = ร้านแฟรนไชส์ที่ได้ครัวครบชุด ขอแค่ตั้งชื่อลิ้นชักตามที่แฟรนไชส์กำหนด (Post ↔ posts)
ที่ไม่เหมือนกัน: คนดูแลสต็อกจริงเห็นลิ้นชักแล้วจำได้ตลอด ·
ส่วน Rails ไม่ได้เขียนชื่อช่องลงไฟล์ post.rb ให้เราเห็นเลย ·
เปิดไฟล์ดูก็ว่างเปล่า ต้องไปดูที่ตาราง (หรือ Post.column_names) ถึงรู้ว่ามีช่องอะไร
เทียบกับ Go + GORM ที่เคยเรียนในบท 27 ให้เห็นชัด ๆ (ตัดมาเฉพาะส่วน struct):
Go + GORM — เขียนทุกช่องเอง
type Post struct {
ID uint
Title string
Body string
}
ลืมเขียน Body = ใช้ post.Body ไม่ได้ แม้ตารางจะมีคอลัมน์นี้
Rails — ไม่เขียนสักช่อง
class Post < ApplicationRecord
end
ตารางมีคอลัมน์ไหน post.ชื่อคอลัมน์ ก็ใช้ได้เลย
แล้ว ApplicationRecord เป็นใคร? เป็น class ที่ rails new สร้างไว้ให้ และเป็นลูกของ ActiveRecord::Base
ซึ่งเป็นตัวที่มีความสามารถคุยกับฐานข้อมูลทั้งหมด (:: แปลว่า "ที่อยู่ในกล่องชื่อ ActiveRecord"):
$ bin/rails runner 'p Post.superclass; p ApplicationRecord.superclass'
ApplicationRecord(abstract)
ActiveRecord::Base
.superclass คือถามว่า "แม่ของ class นี้คือใคร" ·
คำว่า (abstract) ท้าย ApplicationRecord แปลว่า class นี้มีไว้เป็นแม่ให้ model อื่นสืบทอดเท่านั้น ไม่ได้ผูกกับตารางของตัวเอง
นี่คือการสืบทอดจากหัวข้อ 5 ล้วน ๆ: Post → แม่คือ ApplicationRecord → แม่คือ ActiveRecord::Base ·
การบันทึกลงฐานข้อมูล การค้นหา (เช่น Post.where(...) = หาบทความตามเงื่อนไข) ทั้งหมดได้มาจากแม่ — Day 2 จะสอนเรื่องนี้เต็ม ๆ
- model
- class ที่แทนข้อมูลหนึ่งชนิดในฐานข้อมูล เช่น
Postแทนตารางposts· อยู่ในapp/models/· ถาม senior ได้ว่า "model Post นี้ผูกกับตารางไหนครับ มีคอลัมน์อะไรบ้าง" - attribute (มาจาก column)
- ช่องข้อมูลของ model ที่ Rails สร้าง method ให้จากชื่อคอลัมน์ในตาราง · พิมพ์บอก AI ได้ว่า "model ของผมว่างเปล่า แต่เรียก post.title ได้ อธิบายว่า Rails สร้าง attribute จาก column ยังไง"
ApplicationRecord- class แม่ของทุก model ในแอป · เป็นลูกของ
ActiveRecord::Baseอีกทอด · ถาม senior ได้ว่า "ของที่ทุก model ต้องใช้ ควรใส่ไว้ใน ApplicationRecord ไหมครับ"
7 · Module + include — กล่องเครื่องมือที่หลาย class ยืมไปใช้
สมมติอยากให้บทความมี slug — ชื่อเรื่องที่แปลงให้ใส่ใน URL ได้ เช่น "Hello Ruby World" → hello-ruby-world
(เว็บจะได้มี URL แบบ /posts/hello-ruby-world ที่คนอ่านออก) ·
แล้วอยากให้ หมวดหมู่ (Category) ทำ slug ได้ด้วย
ใช้การสืบทอดไม่ได้ เพราะหมวดหมู่ไม่ได้ "เป็น" บทความ · Ruby เลยมี module:
ก้อน method ที่สร้าง object เองไม่ได้ แต่ class ไหนก็ include ไปใช้ได้
module เหมือนชุดเครื่องมือช่างกลางของหมู่บ้าน · ใครก็ยืมไปใช้ได้ บ้านไหนจะยืมก็ไม่ต้องเป็นญาติกัน ·
include Sluggable = "บ้านนี้ยืมชุดเครื่องมือทำ slug มาใช้"
ที่ไม่เหมือนกัน: เครื่องมือช่างจริงทำงานได้ด้วยตัวเอง ·
แต่ method ใน module ใช้ ข้อมูลของบ้านที่ยืมไป — slug ต้องการให้ class นั้นมี title ·
ถ้าไม่มี Ruby ก็ไม่ตรวจตอนยืม แต่จะพังตอนเรียกใช้ครั้งแรก · และยืมแล้วไม่ต้องคืน ทุกบ้านใช้ได้พร้อมกัน
นี่คือโค้ดจากสไลด์หน้า 29:
module Sluggable
def slug
title.downcase.tr(" ", "-")
end
end
class Post
include Sluggable
end
Post.new.slug # ใช้ได้เลย
module Sluggable…end— เขียนเหมือน class แต่ใช้คำว่าmoduletitle.downcase—titleที่เขียนเฉย ๆ ไม่มี@และไม่มีอะไรนำหน้าจุด แปลว่า "เรียก methodtitleของ object ตัวเองที่กำลังทำงานอยู่" · ได้ชื่อเรื่องมาแล้วแปลงเป็นตัวเล็กด้วยdowncase.tr(" ", "-")— แทนที่ช่องว่างทุกตัวด้วยขีดinclude Sluggable— ยัด method ทุกตัวใน Sluggable เข้าไปใน Post
⚠️ พิมพ์ตามสไลด์แล้วพัง — Post ในสไลด์ไม่มี title ให้ slug เรียก:
$ ruby slide29.rb
slide29.rb:3:in 'Sluggable#slug': undefined local variable or method 'title' for an instance of Post (NameError)
title.downcase.tr(" ", "-")
^^^^^
from slide29.rb:11:in '<main>'
แก้โดยเติม attr_accessor :title กับ initialize ให้ Post (ตัวอย่างข้างล่าง)
ฉบับที่รันได้จริง — สอง class ที่ไม่เกี่ยวกัน ยืม Sluggable ตัวเดียวกัน:
# sluggable.rb
module Sluggable
def slug
title.downcase.tr(" ", "-")
end
end
class Post
include Sluggable
attr_accessor :title
def initialize(title)
@title = title
end
end
class Category
include Sluggable
attr_accessor :title
def initialize(title)
@title = title
end
end
puts Post.new("Hello Ruby World").slug
puts Category.new("Thai Food").slug
$ ruby sluggable.rb
hello-ruby-world
thai-food
title ใน slug คือ title ของ object ที่เรียกมัน — ตอนเรียกจาก Post ก็เป็นชื่อบทความ · ตอนเรียกจาก Category ก็เป็นชื่อหมวด ·
และ module หยอดขนมเองไม่ได้ ถ้าสั่ง Sluggable.new จะได้
undefined method 'new' for module Sluggable (NoMethodError)
ฝั่ง Go ที่ใกล้ที่สุดคือ ฝัง struct (embedding) ซึ่งก็ได้ method มาใช้ฟรีเหมือนกัน (รันจริงได้ hello-go-world):
// main.go
package main
import (
"fmt"
"strings"
)
type Sluggable struct {
Title string
}
func (s Sluggable) Slug() string {
return strings.ReplaceAll(strings.ToLower(s.Title), " ", "-")
}
type Post struct {
Sluggable // ฝัง struct ไว้ข้างใน → ได้ Slug() มาใช้ด้วย
}
func main() {
post := Post{Sluggable{Title: "Hello Go World"}}
fmt.Println(post.Slug())
}
type Sluggable struct+func (s Sluggable) Slug()— struct ที่มี methodSlug()ของตัวเอง- ใน
Postเขียนแค่ชื่อชนิดSluggableไม่มีชื่อ field — นี่คือการฝัง · Go จะยก method ของ Sluggable มาให้ Post เรียกได้ตรง ๆ post.Slug()— เรียกได้เหมือนเป็น method ของ Post เอง
ต่างกันตรงที่ Go ต้องย้าย Title ไปอยู่ใน Sluggable เพราะ Slug() มองเห็นแค่ข้อมูลของตัวมันเอง ·
ส่วน module ของ Ruby ใช้ title ของ class ที่ include มันได้เลย ·
และ ไม่เหมือน interface (บท 10) ที่บอกแค่ว่า "ต้องมี method นี้" แล้วให้เราเขียนเอง — module ให้โค้ดที่ทำงานได้มาด้วย
- module
- ก้อน method ที่สร้าง object เองไม่ได้ เอาไว้ให้หลาย class ใช้ร่วมกัน · ถาม senior ได้ว่า "method slug นี้ควรแยกเป็น module ไหมครับ มีหลาย class ใช้"
- mixin /
include - การเอา method ของ module ไปใส่ใน class · class นั้นได้ method ไปใช้ทันที · พิมพ์บอก AI ได้ว่า "แยก method slug ออกเป็น module แล้ว include ใน Post กับ Category ให้หน่อย"
8 · Concern — ที่เก็บ module ของ Rails (แค่ปู)
สไลด์หน้า 29 ปิดท้ายว่า Rails เรียก module แบบนี้ว่า concern และมีโฟลเดอร์ให้เก็บ ·
เปิดโปรเจกต์ที่เพิ่ง rails new ก็เห็นโฟลเดอร์รออยู่แล้ว (.keep คือไฟล์ว่างที่ใส่ไว้ให้ git เก็บโฟลเดอร์เปล่าได้):
$ find app/models/concerns app/controllers/concerns
app/models/concerns
app/models/concerns/.keep
app/controllers/concerns
app/controllers/concerns/.keep
ลองย้าย Sluggable ไปไว้ที่ app/models/concerns/sluggable.rb (เนื้อในเหมือนเดิมทุกตัวอักษร)
แล้วเติม include Sluggable หนึ่งบรรทัดใน class Post < ApplicationRecord:
$ bin/rails runner 'puts Post.new(title: "Hello Rails World").slug'
hello-rails-world
ไม่ต้องเขียน title เอง เพราะ Rails สร้างจากคอลัมน์ให้แล้ว (หัวข้อ 6) · และไม่ต้อง import ไฟล์ sluggable.rb เพราะ Rails หาไฟล์ตามชื่อเอง (บท 99) ·
concern ของจริงมักมีบรรทัด extend ActiveSupport::Concern ด้วย — ไว้เจอทีหลัง ตอนนี้รู้แค่ว่า concern = module ที่วางไว้ในโฟลเดอร์ concerns
Rails เป็นร้านแฟรนไชส์ที่ทุกสาขาวางของตำแหน่งเดียวกัน · โฟลเดอร์ concerns คือชั้นวางเครื่องมือกลางของครัวที่แฟรนไชส์กำหนดไว้แล้ว ·
ห้องไหนในร้านอยากใช้เครื่องทำ slug (model ใช้ชั้นใน app/models/concerns · controller ใช้ชั้นใน app/controllers/concerns) ก็หยิบจากชั้นนี้ (include Sluggable) ไม่ต้องตามหาว่าใครเก็บไว้ที่ไหน
ที่ไม่เหมือนกัน: ชั้นวางจริงหยิบเครื่องมือไปแล้วคนอื่นต้องรอ ·
แต่ module ถูก include ได้พร้อมกันหลาย model ไม่มีใครต้องรอ ·
และของบนชั้นจริงหยิบได้ด้วยมือ ส่วน Rails หาไฟล์ให้จากชื่อ — ตั้งชื่อไฟล์ sluggable.rb ให้ตรงกับ Sluggable ถึงจะหาเจอ
- concern
- module ที่ Rails แนะนำให้วางใน
app/models/concerns/หรือapp/controllers/concerns/เพื่อแชร์ method ระหว่าง model หรือ controller · ถาม senior ได้ว่า "method slug ที่ Post กับ Category ใช้ร่วมกัน ควรแยกเป็น concern ไหมครับ"
9 · ภารกิจ — บทความ สูตรอาหาร และ slug
🎯 ภารกิจ: เขียน blog.rb ไฟล์เดียว (~10 นาที)
- สร้างไฟล์
blog.rbที่ไหนก็ได้ (ไม่ต้องใช้ Rails) - เขียน
module Sluggableที่มี methodslugแบบหัวข้อ 7 - เขียน
class Postที่include Sluggable· มีattr_accessor :title·initialize(title)· และgreeting - เขียน
class Recipe < Postที่รับtitleกับminutes(ใช้super(title)) - ท้ายไฟล์สร้าง
Recipe.new("Tom Yum Goong", 15)แล้วputsค่าgreeting·slug·minutes - รัน
ruby blog.rb· ควรเห็นสามบรรทัด:Hello, Tom Yum Goong·tom-yum-goong·15
# blog.rb
module Sluggable
def slug
title.downcase.tr(" ", "-")
end
end
class Post
include Sluggable
attr_accessor :title
def initialize(title)
@title = title
end
def greeting
"Hello, #{@title}"
end
end
class Recipe < Post
attr_accessor :minutes
def initialize(title, minutes)
super(title)
@minutes = minutes
end
end
recipe = Recipe.new("Tom Yum Goong", 15)
puts recipe.greeting
puts recipe.slug
puts recipe.minutes
$ ruby blog.rb
Hello, Tom Yum Goong
tom-yum-goong
15
สังเกตว่า Recipe ใช้ slug ได้ทั้งที่ไม่ได้ include เอง — เพราะแม่ (Post) include ไว้แล้ว ลูกก็ได้ไปด้วย
ถ้าเจอ error นี้ แปลว่า… (ทุกอันรันจริงจาก blog.rb ข้างบน แล้วแก้ให้ผิดทีละจุด ·
วิธีอ่าน: บรรทัดแรกบอกไฟล์:บรรทัดที่พัง และชื่อ error ในวงเล็บท้ายสุด · บรรทัด from ข้างล่างบอกว่าถูกเรียกมาจากไหน)
blog.rb:4:in 'Sluggable#slug': undefined local variable or method 'title' for an instance of Recipe (NameError)
title.downcase.tr(" ", "-")
^^^^^
Did you mean? @title
from blog.rb:31:in '<main>'
→ ลืม attr_accessor :title ใน Post · slug หา method title ไม่เจอ (มีแค่ @title ข้างใน ซึ่ง Ruby ใบ้ให้ว่า "Did you mean? @title") ·
ก่อน error จะเห็น Hello, Tom Yum Goong พิมพ์ออกมาก่อนด้วย เพราะ greeting ใช้ @title ตรง ๆ ไม่ได้พึ่ง method title
blog.rb:9:in '<class:Post>': uninitialized constant Post::Slugable (NameError)
include Slugable
^^^^^^^^
Did you mean? Sluggable
from blog.rb:8:in '<main>'
→ สะกดชื่อ module ผิด (ตก g ไปหนึ่งตัว) · uninitialized constant แปลว่า "ไม่รู้จักชื่อตัวใหญ่นี้"
blog.rb:12:in 'Post#initialize': wrong number of arguments (given 2, expected 1) (ArgumentError)
caller: blog.rb:25
callee: blog.rb:12
| def initialize(title)
^^^^^^^^^^
from blog.rb:25:in 'Recipe#initialize'
from blog.rb:30:in '<main>'
→ เขียน super เฉย ๆ ไม่มีวงเล็บ · Ruby ส่ง argument ทุกตัวของลูก (title และ minutes = 2 ตัว) ต่อไปให้แม่ ซึ่งรับได้ตัวเดียว ·
caller: blog.rb:25 / callee: blog.rb:12 บอกว่าบรรทัด 25 (ใน Recipe) เรียกไปหาบรรทัด 12 (ใน Post) ·
แก้เป็น super(title) · ถ้าเจอ (given 0, expected 1) แทน แปลว่าเรียก Post.new โดยไม่ได้ส่งชื่อเรื่องไป
10 · ทดสอบความเข้าใจ
เมื่อเขียน Post.new("Hi") Ruby เรียก method ไหนของ Post ให้อัตโนมัติ?
ตัวเลือกที่ 2 ถูก · new สร้าง object เปล่าแล้วส่ง "Hi" ต่อให้ initialize ·
ถ้าเรียก Post.new โดยไม่ส่งอะไร error จะบอกว่าพังใน 'Post#initialize' (given 0, expected 1) ซึ่งยืนยันว่า new ไปเรียก initialize
ตัวเลือกที่ 1 ผิดเพราะ new มีให้ทุก class อยู่แล้ว เราเขียนแค่ initialize ·
ตัวเลือกที่ 3 ผิดเพราะ title ถูกเรียกเมื่อเราสั่ง post.title เท่านั้น ·
ตัวเลือกที่ 4 ผิดเพราะ Ruby ไม่ได้เรียก method ตามลำดับที่เขียน
จุดที่ต้องจำ: Post.new(...) = สร้าง object แล้วรัน initialize(...) · เหมือน NewPost(...) ของ Go
class Post มีแค่ attr_reader :title · ถ้าสั่ง post.title = "ใหม่" จะเกิดอะไรขึ้น?
ตัวเลือกที่ 3 ถูก · ผลรันจริงในหัวข้อ 3 คือ undefined method 'title=' for an instance of Post (NoMethodError) ·
post.title = "ใหม่" คือการเรียก method ชื่อ title= ซึ่ง attr_reader ไม่ได้สร้างให้ (สร้างแค่ title)
ตัวเลือกที่ 1 ผิดเพราะ reader = อ่านอย่างเดียว ·
ตัวเลือกที่ 2 ผิดเพราะ Ruby ไม่ทิ้งคำสั่งเงียบ ๆ แต่พังทันที ·
ตัวเลือกที่ 4 ผิดเพราะโปรแกรมหยุดที่ error ไม่ได้ไปแตะ @title เลย
จุดที่ต้องจำ: เห็น error ที่มีชื่อ method ลงท้ายด้วย = แปลว่าไม่มี setter → ใช้ attr_accessor แทน
ใน Rails ไฟล์ app/models/post.rb มีแค่ class Post < ApplicationRecord กับ end · ทำไม post.title ถึงใช้ได้?
ตัวเลือกที่ 1 ถูก · ในหัวข้อ 6 Post.column_names มี "title" และ respond_to?(:title) ได้ true ·
แต่ respond_to?(:author) ได้ false เพราะตารางไม่มีคอลัมน์ author — method ตามคอลัมน์จริง
ตัวเลือกที่ 2 ผิดเพราะไฟล์ post.rb ว่างจริง ๆ ไม่มีอะไรถูกเขียนเพิ่ม · ตัวเลือกที่ 3 ผิดเพราะ class Post ของเราเองในหัวข้อ 3 ยัง NoMethodError อยู่เลยถ้าไม่มี attr_accessor · ตัวเลือกที่ 4 ผิดเพราะ ApplicationRecord ไม่รู้ว่าแต่ละ model มีช่องอะไร ต้องไปดูที่ตารางของ model นั้น
จุดที่ต้องจำ: Go ต้องเขียน field ใน struct เอง · Rails อ่านช่องข้อมูลจากตารางในฐานข้อมูลให้
ข้อไหนอธิบาย module ได้ถูกต้อง?
ตัวเลือกที่ 4 ถูก · ในหัวข้อ 7 Post กับ Category include Sluggable ตัวเดียวกันได้ ·
ส่วน Sluggable.new พังด้วย undefined method 'new' for module Sluggable
ตัวเลือกที่ 1 ผิดตามผลรันข้างบน ·
ตัวเลือกที่ 2 ผิดเพราะโค้ดสไลด์หน้า 29 include ผ่าน แต่ไปพังตอนเรียก slug (NameError) — Ruby ไม่ตรวจล่วงหน้า ·
ตัวเลือกที่ 3 ผิดเพราะ < ใช้กับ class แม่ ส่วน module ใช้ include
จุดที่ต้องจำ: สืบทอด (<) = "เป็นชนิดหนึ่งของ" · include module = "ยืมความสามารถมาใช้"
ติดตรงไหนถามกลับได้เลย เช่น "ทำไม attr_accessor เขียนในตัว class ได้ ทั้งที่มันเป็น method" หรือ
"เมื่อไหร่ควรใช้ class แม่ เมื่อไหร่ควรใช้ module" · บทถัดไปจะดูว่าเมื่อโค้ดพัง Ruby จัดการ error ยังไง
ถ้าเปิดสไลด์ดูประกอบ
| หน้า | สไลด์เขียน | ของจริง | ต้องสนใจไหม |
|---|---|---|---|
| 29 | Post.new.slug # ใช้ได้เลย |
พังด้วย NameError เพราะ Post ในสไลด์ไม่มี title · ต้องเติม attr_accessor :title + initialize (หัวข้อ 7) |
ใช่ ถ้าพิมพ์ตาม |
| 28 | @ตัวแปรที่ยังไม่เคยตั้ง อ่านแล้วได้ nil ไม่ error | ถูก · แต่แปลว่าสะกดชื่อผิดก็ไม่มีใครเตือน (หัวข้อ 4) | นิดหน่อย |
| 29 | module ใกล้เคียง embedding ของ Go | ใกล้ตรงที่ได้โค้ดมาฟรี · แต่ method ของ Go เห็นแค่ข้อมูลของ struct ที่ฝัง ส่วน module เห็นข้อมูลของ class ที่ include | ไม่ต้อง |
| ทั้งชุด | Ruby 4.0.6 · Rails 8.1.3 | บทนี้ตรวจด้วย Ruby 4.0.9 · Rails 8.1.4 ผลเหมือนกัน | ไม่ต้อง |
อ้างอิง
- Ruby 4.0 — Modules and Classes —
อ่านหัวข้อ Classes → Inheritance (เรื่อง
<และsuper) แล้วย้อนขึ้นไปอ่าน Modules · หน้าเดียวอธิบายทั้งสองอย่าง เห็นความเหมือนและต่างชัด - Ruby 4.0 — Assignment: Instance Variables — หัวข้อสั้น ๆ หัวข้อเดียว · มีประโยคยืนยันว่า @ตัวแปรที่ยังไม่ตั้งค่าได้ nil (กับดักในหัวข้อ 4)
- Ruby 4.0 — Module#attr_accessor —
ดูตัวอย่างของ
attr_accessorแล้วเลื่อนดูattr_reader/attr_writerที่อยู่ติดกัน - Rails Guides — Active Record Basics: Creating Active Record Models —
อ่านแค่หัวข้อนี้ · ยืนยันว่า
class Book < ApplicationRecordว่าง ๆ ก็ผูกกับตาราง books และคอลัมน์กลายเป็น attribute (ตรงกับหัวข้อ 6) · ที่เหลือรอ Day 2 - Rails API — ActiveSupport::Concern — ดูแค่ตัวอย่างแรกพอให้รู้ว่า concern หน้าตาเป็น module · รายละเอียดอื่นข้ามไปก่อน
- Effective Go — Embedding —
ทวนการฝัง struct ของ Go เพื่อเทียบกับ
includeในหัวข้อ 7