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

Ruby on Rails Model Patterns และ Anti-patterns

ยินดีต้อนรับกลับสู่โพสต์ที่สองในชุด 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 ของเราและไม่พลาดแม้แต่โพสต์เดียว!