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

Ruby의 숨겨진 보석 - 위임자 및 전달 가능

오늘은 Ruby 표준 라이브러리의 숨겨진 보석을 탐색하면서 위임에 대해 알아보겠습니다.

불행히도 다른 많은 용어와 마찬가지로 이 용어는 수년에 걸쳐 다소 혼란스러워졌으며 사람들마다 다른 의미를 갖습니다. Wikipedia에 따르면:

<블록 인용>

위임은 다른 원래 개체(발신자)의 컨텍스트에서 한 개체(수신자)의 구성원(속성 또는 메서드)을 평가하는 것을 말합니다. 위임은 보내는 객체를 받는 객체에 전달하여 명시적으로 수행할 수 있습니다. 이는 모든 객체 지향 언어로 수행할 수 있습니다. 또는 암시적으로 해당 기능에 대한 언어 지원이 필요한 언어의 회원 조회 규칙에 의해 수행됩니다.

그러나 종종 사람들은 자신을 인수로 전달하지 않고 다른 개체의 해당 메서드를 호출하는 개체를 설명하는 데 이 용어를 사용합니다. 이를 더 정확하게는 "전달"이라고 할 수 있습니다.

그 과정에서 우리는 "위임"을 사용하여 기사의 나머지 부분에서 이 두 패턴을 모두 설명할 것입니다.

위임자

표준 라이브러리의 Delegator를 살펴보고 Ruby에서 위임에 대한 탐색을 시작하겠습니다. 여러 위임 패턴을 제공하는 클래스입니다.

단순 위임자

이 중 가장 쉽고 내가 가장 많이 만난 것은 SimpleDelegator입니다. , 초기화를 통해 제공된 개체를 래핑한 다음 누락된 모든 메서드를 개체에 위임합니다. 이 작업을 살펴보겠습니다.

require 'delegate'
 
User = Struct.new(:first_name, :last_name)
 
class UserDecorator < SimpleDelegator
  def full_name
    "#{first_name} #{last_name}"
  end
end

먼저 require 'delegate'해야 했습니다. SimpleDelegator를 만들려면 우리 코드에서 사용할 수 있습니다. Struct도 사용했습니다. 간단한 User 생성 first_name 클래스 및 last_name 접근자. 그런 다음 UserDecorator를 추가했습니다. full_name 정의 개별 이름 부분을 단일 문자열로 결합하는 방법. 여기가 SimpleDelegator입니다. first_name이(가) 작동하지 않기 때문에 last_name도 아님 현재 클래스에 정의되어 있으면 대신 래핑된 개체에서 호출됩니다.

decorated_user = UserDecorator.new(User.new("John", "Doe"))
decorated_user.full_name
#=> "John Doe"

SimpleDelegator 또한 super로 위임된 메서드를 재정의할 수 있습니다. , 래핑된 개체에서 해당 메서드를 호출합니다. 이 예에서 전체 이름 대신 이니셜만 표시하는 데 사용할 수 있습니다.

class UserDecorator < SimpleDelegator
  def first_name
    "#{super[0]}."
  end
end
decorated_user.first_name
#=> "J."
decorated_user.full_name
#=> "J. Doe"

위임자

위의 예를 읽으면서 UserDecorator가 어떻게 어떤 개체에 위임해야 하는지 알고 계셨습니까? 이에 대한 답은 SimpleDelegator에 있습니다. 의 상위 클래스 - Delegator . 이것은 __getobj__에 대한 구현을 제공하여 사용자 정의 위임 체계를 정의하기 위한 추상 기본 클래스입니다. 및 __setobj__ 위임 대상을 각각 가져오고 설정합니다. 이 지식을 사용하여 SimpleDelegator 자체 버전을 쉽게 구축할 수 있습니다. 데모 목적:

class MyDelegator < Delegator
  attr_accessor :wrapped
  alias_method :__getobj__, :wrapped
 
  def initialize(obj)
    @wrapped = obj
  end
end
 
class UserDecorator < MyDelegator
  def full_name
    "#{first_name} #{last_name}"
  end
end

이것은 SimpleDelegator와 약간 다릅니다. __setobj__를 호출하는 의 실제 구현 initialize에서 방법. 사용자 지정 위임자 클래스에는 필요하지 않으므로 해당 메서드를 완전히 생략했습니다.

이것은 이전 예제와 똑같이 작동해야 합니다. 실제로 그렇습니다:

UserDecorator.superclass
#=> MyDelegator < Delegator
decorated_user = UserDecorator.new(User.new("John", "Doe"))
decorated_user.full_name
#=> "John Doe"

위임 방법

마지막 위임 패턴 Delegate 우리에게 제공하는 것은 다소 이상한 이름의 Object.DelegateClass입니다. 방법. 이것은 특정 클래스에 대한 위임자 클래스를 생성하고 반환하며 다음에서 상속할 수 있습니다.

  class MyClass < DelegateClass(ClassToDelegateTo)
    def initialize
      super(obj_of_ClassToDelegateTo)
    end
  end

이것은 처음에는 특히 상속의 오른쪽에 임의의 Ruby 코드가 포함될 수 있다는 사실이 혼란스러워 보일 수 있지만 실제로는 이전에 탐색한 패턴을 따릅니다. 즉, SimpleDelegator에서 상속하는 것과 유사합니다. .

Ruby의 표준 라이브러리는 이 기능을 사용하여 Tempfile을 정의합니다. 많은 작업을 File에 위임하는 클래스 저장 위치 및 파일 삭제에 관한 몇 가지 특별한 규칙을 설정하는 동안 클래스. 동일한 메커니즘을 사용하여 사용자 정의 Logfile을 설정할 수 있습니다. 다음과 같은 클래스:

class Logfile < DelegateClass(File)
  MODE = File::WRONLY|File::CREAT|File::APPEND
 
  def initialize(basename, logdir = '/var/log')
    # Create logfile in location specified by logdir
    path = File.join(logdir, basename)
    logfile = File.open(path, MODE, 0644)
 
    # This will call Delegator's initialize method, so below this point
    # we can call any method from File on our Logfile instances.
    super(logfile)
  end
end

전달 가능

흥미롭게도 Ruby의 표준 라이브러리는 Forwardable 형식으로 위임을 위한 또 다른 라이브러리를 제공합니다. 모듈 및 해당 def_delegatordef_delegators 방법.

원래 UserDecorator를 다시 작성해 보겠습니다. Forwardable이 있는 예 .

require 'forwardable'
 
User = Struct.new(:first_name, :last_name)
 
class UserDecorator
  extend Forwardable
  def_delegators :@user, :first_name, :last_name
 
  def initialize(user)
    @user = user
  end
 
  def full_name
    "#{first_name} #{last_name}"
  end
end
 
decorated_user = UserDecorator.new(User.new("John", "Doe"))
decorated_user.full_name
#=> "John Doe"

가장 눈에 띄는 차이점은 위임이 method_missing을 통해 자동으로 제공되지 않는다는 것입니다. 하지만 대신 전달하려는 각 메서드에 대해 명시적으로 선언해야 합니다. 이렇게 하면 클라이언트에게 노출하고 싶지 않은 래핑된 개체의 모든 메서드를 "숨길" 수 있습니다. 이는 공개 인터페이스에 대한 더 많은 제어를 제공하며 제가 일반적으로 Forwardable을 선호하는 주된 이유입니다. SimpleDelegator 이상 .

Forwardable의 또 다른 멋진 기능 def_delegator를 통해 위임된 메서드의 이름을 바꾸는 기능입니다. , 원하는 별칭을 지정하는 선택적 세 번째 인수를 허용합니다.

class UserDecorator
  extend Forwardable
  def_delegator :@user, :first_name, :personal_name
  def_delegator :@user, :last_name, :family_name
 
  def initialize(user)
    @user = user
  end
 
  def full_name
    "#{personal_name} #{family_name}"
  end
end

위의 UserDecorator 별칭 personal_name만 노출 및 family_name first_namelast_name 래핑된 User 개체:

decorated_user = UserDecorator.new(User.new("John", "Doe"))
decorated_user.first_name
#=> NoMethodError: undefined method `first_name' for #<UserDecorator:0x000000010f995cb8>
decorated_user.personal_name
#=> "John"

이 기능은 때때로 매우 유용할 수 있습니다. 인터페이스는 비슷하지만 메서드 이름에 대한 기대치가 다른 라이브러리 간에 코드를 마이그레이션하는 것과 같이 과거에 성공적으로 사용했습니다.

표준 라이브러리 외부

표준 라이브러리의 기존 위임 솔루션에도 불구하고 Ruby 커뮤니티는 수년 동안 여러 대안을 개발했으며 다음에는 그 중 두 가지를 살펴보겠습니다.

대리인

Rails의 인기를 고려하면 delegate 방법은 Ruby 개발자가 사용하는 가장 일반적으로 사용되는 위임 형식일 수 있습니다. 다음은 신뢰할 수 있는 이전 UserDecorator를 다시 작성하는 데 사용하는 방법입니다. :

# In a real Rails app this would most likely be a subclass of ApplicationRecord
User = Struct.new(:first_name, :last_name)
 
class UserDecorator
  attr_reader :user
  delegate :first_name, :last_name, to: :user
 
  def initialize(user)
    @user = user
  end
 
  def full_name
    "#{first_name} #{last_name}"
  end
end
 
decorated_user = UserDecorator.new(User.new("John", "Doe"))
decorated_user.full_name
#=> "John Doe"

이것은 Forwardable과 매우 유사합니다. 하지만 extend를 사용할 필요는 없습니다. delegate 이후 Module에 직접 정의됩니다. 따라서 모든 클래스 또는 모듈 본문에서 사용할 수 있습니다(더 좋든 나쁘든 결정). 그러나 delegate 소매에 몇 가지 깔끔한 트릭이 있습니다. 먼저 :prefix가 있습니다. 위임된 메서드 이름에 우리가 위임할 객체의 이름을 접두사로 붙일 옵션입니다. 그래서

delegate :first_name, :last_name, to: :user, prefix: true

user_first_name 생성 및 user_last_name 행동 양식. 또는 사용자 정의 접두사를 제공할 수 있습니다.

delegate :first_name, :last_name, to: :user, prefix: :account

이제 사용자 이름의 다른 부분에 account_first_name으로 액세스할 수 있습니다. 및 account_last_name .

delegate의 또 다른 흥미로운 옵션 :allow_nil 옵션. 우리가 위임한 객체가 현재 nil인 경우 —예를 들어 설정되지 않은 ActiveRecord 때문에 관계 - 일반적으로 NoMethodError로 끝납니다. :

decorated_user = UserDecorator.new(nil)
decorated_user.first_name
#=> Module::DelegationError: UserDecorator#first_name delegated to @user.first_name, but @user is nil

그러나 :allow_nil 옵션을 선택하면 이 호출은 성공하고 nil을 반환합니다. 대신:

class UserDecorator
  delegate :first_name, :last_name, to: :user, allow_nil: true
 
  ...
end
 
decorated_user = UserDecorator.new(nil)
decorated_user.first_name
#=> nil

캐스팅

마지막으로 살펴볼 위임 옵션은 Jim Gay의 Casting입니다. 개발자가 "Ruby에서 메서드를 위임하고 자체를 보존"할 수 있는 gem. 이것은 Ruby의 동적 특성을 사용하여 메서드 호출 수신자를 일시적으로 다시 바인딩하기 때문에 위임의 엄격한 정의에 가장 가깝습니다.

UserDecorator.instance_method(:full_name).bind(user).call
#=> "John Doe"

이것의 가장 흥미로운 측면은 개발자가 상위 클래스 계층 구조를 변경하지 않고 개체에 동작을 추가할 수 있다는 것입니다.

require 'casting'
 
User = Struct.new(:first_name, :last_name)
 
module UserDecorator
  def full_name
    "#{first_name} #{last_name}"
  end
end
 
user = User.new("John", "Doe")
user.extend(Casting::Client)
user.delegate(:full_name, UserDecorator)

여기에서 user를 확장했습니다. Casting::Client 사용 , delegate에 대한 액세스 권한을 제공합니다. 방법. 또는 include Casting::Client를 사용할 수도 있습니다. User 내부 모든 인스턴스에 이 기능을 부여하는 클래스입니다.

또한 Casting 블록의 수명 동안 또는 수동으로 다시 제거할 때까지 일시적으로 동작을 추가하는 옵션을 제공합니다. 이것이 작동하려면 먼저 누락된 메서드의 위임을 활성화해야 합니다.

user.delegate_missing_methods

단일 블록 기간 동안 동작을 추가하려면 Casting을 사용할 수 있습니다. 의 delegating 클래스 메서드:

Casting.delegating(user => UserDecorator) do
  user.full_name #=> "John Doe"
end
 
user.full_name
#NoMethodError: undefined method `full_name' for #<struct User first_name="John", last_name="Doe">

또는 명시적으로 uncast를 호출할 때까지 동작을 추가할 수 있습니다. 다시:

user.cast_as(UserDecorator)
user.full_name
#=> "John Doe"
user.uncast
NoMethodError: undefined method `full_name' for #<struct User first_name="John", last_name="Doe">

제시된 다른 솔루션보다 약간 더 복잡하지만 Casting 많은 제어 기능을 제공하며 Jim은 Clean Ruby 책에서 다양한 용도와 그 이상을 보여줍니다.

요약

위임 및 메서드 전달은 관련 개체 간의 책임을 나누는 데 유용한 패턴입니다. 일반 Ruby 프로젝트에서 두 DelegatorForwardable Rails 코드는 delegate 쪽으로 끌리는 경향이 있습니다. 방법. 위임된 항목에 대한 최대 제어를 위해 Casting gem은 다른 솔루션보다 약간 더 복잡하지만 탁월한 선택입니다.

객원 작가 Michael Kohl의 Ruby 사랑은 2003년 경에 시작되었습니다. 그는 또한 언어에 대해 글을 쓰고 말하기를 즐기며 Bangkok.rb와 RubyConf Thailand를 공동 주최합니다.