ActiveRecord - 직렬화가 있는 값 개체

10048 단어 railsoopdddruby
나는 가치 개체를 사랑 해요. 그들은 미친 양으로 프로젝트를 개선하고 정리할 수 있지만 거의 모든 사람이 간단하고 이해하기 쉽습니다. 이해하기 쉬울 뿐만 아니라 실제로 쓰기도 쉽습니다!

하지만 잠깐만요, 대부분의 Value Object 구현은 그저 Plain Ruby Objects일 뿐입니다. 맞나요? ActiveRecord를 사용하면 어떻게 됩니까?

당신이 그것을 좋아하든, 아니면 하드코어 Rails 개발자가 작성한 레거시와 함께 살아야 하는지는 중요하지 않습니다. ActiveRecord에는 Value Objects를 주입할 수 있는 방법이 있습니다. 두 경우 모두 코드의 수명과 도메인 모델을 개선합니다. 가장 간단한 가능성 중 하나를 살펴보겠습니다.

과소평가된 직렬화



사용하기 가장 간단한 방법 중 하나는 serialize 방법입니다. load 메서드를 사용하여 객체에 데이터를 로드하고 dump 를 사용하여 데이터를 db에 덤프합니다. 간단하면서도 강력한 메커니즘입니다. 몇 가지 예를 들어보겠습니다(단순화를 위해 내부 클래스로 구현).

class CreateMeetings < ActiveRecord::Migration[6.0]
  def change
    create_table :meetings do |t|
      t.integer :time_limit, default: 1

      t.timestamps
    end
  end
end

class Meeting < ApplicationRecord
  class TimeLimit
    include Comparable

    def self.dump(time_limit)
      time_limit.to_i
    end

    def self.load(time_limit)
      new(time_limit)
    end

    def initialize(time_limit)
      @time_limit = position.to_i
    end

    def to_i
      time_limit
    end

    def <=>(other)
      to_i <=> other.to_i
    end

    def minimum?
      time_limit == 1
    end

    def maximum?
      time_limit == 60
    end

    def seconds
      time_limit.minutes.seconds
    end

    private

    attr_reader :time_limit
  end

  serialize :time_limit, TimeLimit
end


보시다시피 TimeLimit 값 개체의 구현이 있어 도메인 관련 동작( minimum? , maximum? , seconds 메서드)을 가질 수 있습니다. 그런 다음 serialize 매크로를 사용하여 간단히 ActiveRecord에 등록합니다.

serialize :time_limit, TimeLimit


작동 방식을 살펴보겠습니다.

 :004 > meeting = Meeting.new
 :005 > meeting
 => #<Meeting id: nil, time_limit: #<Meeting::TimeLimit:0x00007f9744443120 @time_limit=1>, created_at: nil, updated_at: nil>
 :006 > meeting.time_limit.minimum?
 => true 


보시다시피 별도의 개체가 사용된 것을 명확하게 볼 수 있습니다. Rails의 마법을 사용하지 않고도 모든 메소드를 직접 호출할 수 있습니다. setter를 사용하여 값을 변경해 보겠습니다.

 :014 > meeting.time_limit = 5
 :015 > meeting
 => #<Meeting id: nil, time_limit: #<Meeting::TimeLimit:0x000056020ecfec50 @time_limit=5>, created_at: nil, updated_at: nil> 


흥미롭지? 우리의 가치를 지닌 새로운 물건! ActiveRecord는 load 메서드를 사용하여 생성합니다. 그럼 저장을 해보자!

 :016 > meeting.save
   (0.1ms)  begin transaction
  Meeting Create (0.5ms)  INSERT INTO "meetings" ("time_limit", "created_at", "updated_at") VALUES (?, ?, ?)  [["time_limit", 5], ["created_at", "2021-04-12 20:32:06.867606"], ["updated_at", "2021-04-12 20:32:06.867606"]]
   (17.9ms)  commit transaction
 => true
 :017 > meeting
 => #<Meeting id: 2, time_limit: #<Meeting::TimeLimit:0x000056020ebf89c8 @time_limit=5>, created_at: "2021-04-12 20:32:06", updated_at: "2021-04-12 20:32:06"> 


올바른 값을 저장하므로 좋습니다! 우리의 dump 메서드를 올바르게 사용한 것으로 나타났습니다. 그러나 이것은 메모리에 있는 동일한 레코드입니다. 대신 db에서 새로 생성된 회의를 가져올 수 있습니다.

 :024 > meeting = Meeting.last
  Meeting Load (0.2ms)  SELECT "meetings".* FROM "meetings" ORDER BY "meetings"."id" DESC LIMIT ?  [["LIMIT", 1]]
 :025 > meeting
 => #<Meeting id: 2, time_limit: #<Meeting::TimeLimit:0x000056020b8a3618 @time_limit=5>, created_at: "2021-04-12 20:32:06", updated_at: "2021-04-12 20:32:06"> 
 :026 > meeting.time_limit.seconds
 => 300 seconds 


이거 너무 멋지죠? 간단하지만 강력한 인터페이스로 마법이 거의 필요하지 않습니다. 이를 통해 Plain Old Ruby Objects를 생성하고 문제 없이 ActiveRecord 기반 엔터티에 삽입할 수 있습니다. 당신의 maximum 논리를 여기저기 퍼뜨릴 필요가 없습니다(당신을 보고 있는 서비스 개체...), 우리는 그것들을 캡슐화할 수 있고 여전히 우리의 사랑하는(싫어하는?) 모델을 사용할 수 있습니다!


"AR 제거"개발자의 경우: 이 메커니즘을 사용하여 문제 없이 레거시 코드에 도메인 모델링을 삽입할 수 있습니다. 루비 덕 타이핑은 당신의 친구입니다:

# using ActiveRecord:
meeting.time_limit.seconds

# using some PORO or w/e:
meeting.time_limit.seconds


예, 차이가 없습니다. 언제든지 리팩토링할 수 있으며 코드는 그대로 유지됩니다. 아름답지 않습니까?


주의를 끌 수 있는 한 가지 언급해야 할 것이 있습니다. 바로 객체 생성 유효성 검사입니다. 예제를 약간 수정해 보겠습니다.

  class TimeLimit
    # ...

    InvalidLimitError = Class.new(StandardError)

    def initialize(time_limit)
      raise InvalidLimitError unless (1..60).include?(time_limit.to_i)

      @time_limit = time_limit.to_i
    end

   # ...


그리고 잘못된 한도를 생성해 보세요.

 :034 > meeting.time_limit = 100
 :035 > meeting.id
 :036 > meeting.time_limit
Traceback (most recent call last):
        3: from app/models/meeting.rb:10:in `load'
        2: from app/models/meeting.rb:10:in `new'
        1: from app/models/meeting.rb:16:in `initialize'
Meeting::TimeLimit::InvalidLimitError (Meeting::TimeLimit::InvalidLimitError)


보시다시피 ActiveRecord는 직렬화된 속성에 대해 지연 로드를 수행하므로 load 메서드는 할당이 아닌 첫 번째 호출 후에 호출됩니다. 이런 종류의 코드를 작성할 때 염두에 두십시오.


유용한 링크:
  • serialize documentation
  • Value Object by Martin Fowler
  • Good Value Object Conventions for Ruby
  • 좋은 웹페이지 즐겨찾기