오늘은 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_delegator
및 def_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_name
및 last_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 프로젝트에서 두 Delegator
및 Forwardable
Rails 코드는 delegate
쪽으로 끌리는 경향이 있습니다. 방법. 위임된 항목에 대한 최대 제어를 위해 Casting
gem은 다른 솔루션보다 약간 더 복잡하지만 탁월한 선택입니다.
객원 작가 Michael Kohl의 Ruby 사랑은 2003년 경에 시작되었습니다. 그는 또한 언어에 대해 글을 쓰고 말하기를 즐기며 Bangkok.rb와 RubyConf Thailand를 공동 주최합니다.