ยินดีต้อนรับสู่โพสต์แรกในซีรีส์ Ruby on Rails Patterns and Anti-patterns ในแต่ละโพสต์จะ deep dive ทุกประเภท patterns ที่อาจเจอขณะทำงานกับ Rails apps
วันนี้จะ show ว่า (design) pattern คืออะไร และพยายามอธิบาย anti-pattern ด้วย เพื่อ illustrate ให้ชัดเจน จะใช้ Ruby on Rails framework ที่มานานแล้ว ถ้า Rails ไม่ใช่ cup of tea ของคุณ กดทนไว้ ไอเดีย (หรือ patterns) ที่อธิบายที่นี่อาจ resonate กับ technology ใดๆ ที่คุณใช้
แต่ก่อนจะ jump เข้าไปอธิบาย patterns และ anti-patterns เราไปถึงจุดที่ต้องการพวกมันยังไง? ทำไมต้องมีทั้งหมดนี้สำหรับซอฟต์แวร์? ทำไมต้อง design solution ของเรา?
Yes, You Are a Designer (ใช่ คุณคือ Designer)
ตั้งแต่วันแรกของคอมพิวเตอร์ programming เผลอต้อง deal กับ design ของโปรแกรมที่เขียน การเขียนโปรแกรม (หรือซอฟต์แวร์) คือการ design solution สำหรับปัญหา ตอนเขียนซอฟต์แวร์ คุณคือ designer — feel free เพิ่มใน job title การ design good solutions สำคัญเพราะซอฟต์แวร์ที่เราเขียนจะถูกอ่านและ/หรือแก้ไขโดยผู้อื่น และ solutions ที่เราคิดจะถูก built on โดยผู้อื่นในอนาคต
มีทั้งหมดนี้ในใจ generations ของ engineers เริ่มเห็น similar designs ใน code และ architecture ตลอดอาชีพ คนเริ่ม extract และ document standard solutions ไปยังปัญหา บางคนบอกเป็น natural way ของมนุษย์ เราชอบ categorize และหา patterns ในทุกอย่าง software ไม่ใช่ exception
เป็นมนุษย์ patterns เริ่ม emerge มากขึ้นเมื่อ software engineering complex ขึ้น Software design patterns เริ่มพัฒนาและคemment ตัวเองกับ engineers ทั่วโลก หนังสือ essays และ talks ถูกให้ เพิ่มเติม ideas ของ well thought out และ battle-tested solutions solutions นั้นประหยัดเวลาและเงินเยอะ มาไปรู้จัก design pattern จริงๆ
Design Pattern คืออะไร?
ใน software engineering pattern ถูกอธิบายว่า solution ที่ reuse ได้แก้ปัญหาทั่วไป Pattern คือสิ่งที่ considered good practice ระหว่าง software engineers เนื่องจาก software engineers ตั้งค่า พวกมันสามารถเปลี่ยนจาก patterns ไป opposite — anti-patterns — ได้เร็ว แต่จะไปที่นั้นภายหลัง
Design pattern จะ show you the way ไป solution แต่ไม่ให้ code snippet พร้อมเสียบเข้า software คิด pattern เป็น guide สำหรับเขียน well-designed code แต่คุณต้อง implement เอง การใช้ patterns ใน day-to-day coding emerge ใน late 80s ที่ Kent Beck และ Ward Cunningham คิด idea ใช้ 'pattern language'
Pattern languages idea มา late 70s โดย Christopher Alexander ในหนังสือ A Pattern Language คุณอาจ surprised หนังสือไม่อยู่เกี่ยว software engineering แต่ architecture ของ buildings Pattern language คือ organized และ coherent set ของ patterns แต่ละอัน describe ปัญหาและ core ของ solution ที่ใช้ได้หลายทาง คุ้นเคยไหม? (Hint: frameworks, another hint: Rails)
ภายหลัง design patterns ใน software engineering famous กับ large audiences หลังหนังสือตำนาน Design Patterns by the Gang Of Four เผยแพร่ 1994 ในหนังสือมี explanations และ definitions ของ patterns ที่ใช้ اليوم — Factory, Singleton, Decorator เพื่อนำเพียง 몇
ดี ตอนนี้รู้จักหรือ refresh knowledge บน design และ patterns แล้ว มาหาว่า anti-patterns คืออะไร
Design Anti-Pattern คืออะไร?
คิด patterns เป็น good guys anti-patterns คือ bad guys เพื่อ precise software anti-pattern คือ pattern ที่ commonly used แต่ considered ineffective หรือ counterproductive ตัวอย่างทั่วไปของ anti-patterns คือ God objects ที่มี functions และ dependencies มาก อาจ extract และแยกเป็น objects ต่างๆ ได้
Common causes ของ anti-patterns ใน code มากมาย เช่น good pattern กลาย bad guy (anti-pattern) สมมติคุณเคยใช้ technology เฉพาะที่ company ก่อนหน้า และมี competence สูง มาใช้ Docker ตัวอย่าง คุณรู้ pack applications เข้า Docker containers, orchestrate ใน cloud, และ pull logs จาก cloud งานใหม่ต้อง ship front end applications เนื่องจากรู้ Docker มาก 決定 package ทุกอย่าง deploy ไป cloud
แต front end apps ไม่ complex ที่งานปัจจุบัน การใส่ containers อาจไม่ใช่ effective solution ดูดีตอนแรก แต่ภายหลังพบว่า counterproductive Anti-pattern นี้เรียกว่า "Golden Hammer"
สรุปได้จากคำพูด "If you have a hammer, everything looks like a nail" ถ้าคุณเก่ง Docker และ orchestration ทุกอย่างคือ Docker service ทำให้ orchestrate ใน cloud
สิ่งเหล่านี้เกิดขึ้นและจะเกิดขึ้น Good guys กลาย bad guys และ vice-versa แต Ruby และ Rails เข้ามาใน picture ยังไง?
Ruby First, Then Rails (Ruby ก่อน แล้วค่อย Rails)
ส่วนใหญ่รู้จัก Ruby ผ่าน Ruby on Rails framework ยอดนิยมสำหรับสร้างเว็บไว ผมรู้จัก Ruby แบบเดียวกัน ไม่มีอะไรผิด Rails based on well-established software pattern เรียกว่า Model-View-Controller หรือ MVC ก่อนจะ dive MVC ใน Rails fallacy ใหญ่ที่เกิดบ่อยคือใช้ Rails โดยไม่เรียน Ruby ให้ถูกต้อง
Rails framework เป็น go-to frameworks ตอนมี idea อยาก build ไว ตอนนี้ต่างหาก Rails ยังใช้ แต่ไม่ถึง extent ใน prime ง่ายต่อการใช้และรัน หลาย beginners เริ่ม build web apps ด้วย rails new command แล้ว problems เริ่ม occurring ตามทาง Beginner ถูกล่อลวงโดย speed และ simplicity ของ development กับ Rails ทุกอย่าง work magically และ smoothly ตอนแรก แล้วเห็นว่าเอา 'magic' มากเกินไป granted และไม่เข้าใจ behind the curtain
ผมเจอปัญหานี้ และแน่ใจว่า beginners และ advanced beginners มากมาย suffer จากมัน เริ่มด้วย framework ในมือ build บนมัน พยายาม add something highly custom ไม่ได้ เพราะใช้ magic points ของ framework หมดแล้ว ที่จุดนั้น ต้องกลับต้น head เรียน basics กลับไปไม่ใช่ big deal เกิดกับ the best of us แตปัญหาใหญ่ขึ้นถ้า move on โดยไม่เรียน essential things เช่น Ruby หนังสือดีที่ช่วยคือ The Well-Grounded Rubyist
Beginner ไม่ต้องอ่าน cover-to-cover แต่เก็บข้างเคียง consult เร็วๆ ได้ ไม่บอกให้หยุดทุกอย่างอ่านทั้งเล่ม แต่หยุดบ้าง refresh Ruby basics อาจเปิด new horizons
MVC: Rails' Bread & Butter
โอเค แต่ MVC นี่? Model-View-Controller pattern มานานแล้ว ถูก adopt โดย frameworks หลายๆ ตัว across plethora ของ languages เช่น Ruby (Rails), Python (Django), Java (Play, Spring MVC) Idea คือมี separate components แต่ละอันทำหน้าที่:
- Model จัดการ data และ business logic
- View สำหรับ presentation ของ data และ user interface
- Controller ผูกสองอย่างด้วยการได้ data จาก Model และ show View ให้ user
聽起來 great ใน theory และ excellent ตอน logic minimal และเว็บไซต์ไม่ hold complex logic นั่นคือที่ tricky แต่เราจะไปถึงในวินาที
MVC spread out like wildfire ใน web development community แม้ libraries อย่าง React ที่ popular บาปๆ อธิบายว่าเป็น view layer ของ web app No other pattern ถูก popularized มากจน shake off ไม่ได้ Rails เพิ่ม Publish-Subscribe ด้วย ActionCable โดย concept ของ channels อธิบายว่าเป็น controller ของ MVC pattern
แต anti-patterns อยู่ที่ไหน ใน pattern ที่ widely used นี้? ไปดู common anti-patterns สำหรับแต่ละส่วนของ MVC
Model Problems
แอปพลิเคชันโต และ business logic ขยาย คน tend to overcrowd models Constant growth นำไปสู่ anti-pattern เรียกว่า Fat Model
Known 'Fat Model, Skinny Controller' pattern บางคน identify เป็น bad guy บ้างเป็น good guy เราจะบอกว่า having any of the fat คือ anti-pattern เข้าใจดีกว่ามา example สมมติมี streaming service เช่น Spotify หรือ Deezer ภายในมี model สำหรับ songs เหมือนนี้:
class Song < ApplicationRecord
belongs_to :album
belongs_to :artist
belongs_to :publisher
has_one :text
has_many :downloads
validates :artist_id, presence: true
validates :publisher_id, presence: true
after_update :alert_artist_followers
after_update :alert_publisher
def alert_artist_followers
return if unreleased?
artist.followers.each { |follower| follower.notify(self) }
end
def alert_publisher
PublisherMailer.song_email(publisher, self).deliver_now
end
def includes_profanities?
text.scan_for_profanities.any?
end
def user_downloaded?(user)
user.library.has_song?(self)
end
def find_published_from_artist_with_albums
...
end
def find_published_with_albums
...
end
def to_wav
...
end
def to_mp3
...
end
def to_flac
...
end
end
ปัญหา model แบบนี้กลายเป็น dumping ground สำหรับ logic ต่างๆ ที่เกี่ยวกับเพลง เกิดขึ้นเมื่อ methods เพิ่มทีละนิดตามเวลา Whole model ดูใหญ่และซับซ้อน แยก logic ไปสองสามที่อาจ beneficial ในอนาคต
ทันทีเห็น recommended practices ที่ model นี้ breaking SRP จัดการ notify followers และ publisher check text for profanities มี methods export เพลงไป audio formats ต่างๆ ทั้งหมดเพิ่ม complexity ของ model ไม่จินตนาการได้ถึง test file
Refactor model นี้ majorly depends on ว่ methods ถูกเรียกและใช้ในที่อื่นยังไง จะ present general ideas จัดการ และเลือกที่ fit กรณีคุณ
Callbacks notify followers และ publisher extract ไป jobs Jobs จะ enqueued และ logic ออกจาก model:
class NotifyFollowers < ApplicationJob
def perform(followers)
followers.each { |follower| follower.notify }
end
end
class NotifyPublisher < ApplicationJob
def perform(publisher, song)
PublisherMailer.song_email(publisher, self).deliver_now
end
end
Jobs รัน process แยก ห่างจาก model ตอนนี้ test job logic แยกได้ และ check ว่า proper job enqueued จาก model
สมมติ check profanities และ user downloaded song เกิดใน view part ใช้ Decorator pattern หนึ่ง popular solution คือ Draper gem เขียน decorator คล้ายนี้:
class SongDecorator < Draper::Decorator
delegate_all
def includes_profanities?
object.text.scan_for_profanities.any?
end
def user_downloaded?(user)
object.user.library.has_song?(self)
end
end
เรียก decorate ใน controller:
def show
@song = Song.find(params[:id]).decorate
end
ใช้ใน views:
<%= @song.includes_profanities? %>
<%= @song.user_downloaded?(user) %>
ถ้าไม่ชอบ dependency rolling your decorator ได้ แต่พูดในโพสต์อื่น ตอนนี้ got majority ของ model concerns separated มาจัดการ methods หาเพลงและแปลงเพลง ใช้ modules แยก:
module SongFinders
def find_published_from_artist_with_albums
...
end
def find_published_with_albums
...
end
end
module SongConverter
def to_wav
...
end
def to_mp3
...
end
def to_flac
...
end
end
Song model extend SongFinders module ให้ methods available as class methods Song model include SongConverter module ให้ methods available on model instances
ทั้งหมดควรทำให้ Song model สลิมและ on point:
class Song < ApplicationRecord
extend SongFinders
include SongConverter
belongs_to :album
belongs_to :artist
belongs_to :publisher
has_one :text
has_many :downloads
validates :artist_id, presence: true
validates :publisher_id, presence: true
after_update :alert_artist_followers, if: :published?
after_update :alert_publisher
def alert_artist_followers
NotifyFollowers.perform_later(self)
end
def alert_publisher
NotifyPublisher.perform_later(publisher, self)
end
end
มี model anti-patterns อีกเยอะ นี่แค่ตัวอย่างหนึ่งของสิ่งที่ผิดพลาดได้กับ models ติดตามโพสต์ต่อไปในซีรีส์ จะไป details model anti-patterns มากขึ้น ตอนนี้ดู view problems
View Problems
นอกจาก model problems Rails folks บางครั้ง struggle กับ complexity ของ views Back in the day HTML และ CSS เป็น kings ของ view part เวลาผ่าน JavaScript มาราชา และเกือบ all aspects ของ front end เขียนใน JavaScript Rails follow paradigm ต่างนิดหน่อย แทนที่จะมี everything ใน JavaScript ใน view ควรแค่ "sprinkle" JS onto it
ในทุกกรณี deal กับ HTML, CSS, JS, และ Ruby ที่เดียวกัน messy ได้ Tricky กับ building Rails views คือ domain logic บางครั้งอยู่ใน view นี่ no-no เพราะ break MVC pattern ตั้งแต่ต้น
Another case ใช้ embedded Ruby มากเกินไปใน views และ partials บาง logic อาจไปใน helper หรือ decorator (รู้จักว่า view model หรือ presenter) จะไป examples ในโพสต์ถัดไปในซีรีส์ ติดตาม
Controller Problems
Rails controllers สามารถ suffer จากปัญหาต่างๆ หนึ่งคือ Fat Controller anti-pattern
ก่อน model fat แต่ลดน้ำหนัก ตอนนี้เห็น controller เพิ่มน้ำหนักใน process ปกติเกิดเมื่อ business logic วางใน Controller แต่ที่จริงควรอยู่ใน model หรือที่อื่น Ideas จาก large Model section บางอย่างยัง apply ให้ controller — extract code to presenters, ใช้ ActiveRecord callbacks, resort to Service objects
บางคน resort gems อย่าง Trailblazer หรือ dry-transaction Idea คือสร้าง classes จัดการ specific transactions ย้ายทุกอย่างออกจาก controller เก็บ model skinny store และ test logic ใน separate classes ที่บางคนเรียก services, transactions, actions, etc.
สรุป
มี anti-patterns อีกเยอะ และ solutions เยอะกว่า พยายาม cover ทุกอย่างในโพสต์นี้จะใช้ space และ time มาก และทำให้โพสต์ดู fat (เหมือน model และ controller ที่พูด) ติดตามซีรีส์ จะ deep dive ทุก aspect ของ MVC pattern ใน Rails ที่นี่จะหาวิธีจัดการ famous anti-patterns จนกว่าจะเจอ cheers!
P.S. หากอยากอ่าน Ruby Magic posts ทันทีที่เผยแพร่ สมัครสมาชิก Ruby Magic newsletter และไม่พลาดโพสต์เด็ดๆ!