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

ActiveSupports #descendants 방법:심층 분석

Rails는 Ruby의 내장 객체에 많은 것을 추가합니다. 이것은 일부 사람들이 Ruby의 "방언"이라고 부르는 것으로 Rails 개발자가 1.day.ago와 같은 줄을 작성할 수 있게 해줍니다. .

이러한 추가 방법의 대부분은 ActiveSupport에 있습니다. 오늘은 ActiveSupport가 Class:descendants에 직접 추가하는 덜 알려진 방법을 살펴보겠습니다. . 이 메서드는 호출된 클래스의 모든 하위 클래스를 반환합니다. 예:ApplicationRecord.descendants 앱에서 상속한 클래스(예:애플리케이션의 모든 모델)를 반환합니다. 이 기사에서는 어떻게 작동하는지, 왜 사용하고 싶은지, Ruby에 내장된 상속 관련 메서드를 어떻게 보완하는지 살펴보겠습니다.

객체 지향 언어의 상속

먼저 Ruby의 상속 모델에 대해 간략히 살펴보겠습니다. 다른 객체 지향(OO) 언어와 마찬가지로 Ruby는 계층 구조 내에 있는 객체를 사용합니다. 클래스를 생성한 다음 해당 클래스의 하위 클래스를 생성한 다음 해당 하위 클래스의 하위 클래스 등을 생성할 수 있습니다. 이 계층을 올라갈 때 우리는 조상 목록을 얻습니다. Ruby에는 모든 엔터티는 객체 자체(클래스, 정수 및 심지어 nil 포함)인 반면, 일부 다른 언어는 일반적으로 성능(예:정수, 이중, 부울 등)을 위해 실제 객체가 아닌 "기본값"을 사용하는 경우가 많습니다. 너를 바라보고 있어, 자바).

Ruby와 모든 OO 언어는 어디에서 메소드를 찾고 어떤 것이 우선하는지 알 수 있도록 조상을 추적해야 합니다.

class BaseClass
  def base
    "base"
  end

  def overridden
    "Base"
  end
end

class SubClass < BaseClass
  def overridden
    "Subclass"
  end
end

여기에서 SubClass.new.overridden 호출 "SubClass" 제공 . 그러나 SubClass.new.base 는 SubClass 정의에 없으므로 Ruby는 각 조상을 살펴보고 어느 것이 메서드를 구현하는지 확인합니다(있는 경우). 단순히 SubClass.ancestors를 호출하여 조상 목록을 볼 수 있습니다. . Rails에서 결과는 다음과 같습니다.

[SubClass,
 BaseClass,
 ActiveSupport::Dependencies::ZeitwerkIntegration::RequireDependency,
 ActiveSupport::ToJsonWithActiveSupportEncoder,
 Object,
 PP::ObjectMixin,
 JSON::Ext::Generator::GeneratorMethods::Object,
 ActiveSupport::Tryable,
 ActiveSupport::Dependencies::Loadable,
 Kernel,
 BasicObject]

우리는 여기에서 이 전체 목록을 분석하지 않을 것입니다. 우리의 목적을 위해 SubClass BaseClass와 함께 맨 위에 있습니다. 그 아래. 또한 BasicObject 맨 아래에 있습니다. 이것은 Ruby의 최상위 개체이므로 항상 스택의 맨 아래에 있습니다.

모듈(일명 '믹스인')

믹스에 모듈을 추가하면 상황이 조금 더 복잡해집니다. 모듈은 클래스 계층 구조의 조상이 아니지만 클래스에 "포함"할 수 있으므로 Ruby는 모듈에서 메서드를 확인할 때 또는 여러 모듈이 포함된 경우 먼저 확인할 모듈을 알아야 합니다. .

일부 언어는 이러한 종류의 "다중 상속"을 허용하지 않지만 Ruby는 한 단계 더 나아가 모듈을 포함할지 또는 앞에 추가할지에 따라 계층 구조에 모듈이 삽입되는 위치를 선택할 수 있도록 합니다.

앞에 모듈

앞에 붙은 모듈은 이름에서 알 수 있듯이 기본적으로 클래스의 모든 메서드를 재정의하여 클래스 앞에 있는 상위 목록에 삽입됩니다. 이것은 또한 원래 클래스의 메소드를 호출하기 위해 앞에 추가된 모듈의 메소드에서 "super"를 호출할 수 있음을 의미합니다.

module PrependedModule
  def test
    "module"
  end

  def super_test
    super
  end
end

# Re-using `BaseClass` from earlier
class SubClass < BaseClass
  prepend PrependedModule

  def test
    "Subclass"
  end

  def super_test
    "Super calls SubClass"
  end
end

SubClass의 조상은 이제 다음과 같습니다.

[PrependedModule,
 SubClass,
 BaseClass,
 ActiveSupport::Dependencies::ZeitwerkIntegration::RequireDependency,
 ...
]

이 새로운 조상 목록을 통해 PrependedModule 이제 Ruby가 SubClass에서 호출하는 모든 메서드를 먼저 찾습니다. . 이 또한 super를 호출하면 PrependedModule 내 , 우리는 SubClass에서 메소드를 호출할 것입니다. :

> SubClass.new.test
=> "module"
> SubClass.new.super_test
=> "Super calls SubClass"

모듈 포함

반면에 포함된 모듈은 상위 항목에 삽입됩니다. 클래스. 따라서 기본 클래스에서 처리할 메서드를 가로채기에 이상적입니다.

class BaseClass
  def super_test
    "Super calls base class"
  end
end

module IncludedModule
  def test
    "module"
  end

  def super_test
    super
  end
end

class SubClass < BaseClass
  include IncludedModule

  def test
    "Subclass"
  end
end

이 배열로 SubClass의 조상은 이제 다음과 같습니다.

[SubClass,
 IncludedModule,
 BaseClass,
 ActiveSupport::Dependencies::ZeitwerkIntegration::RequireDependency,
 ...
]

이제 SubClass가 첫 번째 호출 지점이므로 Ruby는 IncludedModule의 메서드만 실행합니다. SubClass에 없는 경우 . super의 경우 , super에 대한 모든 호출 SubClass에서 IncludedModule로 이동합니다. 먼저 super를 호출하는 동안 IncludedModule BaseClass로 이동합니다. .

다시 말해, 포함된 모듈은 상위 계층에서 하위 클래스와 기본 클래스 사이에 위치합니다. 이는 기본 클래스에서 처리할 메서드를 '가로채기'하는 데 효과적으로 사용할 수 있음을 의미합니다.

> SubClass.new.test
=> "Subclass"
> SubClass.new.super_test
=> "Super calls BaseClass"

이 "명령 체인" 때문에 Ruby는 클래스 조상을 추적해야 합니다. 그러나 그 반대는 사실이 아닙니다. 특정 클래스가 주어지면 Ruby는 자식 또는 "하위 항목"을 추적할 필요가 없습니다. 메서드를 실행하는 데 이 정보가 필요하지 않기 때문입니다.

선조 순서

예리한 독자라면 한 클래스에서 여러 모듈을 사용하는 경우 모듈을 포함(또는 추가)하는 순서가 다른 결과를 생성할 수 있다는 것을 깨달았을 것입니다. 예를 들어 메서드에 따라 이 클래스는 다음과 같습니다.

class SubClass < BaseClass
  include IncludedModule
  include IncludedOtherModule
end

그리고 이 클래스:

class SubClass < BaseClass
  include IncludedOtherModule
  include IncludedModule
end

상당히 다르게 행동할 수 있습니다. 이 두 모듈에 같은 이름의 메서드가 있는 경우 여기의 순서에 따라 어느 것이 우선하는지 결정합니다. super 호출 로 해결될 것입니다. 개인적으로, 특히 모듈이 포함되는 순서와 같은 것에 대해 걱정할 필요가 없도록 이와 같이 서로 겹치는 메서드를 사용하지 않는 것이 좋습니다.

실제 사용

include의 차이점을 아는 것은 좋지만 및 prepend 모듈의 경우 더 실제적인 예가 다른 것을 선택할 때를 보여주는 데 도움이 된다고 생각합니다. 이와 같은 모듈의 주요 사용 사례는 Rails 엔진을 사용하는 것입니다.

아마도 가장 인기 있는 Rails 엔진 중 하나는 devise일 것입니다. 사용 중인 암호 다이제스트 알고리즘을 변경하고 싶지만 먼저 빠른 면책 조항이 있다고 가정해 보겠습니다.

나의 일상적인 모듈 사용은 기본 비즈니스 로직을 유지하는 Rails 엔진의 동작을 사용자 정의하는 것이었습니다. 우리는 우리가 통제하는 코드의 동작을 재정의하고 있습니다. . 물론 모든 Ruby 조각에 동일한 방법을 적용할 수 있습니다. 하지만 외부 코드에 대한 변경 사항이 변경 사항과 호환되지 않을 수 있으므로 제어하지 않는 코드(예:다른 사람이 유지 관리하는 gem에서)를 재정의하지 않는 것이 좋습니다.

Devise의 비밀번호 다이제스트는 여기 Devise::Models::DatabaseAuthenticatable 모듈에서 발생합니다.

  def password_digest(password)
    Devise::Encryptor.digest(self.class, password)
  end

  # and also in the password check:
  def valid_password?(password)
    Devise::Encryptor.compare(self.class, encrypted_password, password)
  end

Devise를 사용하면 자신만의 Devise::Encryptable::Encryptors를 만들어 여기에 사용되는 알고리즘을 사용자 지정할 수 있습니다. , 올바른 방법입니다. 그러나 데모 목적으로 모듈을 사용합니다.

# app/models/password_digest_module
module PasswordDigestModule
  def password_digest(password)
    # Devise's default bcrypt is better for passwords,
    # using sha1 here just for demonstration
    Digest::SHA1.hexdigest(password)
  end

  def valid_password?(password)
    Devise.secure_compare(password_digest(password), self.encrypted_password)
  end
end

begin
  User.include(PasswordDigestModule)
# Pro-tip - because we are calling User here, ActiveRecord will
# try to read from the database when this class is loaded.
# This can cause commands like `rails db:create` to fail.
rescue ActiveRecord::NoDatabaseError, ActiveRecord::StatementInvalid
end

이 모듈을 로드하려면 Rails.application.eager_load!를 호출해야 합니다. 개발 중이거나 Rails 이니셜라이저를 추가하여 파일을 로드합니다. 테스트하여 예상대로 작동하는지 확인할 수 있습니다.

> User.create!(email: "[email protected]", name: "Test", password: "TestPassword")
=> #<User id: 1, name: "Test", created_at: "2021-05-01 02:08:29", updated_at: "2021-05-01 02:08:29", posts_count: nil, email: "[email protected]">
> User.first.valid_password?("TestPassword")
=> true
> User.first.encrypted_password
=> "4203189099774a965101b90b74f1d842fc80bf91"

여기 우리의 경우 둘 다 includeprepend 동일한 결과를 얻을 수 있지만 합병증을 추가해 보겠습니다. 사용자 모델이 자체 password_salt를 구현하면 어떻게 될까요? 메서드이지만 모듈 메서드에서 재정의하고 싶습니다.

class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
  has_many :posts

  def password_salt
    # Terrible way to create a password salt,
    # purely for demonstration purposes
    Base64.encode64(email)[0..-4]
  end
end

그런 다음 자체 password_salt를 사용하도록 모듈을 업데이트합니다. 비밀번호 다이제스트 생성 시 메소드:

  def password_digest(password)
    # Devise's default bcrypt is better for passwords,
    # using sha1 here just for demonstration
    Digest::SHA1.hexdigest(password + "." + password_salt)
  end

  def password_salt
    # an even worse way of generating a password salt
    "salt"
  end

이제 includeprepend 우리가 사용하는 것이 어떤 password_salt Ruby가 실행하는 메소드. prepend 사용 , 모듈이 우선하며 다음을 얻습니다.

> User.last.password_digest("test")
=> "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3.salt"

include를 사용하도록 모듈 변경 대신 User 클래스 구현이 우선 적용됨을 의미합니다.

> User.last.password_digest("test")
=> "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3.dHdvQHRlc3QuY2"

일반적으로 prepend 첫째, 모듈을 작성할 때 하위 클래스처럼 취급하고 모듈의 모든 메소드가 클래스의 버전을 재정의한다고 가정하는 것이 더 쉽다는 것을 알게 되었기 때문입니다. 분명히 이것이 항상 바람직한 것은 아닙니다. 이것이 Ruby에서 include도 제공하는 이유입니다. 옵션.

하위 항목

Ruby가 메소드를 실행할 때 우선 순위를 알기 위해 클래스 조상을 추적하는 방법과 모듈을 통해 이 목록에 항목을 삽입하는 방법을 보았습니다. 그러나 프로그래머로서 모든 클래스의 하위 항목을 반복하는 것이 유용할 수 있습니다. , 도. 여기가 ActiveSupport의 #descendants입니다. 메소드가 들어옵니다. 이 메소드는 매우 짧고 필요한 경우 Rails 외부에서 쉽게 복제할 수 있습니다.

class Class
  def descendants
    ObjectSpace.each_object(singleton_class).reject do |k|
      k.singleton_class? || k == self
    end
  end
end

ObjectSpace는 현재 메모리에 있는 모든 Ruby 객체에 대한 정보를 저장하는 Ruby의 매우 흥미로운 부분입니다. 여기서 자세히 다루지는 않겠지만 응용 프로그램에 클래스가 정의되어 있고 로드된 경우 ObjectSpace에 표시됩니다. ObjectSpace#each_object , 모듈이 전달되면 모듈과 일치하거나 해당 모듈의 하위 클래스인 개체만 반환합니다. 여기서 블록은 또한 최상위 레벨을 거부합니다(예:Numeric.descendants , Numeric가 필요하지 않습니다. 결과에 포함됨).

여기서 무슨 일이 일어나고 있는지 잘 알지 못하더라도 걱정하지 마십시오. 실제로 이해하려면 ObjectSpace에 대해 더 많이 읽어야 할 것입니다. 우리의 목적을 위해 이 메소드가 Class에 있다는 것을 아는 것으로 충분합니다. 하위 클래스 목록을 반환하거나 해당 클래스의 자식, 손자 등의 "가계도"로 생각할 수 있습니다.

실제 #descendants 사용

2018 RailsConf에서 Ryan Laughlin은 '검진'에 대해 이야기했습니다. 비디오는 볼 가치가 있지만 데이터베이스의 모든 행을 주기적으로 실행하고 모델의 유효성 검사를 통과하는지 확인하는 한 가지 아이디어만 추출하겠습니다. 데이터베이스의 얼마나 많은 행이 #valid?를 통과하지 못했는지 놀랄 수 있습니다. 테스트.

그렇다면 문제는 모델 목록을 수동으로 유지 관리하지 않고도 이 검사를 어떻게 구현할 수 있느냐는 것입니다. #descendants 정답입니다:

# Ensure all models are loaded (should not be necessary in production)
Rails.application.load! if Rails.env.development?

ApplicationRecord.descendants.each do |model_class|
  # in the real world you'd want to send this off to background job(s)
  model_class.all.each do |record|
    if !record.valid?
      HoneyBadger.notify("Invalid #{model.name} found with ID: #{record.id}")
    end
  end
end

여기 ApplicationRecord.descendants 표준 Rails 애플리케이션의 모든 모델 목록을 제공합니다. 루프에서 model 클래스입니다(예:User 또는 Product ). 여기서 구현은 매우 기본적이지만 결과는 모든 모델(또는 더 정확하게는 ApplicationRecord의 모든 하위 클래스)을 통해 반복하고 .valid?를 호출합니다. 모든 행에 대해.

결론

대부분의 Rails 개발자에게 모듈은 일반적으로 사용되지 않습니다. 이것은 좋은 이유가 있습니다. 코드를 소유하고 있다면 일반적으로 코드의 동작을 사용자 정의하는 더 쉬운 방법이 있으며, 소유하지 않으면 코드를 소유한 경우 모듈로 동작을 변경할 위험이 있습니다. 그럼에도 불구하고 사용 사례가 있으며 다른 파일에서 클래스를 변경할 수 있을 뿐만 아니라 위치를 선택할 수 있는 옵션도 있다는 것은 Ruby의 유연성에 대한 증거입니다. 조상 체인에서 우리 모듈이 나타납니다.

그런 다음 ActiveSupport가 #ancestors의 역을 제공합니다. #descendants 포함 . 이 방법은 내가 본 한 거의 사용되지 않지만 일단 거기에 있다는 것을 알게 되면 점점 더 많이 사용하게 될 것입니다. 개인적으로 모델 유효성을 확인하는 데 사용했을 뿐만 아니라 attribute_alias를 올바르게 추가하고 있는지 확인하는 사양에도 사용했습니다. 모든 모델에 대한 방법입니다.