Computer >> 컴퓨터 >  >> 프로그램 작성 >> Ruby

Ruby on Rails 모델 패턴 및 안티 패턴

Ruby on Rails Patterns and Anti-patterns 시리즈의 두 번째 게시물에 오신 것을 환영합니다. 지난 블로그 게시물에서 우리는 일반적으로 패턴과 안티 패턴이 무엇인지 살펴보았습니다. 또한 Rails 세계에서 가장 유명한 패턴과 안티 패턴에 대해서도 언급했습니다. 이 블로그 게시물에서는 몇 가지 Rails 모델 안티 패턴 및 패턴을 살펴보겠습니다.

모델에 어려움을 겪고 있다면 이 블로그 게시물이 적합합니다. 모델을 다이어트에 적용하는 과정을 빠르게 진행하고 마이그레이션을 작성할 때 피해야 할 몇 가지 사항을 강력하게 마무리합니다. 바로 들어가 봅시다.

지방 과체중 모델

Rails 애플리케이션을 개발할 때 완전한 Rails 웹사이트든 API이든 사람들은 대부분의 로직을 모델에 저장하는 경향이 있습니다. 지난 블로그 게시물에서 Song의 예를 살펴보았습니다. 많은 일을 한 클래스입니다. 모델에 많은 것을 유지하면 단일 책임 원칙(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 범위를 사용할 수 있습니다. 이 코드를 건조시키십시오. 범위 지정을 사용하면 연결 및 개체에 대해 호출할 수 있는 일반적으로 사용되는 쿼리를 정의할 수 있습니다. 이것은 코드를 읽기 쉽고 변경하기 쉽게 만듭니다. 하지만 가장 중요한 점은 범위를 통해 joins과 같은 다른 Active Record 메서드를 연결할 수 있다는 것입니다. , 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 관련 코드의 경우 저장소 패턴이라는 것을 사용할 수 있습니다. 그것이 무엇인지 알아봅시다.

저장소 패턴

우리가 보여주려는 것은 Domain-Driven Design 책에 정의된 1:1 저장소 패턴이 아닙니다. 우리와 Rails 저장소 패턴의 이면에 있는 아이디어는 비즈니스 논리에서 데이터베이스 논리를 분리하는 것입니다. 우리는 또한 Active Record 대신에 우리를 위해 원시 SQL 호출을 수행하는 저장소 클래스를 만들 수도 있지만 정말로 필요한 경우가 아니면 그런 것을 권장하지 않습니다.

우리가 할 수 있는 일은 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에서 벗어나기 위한 사용 사례는 Active Record에서 쉽게 지원하지 않는 SQL 내부에 몇 가지 복잡한 트릭이 필요하다는 것입니다.

원시 SQL과 Active Record에 대해 이야기하면서 나도 한 가지 주제를 가져와야 합니다. 마이그레이션 주제 및 마이그레이션을 올바르게 수행하는 방법. 자세히 살펴보겠습니다.

이주 — 누가 신경을 쓰나요?

마이그레이션을 작성할 때 코드가 애플리케이션의 나머지 부분만큼 좋지 않아야 한다는 주장을 종종 듣습니다. 그리고 그 주장은 저와 어울리지 않습니다. 사람들은 이 변명을 사용하여 마이그레이션에서 냄새나는 코드를 설정하는 경향이 있습니다. 한 번만 실행되고 잊어버리기 때문입니다. 두 사람과 함께 작업하고 모든 사람이 항상 동기화되지 않는 경우에 해당될 수 있습니다.

현실은 다른 경우가 많습니다. 응용 프로그램은 다른 응용 프로그램 부분에서 어떤 일이 발생하는지 모르는 많은 사람들이 사용할 수 있습니다. 그리고 거기에 의심스러운 일회성 코드를 넣으면 손상된 데이터베이스 상태나 이상한 마이그레이션으로 인해 몇 시간 동안 누군가의 개발 환경을 손상시킬 수 있습니다. 이것이 안티 패턴인지 확실하지 않지만 알고 있어야 합니다.

다른 사람들이 더 편리하게 마이그레이션할 수 있는 방법은 무엇입니까? 프로젝트의 모든 사람이 마이그레이션을 더 쉽게 할 수 있는 목록을 살펴보겠습니다.

항상 다운 방법을 제공해야 합니다.

언제 어떤 것이 롤백될지 알 수 없습니다. 마이그레이션을 되돌릴 수 없는 경우 ActiveRecord::IrreversibleMigration을 발생시켜야 합니다. 다음과 같은 예외:

def down
  raise ActiveRecord::IrreversibleMigration
end

마이그레이션에서 활성 레코드를 피하십시오

여기서 아이디어는 마이그레이션을 실행해야 하는 시점의 데이터베이스 상태를 제외하고 외부 종속성을 최소화하는 것입니다. 따라서 하루를 망치는(또는 저장하는) 활성 레코드 유효성 검사가 없습니다. 일반 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가 꼭 필요한 경우 모델에서 제안은 마이그레이션 내부에 정의하는 것입니다. 그렇게 하면 app/models 내부의 실제 Active Record 모델의 잠재적인 변경으로부터 마이그레이션을 방탄할 수 있습니다. .하지만, 이게 다 괜찮고 멋져? 다음 요점으로 가자.

데이터 마이그레이션과 별도의 스키마 마이그레이션

마이그레이션에 대한 Rails 가이드를 살펴보면 다음 내용을 읽을 수 있습니다.

<블록 인용>

마이그레이션은 데이터베이스 스키마를 발전시킬 수 있는 Active Record의 기능입니다. 시간이 지남에 따라. 스키마 수정을 순수 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 패턴과 안티패턴에 관심이 있다면 시리즈의 다음 기사를 기대해 주세요. 다음 포스트에서 우리는 Rails MVC의 뷰와 컨트롤러 측에 대한 일반적인 문제와 솔루션을 다룰 것입니다.

다음 시간까지, 건배!

추신 Ruby Magic 게시물이 언론에 공개되는 즉시 읽고 싶다면 Ruby Magic 뉴스레터를 구독하고 게시물을 놓치지 마세요!