ยินดีต้อนรับกลับสู่โพสต์ที่สองในชุด Ruby on Rails Patterns และ Anti-patterns ในบล็อกโพสต์ที่แล้ว เราได้พูดถึงรูปแบบและรูปแบบการต่อต้านโดยทั่วไป นอกจากนี้เรายังกล่าวถึงรูปแบบที่มีชื่อเสียงที่สุดและรูปแบบการต่อต้านรูปแบบต่างๆ ในโลกของ Rails ในบล็อกโพสต์นี้ เราจะพูดถึงรูปแบบและรูปแบบการต่อต้านรูปแบบ Rails สองสามแบบ
หากคุณกำลังดิ้นรนกับโมเดล โพสต์บล็อกนี้เหมาะสำหรับคุณ เราจะดำเนินขั้นตอนอย่างรวดเร็วในการนำแบบจำลองของคุณเข้าสู่การควบคุมอาหารและปิดท้ายด้วยบางสิ่งที่ควรหลีกเลี่ยงเมื่อเขียนการย้ายถิ่น โดดเข้าไปเลย
อ้วน รุ่นน้ำหนักเกิน
เมื่อพัฒนาแอปพลิเคชัน Rails ไม่ว่าจะเป็นเว็บไซต์ Rails เต็มรูปแบบหรือ API ผู้คนมักจะเก็บตรรกะส่วนใหญ่ไว้ในโมเดล ในบล็อกโพสต์ที่แล้ว เรามีตัวอย่าง Song
คลาสที่ทำหลายๆ อย่าง การเก็บหลายๆ อย่างไว้ในโมเดลเป็นการฝ่าฝืนหลักการ Single Responsibility Principle (SRP)
มาดูกันเลย
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
ปัญหาของโมเดลเหล่านี้คือพวกมันกลายเป็นจุดทิ้งของตรรกะต่างๆ ที่เกี่ยวข้องกับเพลง วิธีการเริ่มซ้อนขึ้นเมื่อเพิ่มทีละน้อยทีละครั้ง
ฉันแนะนำให้แยกโค้ดภายในโมเดลออกเป็นโมดูลเล็กๆ แต่การทำเช่นนั้น คุณเพียงแค่ย้ายโค้ดจากที่หนึ่งไปยังอีกที่หนึ่ง อย่างไรก็ตาม การย้ายโค้ดไปรอบๆ ช่วยให้คุณจัดระเบียบโค้ดได้ดีขึ้น และหลีกเลี่ยงโมเดลอ้วนที่มีความสามารถในการอ่านลดลง
บางคนถึงกับหันไปใช้ความกังวลเรื่อง Rails และพบว่าตรรกะนี้สามารถนำมาใช้ซ้ำได้ในหลายรุ่น ก่อนหน้านี้ฉันเขียนเกี่ยวกับเรื่องนี้และบางคนชอบมัน แต่คนอื่นไม่ชอบ อย่างไรก็ตาม เรื่องราวที่มีความกังวลนั้นคล้ายกับโมดูล คุณควรตระหนักว่าคุณกำลังย้ายโค้ดไปยังโมดูลที่สามารถรวมไว้ที่ใดก็ได้
อีกทางเลือกหนึ่งคือสร้างชั้นเรียนขนาดเล็กแล้วเรียกชั้นเรียนเหล่านั้นเมื่อต้องการ ตัวอย่างเช่น เราสามารถแยกรหัสการแปลงเพลงเป็นชั้นเรียนแยกต่างหากได้
class SongConverter
attr_reader :song
def initialize(song)
@song = song
end
def to_wav
...
end
def to_mp3
...
end
def to_flac
...
end
end
class Song
...
def converter
SongConverter.new(self)
end
...
end
ตอนนี้เรามี SongConverter
ที่มีจุดประสงค์เพื่อแปลงเพลงเป็นรูปแบบต่างๆ มันสามารถมีการทดสอบของตัวเองและตรรกะในอนาคตเกี่ยวกับการแปลง และถ้าเราต้องการแปลงเพลงเป็น MP3 เราสามารถทำสิ่งต่อไปนี้:
@song.converter.to_mp3
สำหรับฉัน สิ่งนี้ดูชัดเจนกว่าการใช้โมดูลหรือข้อกังวลเล็กน้อย อาจเป็นเพราะฉันชอบใช้องค์ประกอบมากกว่าการสืบทอด ฉันคิดว่ามันสัญชาตญาณและอ่านง่ายกว่า ฉันแนะนำให้คุณทบทวนทั้งสองกรณีก่อนตัดสินใจว่าจะไปทางไหน หรือจะเลือกทั้งสองอย่างก็ได้ตามใจชอบ ไม่มีใครหยุดคุณได้
SQL พาสต้าพาร์เมซาน
ใครไม่ชอบพาสต้าดีๆในชีวิตจริงบ้าง? ในทางกลับกัน เมื่อพูดถึงโค้ดพาสต้า แทบไม่มีใครเป็นแฟน และด้วยเหตุผลที่ดี ใน Railsmodels คุณสามารถเปลี่ยนการใช้งาน Active Record ของคุณให้เป็นสปาเก็ตตี้ได้อย่างรวดเร็ว โดยหมุนวนไปรอบๆ โค้ดเบส คุณจะหลีกเลี่ยงสิ่งนี้ได้อย่างไร
มีแนวคิดบางอย่างที่ดูเหมือนจะช่วยป้องกันไม่ให้ข้อความค้นหายาวเหยียดกลายเป็นเส้นสปาเก็ตตี้ได้ ก่อนอื่นเรามาดูกันว่ารหัสที่เกี่ยวข้องกับฐานข้อมูลสามารถอยู่ได้ทุกที่ได้อย่างไร กลับไปที่ Song
. ของเรา แบบอย่าง. โดยเฉพาะเมื่อเปียกน้ำเพื่อดึงบางสิ่งบางอย่างจากมัน
class SongReportService
def gather_songs_from_artist(artist_id)
songs = Song.where(status: :published)
.where(artist_id: artist_id)
.order(:title)
...
end
end
class SongController < ApplicationController
def index
@songs = Song.where(status: :published)
.order(:release_date)
...
end
end
class SongRefreshJob < ApplicationJob
def perform
songs = Song.where(status: :published)
...
end
end
ในตัวอย่างข้างต้น เรามีกรณีการใช้งานสามกรณีโดยที่ Song
กำลังสอบถามโมเดล ใน SongReporterService
ที่ใช้สำหรับการรายงานข้อมูลเกี่ยวกับเพลง เราพยายามหาเพลงที่เผยแพร่จากศิลปินที่เป็นรูปธรรม จากนั้นในSongController
เราได้รับเพลงที่เผยแพร่และเรียงลำดับตามวันที่เผยแพร่ และสุดท้ายใน SongRefreshJob
เราได้รับเฉพาะเพลงที่เผยแพร่และทำอะไรกับพวกเขา
ทั้งหมดนี้เป็นเรื่องปกติ แต่ถ้าเราตัดสินใจเปลี่ยนชื่อสถานะเป็นreleased
หรือทำการเปลี่ยนแปลงอื่น ๆ ในการดึงเพลง? เราจะต้องไปแก้ไขเหตุการณ์ทั้งหมดแยกกัน นอกจากนี้ รหัสด้านบนไม่ใช่ DRY ซ้ำตัวเองในแอปพลิเคชัน อย่าปล่อยให้สิ่งนี้ทำให้คุณผิดหวัง โชคดีที่มีวิธีแก้ไขปัญหานี้
เราสามารถใช้ ขอบเขต Rails เพื่อทำให้โค้ดนี้แห้ง การกำหนดขอบเขตทำให้คุณสามารถกำหนดคิวรีที่ใช้ทั่วไป ซึ่งสามารถเรียกใช้บนความสัมพันธ์และอ็อบเจ็กต์ ทำให้โค้ดของเราสามารถอ่านและเปลี่ยนแปลงได้ง่ายขึ้น แต่บางทีสิ่งที่สำคัญที่สุดคือขอบเขตนั้นทำให้เราเชื่อมโยงวิธี Active Record อื่น ๆ เช่น joins
, where
ฯลฯ มาดูกันว่าโค้ดของเรามีขอบเขตเป็นอย่างไร
class Song < ApplicationRecord
...
scope :published, -> { where(published: true) }
scope :by_artist, ->(artist_id) { where(artist_id: artist_id) }
scope :sorted_by_title, { order(:title) }
scope :sorted_by_release_date, { order(:release_date) }
...
end
class SongReportService
def gather_songs_from_artist(artist_id)
songs = Song.published.by_artist(artist_id).sorted_by_title
...
end
end
class SongController < ApplicationController
def index
@songs = Song.published.sorted_by_release_date
...
end
end
class SongRefreshJob < ApplicationJob
def perform
songs = Song.published
...
end
end
ไปเลย เราจัดการตัดโค้ดซ้ำแล้วใส่ลงในโมเดล แต่สิ่งนี้ไม่ได้ผลเสมอไปโดยเฉพาะหากคุณได้รับการวินิจฉัยว่าเป็นแบบจำลองอ้วนหรือวัตถุแห่งพระเจ้า การเพิ่มวิธีการและความรับผิดชอบให้กับแบบจำลองมากขึ้นเรื่อยๆ อาจไม่ใช่ความคิดที่ดี
คำแนะนำของฉันที่นี่คือการใช้ขอบเขตให้น้อยที่สุดและแยกเฉพาะการสืบค้นทั่วไปที่นั่นเท่านั้น ในกรณีของเรา อาจเป็น where(published: true)
จะเป็นขอบเขตที่สมบูรณ์แบบเพราะใช้ได้ทุกที่ สำหรับโค้ดอื่นๆ ที่เกี่ยวข้องกับ SQL คุณสามารถใช้สิ่งที่เรียกว่า Repository pattern ได้ มาดูกันว่ามันคืออะไร
รูปแบบพื้นที่เก็บข้อมูล
สิ่งที่เราจะแสดงไม่ใช่รูปแบบพื้นที่เก็บข้อมูล 1:1 ตามที่กำหนดไว้ในหนังสือการออกแบบที่ขับเคลื่อนด้วยโดเมน แนวคิดเบื้องหลังของเราและรูปแบบพื้นที่เก็บข้อมูล Rails คือการแยกตรรกะของฐานข้อมูลออกจากตรรกะทางธุรกิจ นอกจากนี้เรายังสามารถดำเนินการอย่างเต็มที่และสร้างคลาสพื้นที่เก็บข้อมูลที่เรียกใช้ SQL แบบดิบแทนเราแทน Active Record แต่ฉันจะไม่แนะนำสิ่งเหล่านี้เว้นแต่คุณต้องการจริงๆ
สิ่งที่เราทำได้คือสร้าง SongRepository
และใส่ตรรกะของฐานข้อมูลลงไป
class SongRepository
class << self
def find(id)
Song.find(id)
rescue ActiveRecord::RecordNotFound => e
raise RecordNotFoundError, e
end
def destroy(id)
find(id).destroy
end
def recently_published_by_artist(artist_id)
Song.where(published: true)
.where(artist_id: artist_id)
.order(:release_date)
end
end
end
class SongReportService
def gather_songs_from_artist(artist_id)
songs = SongRepository.recently_published_by_artist(artist_id)
...
end
end
class SongController < ApplicationController
def destroy
...
SongRepository.destroy(params[:id])
...
end
end
สิ่งที่เราทำในที่นี้คือ เราแยกตรรกะการสืบค้นออกเป็นคลาสที่ทดสอบได้ นอกจากนี้ โมเดลไม่เกี่ยวข้องกับขอบเขตและตรรกะอีกต่อไป ผู้ควบคุมและรุ่นบางและทุกคนมีความสุข ใช่ไหม ยังมี ActiveRecord ที่ทำการดึงอย่างหนักที่นั่น ในสถานการณ์ของเรา เราใช้ find
ซึ่งสร้างสิ่งต่อไปนี้:
SELECT "songs".* FROM "songs" WHERE "songs"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
วิธีที่ "ถูกต้อง" คือการกำหนดทั้งหมดนี้ไว้ในSongRepository
. อย่างที่ฉันพูดฉันจะไม่แนะนำ คุณไม่ต้องการมันและคุณต้องการควบคุมอย่างเต็มที่ กรณีการใช้งานในการออกจาก Active Record คือคุณต้องการลูกเล่นที่ซับซ้อนภายใน SQL ที่ Active Record ไม่รองรับอย่างง่ายดาย
เมื่อพูดถึง raw SQL และ Active Record ฉันต้องนำเสนอหนึ่งหัวข้อด้วย หัวข้อการย้ายถิ่นและวิธีการดำเนินการอย่างถูกต้อง มาดำน้ำกันเถอะ
การย้ายถิ่น — ใครสน?
ฉันมักจะได้ยินข้อโต้แย้งเมื่อเขียนการโยกย้ายว่าโค้ดในนั้นไม่ควรดีเท่ากับในส่วนที่เหลือของแอปพลิเคชัน และการโต้เถียงนั้นไม่เหมาะกับฉัน ผู้คนมักใช้ข้ออ้างนี้ในการตั้งค่าโค้ดที่มีกลิ่นเหม็นในการจำลอง เนื่องจากโค้ดจะเรียกใช้เพียงครั้งเดียวและถูกลืม บางทีนี่อาจเป็นจริงถ้าคุณทำงานกับคนสองสามคนและทุกคนไม่ซิงค์กันตลอดเวลา
ความเป็นจริงมักจะแตกต่างกัน แอพพลิเคชั่นนี้สามารถใช้งานได้โดยผู้คนจำนวนมากขึ้นโดยไม่รู้ว่าจะเกิดอะไรขึ้นกับส่วนต่าง ๆ ของแอพพลิเคชั่น และถ้าคุณใส่โค้ดแบบใช้ครั้งเดียวที่น่าสงสัยไว้ที่นั่น คุณอาจทำลายสภาพแวดล้อมการพัฒนาของใครบางคนเป็นเวลาสองสามชั่วโมงเนื่องจากสถานะฐานข้อมูลเสียหายหรือเพียงแค่การย้ายถิ่นที่แปลกประหลาด ไม่แน่ใจว่านี่คือการต่อต้านรูปแบบหรือไม่ แต่คุณควรระวัง
จะทำให้การย้ายถิ่นสะดวกขึ้นสำหรับผู้อื่นได้อย่างไร มาดูรายการที่จะทำให้การย้ายข้อมูลง่ายขึ้นสำหรับทุกคนในโครงการ
ตรวจสอบให้แน่ใจว่าคุณมีวิธีการลงเสมอ
คุณไม่มีทางรู้ได้เลยว่าเมื่อไรบางสิ่งจะถูกย้อนกลับ หากการย้ายข้อมูลของคุณไม่สามารถย้อนกลับได้ อย่าลืมเพิ่ม ActiveRecord::IrreversibleMigration
ข้อยกเว้นเช่นนี้:
def down
raise ActiveRecord::IrreversibleMigration
end
พยายามหลีกเลี่ยง Active Record ในการย้ายข้อมูล
แนวคิดในที่นี้คือการลดการพึ่งพาภายนอกให้เหลือน้อยที่สุด ยกเว้นสถานะของฐานข้อมูล ณ เวลาที่ควรดำเนินการย้ายข้อมูล ดังนั้นจะไม่มีการตรวจสอบระเบียนที่ใช้งานอยู่เพื่อทำลาย (หรืออาจบันทึก) วันของคุณ คุณเหลือ SQL ธรรมดา ตัวอย่างเช่น ลองเขียนการย้ายข้อมูลที่จะเผยแพร่เพลงทั้งหมดจากศิลปินบางคน
class UpdateArtistsSongsToPublished < ActiveRecord::Migration[6.0]
def up
execute <<-SQL
UPDATE songs
SET published = true
WHERE artist_id = 46
SQL
end
def down
execute <<-SQL
UPDATE songs
SET published = false
WHERE artist_id = 46
SQL
end
end
หากคุณต้องการ Song
แบบจำลอง ข้อเสนอแนะคือการกำหนดภายในการโยกย้าย ด้วยวิธีนี้ คุณสามารถป้องกันการย้ายข้อมูลของคุณจากการเปลี่ยนแปลงที่อาจเกิดขึ้นในรูปแบบ Active Record จริงภายใน app/models
.แต่ทั้งหมดนี้ดีและหรูหราหรือไม่? ไปที่จุดต่อไปของเรา
แยก Schema Migration ออกจาก Data Migration
เมื่ออ่านคำแนะนำเกี่ยวกับการย้ายข้อมูลของ Rails คุณจะอ่านข้อมูลต่อไปนี้:
การย้ายข้อมูลเป็นคุณลักษณะของ Active Record ที่ช่วยให้คุณพัฒนาสคีมาฐานข้อมูล ล่วงเวลา. แทนที่จะเขียนการแก้ไขสคีมาใน Pure SQL การย้ายข้อมูลจะอนุญาตให้คุณใช้ Ruby DSL เพื่ออธิบายการเปลี่ยนแปลงในตารางของคุณ
ในบทสรุปของคู่มือนี้ ไม่มีการกล่าวถึงการแก้ไขข้อมูลจริงของตารางฐานข้อมูล มีเพียงโครงสร้างเท่านั้น ดังนั้นการที่เราใช้การโยกย้ายปกติเพื่ออัปเดตเพลงในจุดที่สองนั้นไม่ถูกต้องทั้งหมด
หากคุณต้องการทำสิ่งที่คล้ายกันในโปรเจ็กต์ของคุณเป็นประจำ ให้ลองใช้ data_migrate
อัญมณี. เป็นวิธีที่ดีในการแยกการย้ายข้อมูลออกจากการย้ายข้อมูลสคีมา เราสามารถเขียนตัวอย่างก่อนหน้าของเราใหม่ได้อย่างง่ายดายด้วยมัน ในการสร้างการย้ายข้อมูล เราสามารถดำเนินการดังต่อไปนี้:
bin/rails generate data_migration update_artists_songs_to_published
แล้วเพิ่มตรรกะการย้ายข้อมูลที่นั่น:
class UpdateArtistsSongsToPublished < ActiveRecord::Migration[6.0]
def up
execute <<-SQL
UPDATE songs
SET published = true
WHERE artist_id = 46
SQL
end
def down
execute <<-SQL
UPDATE songs
SET published = false
WHERE artist_id = 46
SQL
end
end
ด้วยวิธีนี้ คุณจะเก็บการโยกย้ายสคีมาทั้งหมดไว้ใน db/migrate
ไดเร็กทอรีและการโยกย้ายทั้งหมดที่จัดการกับข้อมูลภายใน db/data
ไดเรกทอรี
ความคิดสุดท้าย
การจัดการกับโมเดลและการทำให้โมเดลเหล่านี้อ่านง่ายใน Rails นั้นเป็นการต่อสู้อย่างต่อเนื่อง หวังว่าในบล็อกโพสต์นี้ คุณจะต้องเห็นข้อผิดพลาดที่เป็นไปได้และวิธีแก้ไขปัญหาทั่วไป รายการรูปแบบและรูปแบบการต่อต้านแบบจำลองนั้นยังห่างไกลจากความสมบูรณ์ในโพสต์นี้ แต่สิ่งเหล่านี้เป็นสิ่งที่โดดเด่นที่สุดที่ฉันพบเมื่อเร็วๆ นี้
หากคุณสนใจรูปแบบ Rails และ anti-patterns เพิ่มเติม โปรดติดตามตอนต่อไปในซีรีส์นี้ ในโพสต์ต่อๆ ไป เราจะพูดถึงปัญหาทั่วไปและวิธีแก้ไขด้านมุมมองและตัวควบคุมของ Rails MVC
ไว้เจอกันใหม่คราวหน้า !
ป.ล. หากคุณต้องการอ่านโพสต์ Ruby Magic ทันทีที่ออกจากสื่อ สมัครรับจดหมายข่าว Ruby Magic ของเราและไม่พลาดแม้แต่โพสต์เดียว!