เพราะ Ruby เป็น object-oriented language เรามัก model โลกเป็น set of objects เราบอกว่าสอง integers (x และ y) เป็น Point และ Line มีสองของมัน
แนวทางนี้บ่อยครั้ง useful แต่มีปัญหาใหญ่หนึ่ง มัน privilege one interpretation ของ data ทั้งหมด มัน assume ว่า x และ y จะเป็น Point เสมอ และคุณไม่ต้องการให้มัน act as Cell หรือ Vector
เกิดอะไรตอนต้องการ Cell? Point own data ดังนั้นคุณเพิ่ม cell methods เข้า Point เวลาผ่านไป คุณได้ Point class ที่เป็น junk drawer ของ loosely-related methods
Junk-drawer classes common จนเรายอมรับว่า inevitable และ tack on "refactoring" step แต่ถ้าเราสามารถหลีกเลี่ยงปัญหาตั้งแต่แรกได้ไหม?
กฎแรกของ web development คือเราไม่พูดถึง User class
หัวใจของ battle-hardened production Rails app คือ class ยักษ์ชื่อ User
มันเริ่ม innocently อยากให้คน login ต้องเก็บ username password สร้าง class:
class User
attr_accessor :username, :password, :email, :address
def authenticate!(password)
...
end
end
ทำงานดีจนคนอยากให้เงิน "โอเค" เราบอก "User 基本上 same as Subscriber เพิ่ม attributes และ methods เลย"
class User
...
attr_accessor :payment_processor_token, :subscription_plan_id, ...etc
def charge_cc
...
end
end
เยี่ยม! ทำเงินจริง! CEO อยาก export VCards ด้วย contact info ให้ sales team import SalesForce เปิด vim:
class User
...
def export_vcard
...
end
end
เราทำอะไรไป?
เริ่ม User class purpose เฉพาะ authentication เพิ่ม methods และ attributes กลายเป็น User/Subscriber/Contact Frankenstein hybrid
ทำไมเราทำนี้? เราไม่ respect ourselves as developers? พ่อแม่ไม่รักพอ? หรือเราตั้งตัวเอง fail โดยเชื่อว่า data นี้ is a thing, an object?
ทำไมบอกว่า username, password, email combo คือ User? ทำไมไม่ใช่ Subscriber? Contact? SessionHolder?
ความจริงของ data คือมันคือ data
Data คือแค่ data วันนี้ต้อง treat มันเหมือน boyfriend พรุ่งนี้อาจเป็น ex-boyfriend
นี่คือแนวทางของ functional languages อย่าง Elixir Data เก็บใน simple structures เหมือน Ruby hashes, arrays, strings ต้องทำอะไรกับ data ส่งเข้า function ผลลัพธ์ return จาก function
ดู simple แตแนวทางนี้ทำให้ separate concerns เข้า modules ง่ายมาก
Cartoon ของการสร้าง User system ใน Elixir:
my_user = %{username: "foo", password: ..., phone: ..., payment_token: ...}
my_user = Authentication.authenticate(my_user)
my_user = Subscription.charge(my_user)
my_user = Contact.export_vcard(my_user)
เพราะ data แยกจาก code ไม่มี module ไหน privileged interpretation ของ data นั้น
นำกลับมาที่ Ruby
เพราะแนวทางนี้ดีใน Elixir ทำไมไม่ adopt ใน Ruby? ไม่มีอะไรขวางไม่ให้ทำ User ให้ simple wrapper สำหรับ data และดึง business logic ไป modules
class User
attr_accessor :username, :password, :email, :address
end
module Authentication
def self.authenticate!(user)
..
end
end
module Subscription
def self.charge(user)
..
end
end
module Contact
def self.export_vcard(user)
..
end
end
อาจทำ User class เป็น struct หรือเพิ่ม code ทำให้ (kind-of) immutable
So what? (那又怎样?)
ดูเหมือน trivial change จุดประสงค์คืออะไร?
เหมือนที่เห็น typical OO approach มีปัญหาเมื่ออายุเพิ่ม ด้วยการบอกว่า class แค่ owns data เราเจอปัญหาตอนต้องใช้ data 方式อื่น
ใน modular approach แต่ adding behavior ไม่มีปัญหา สร้าง module ใหม่ interpret data ตามที่ต้องการ ง่ายเพราะ data และ functionality แยกกันสมบูรณ์
Other approaches (แนวทางอื่นๆ)
มีแนวทางอื่นป้องกัน junk-drawer problem แบบดั้งเดิมใน OOP พยายามผ่าน inheritance และ careful refactoring ปี 2012 Sandi Metz 出版 Practical Object Oriented Design in Ruby convainc หลายคน compose objects ด้วย dependency injection ไม่กี่ปีมา functional programming popular ทำให้ Rubyists ทดลอง immutable "data objects"
ทุกแนวทางสร้าง clean, beautiful code ได้ แต่ข้อเท็จจริงที่ classes typically own data หมายถึงจะมี tension ระหว่าง class is และ class ทำกับ data
ผมสงสัยว่า success ของแนวทางเหล่านี้ในการหลีกเลี่ยง junk-drawer problem มาจากการ decouple data จาก code