Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

ป้องกัน Unused Classes ใน Ruby: คู่มือ Clean Object Modeling

เพราะ 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