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

클래스 수준 인스턴스 변수의 마법

이전 Ruby Magic에서 .new를 덮어써서 모듈을 클래스에 안정적으로 주입하는 방법을 알아냈습니다. 메서드를 사용하여 메서드를 추가 동작으로 래핑할 수 있습니다.

이번에는 해당 동작을 재사용할 수 있도록 자체 모듈로 추출하여 한 단계 더 나아갑니다. Wrappable 클래스 확장을 처리하는 모듈을 살펴보고 그 과정에서 클래스 수준 인스턴스 변수에 대해 모두 배울 것입니다. 바로 뛰어들자!

Wrappable 소개 모듈

객체가 초기화될 때 모듈로 객체를 래핑하려면 클래스에 사용할 래핑 모델을 알려야 합니다. 간단한 Wrappable 생성부터 시작하겠습니다. wrap을 제공하는 모듈 주어진 모듈을 클래스 속성으로 정의된 배열로 푸시하는 메소드입니다. 또한 new 이전 게시물에서 논의된 방법입니다.

module Wrappable
  @@wrappers = []
 
  def wrap(mod)
    @@wrappers << mod
  end
 
  def new(*arguments, &block)
    instance = allocate
    @@wrappers.each { |mod| instance.singleton_class.include(mod) }
    instance.send(:initialize, *arguments, &block)
    instance
  end
end

클래스에 새 동작을 추가하려면 extend를 사용합니다. . extend 메소드는 주어진 모듈을 클래스에 추가합니다. 그러면 메서드는 클래스 메서드가 됩니다. 이 클래스의 인스턴스를 래핑할 모듈을 추가하려면 이제 wrap 방법.

module Logging
  def make_noise
    puts "Started making noise"
    super
    puts "Finished making noise"
  end
end
 
class Bird
  extend Wrappable
 
  wrap Logging
 
  def make_noise
    puts "Chirp, chirp!"
  end
end

Bird의 새 인스턴스를 만들어 시도해 보겠습니다. make_noise 호출 방법.

bird = Bird.new
bird.make_noise
# Started making noise
# Chirp, chirp!
# Finished making noise

엄청난! 예상대로 작동합니다. 그러나 Wrappable을 사용하여 두 번째 클래스를 확장하면 상황이 약간 이상하게 작동하기 시작합니다. 모듈.

module Powered
  def make_noise
    puts "Powering up"
    super
    puts "Shutting down"
  end
end
 
class Machine
  extend Wrappable
 
  wrap Powered
 
  def make_noise
    puts "Buzzzzzz"
  end
end
 
machine = Machine.new
machine.make_noise
# Powering up
# Started making noise
# Buzzzzzz
# Finished making noise
# Shutting down
 
bird = Bird.new
bird.make_noise
# Powering up
# Started making noise
# Chirp, chirp!
# Finished making noise
# Shutting down

Machine Logging으로 래핑되지 않았습니다. 모듈에서 여전히 로깅 정보를 출력합니다. 더 나쁜 것은 새조차도 이제 전원을 켜고 끌 수 있습니다. 옳지 않겠죠?

이 문제의 근원은 모듈을 저장하는 방식에 있습니다. 클래스 변수 @@wrappables Wrappable에 정의되어 있습니다. 모듈 및 wrap 클래스에 관계없이 새 모듈을 추가할 때마다 사용 에서 사용됩니다.

이것은 Wrappable에 정의된 클래스 변수를 볼 때 더 분명합니다. 모듈 및 BirdMachine 클래스. Wrappable 동안 클래스 메서드가 정의되어 있지만 두 클래스는 그렇지 않습니다.

Wrappable.class_variables # => [:@@wrappers]
Bird.class_variables # => []
Machine.class_variables # => []

이 문제를 해결하려면 인스턴스 변수를 사용하도록 구현을 수정해야 합니다. 그러나 이들은 Bird 인스턴스의 변수가 아닙니다. 또는 Machine 하지만 클래스 자체의 인스턴스 변수입니다.

Ruby에서 클래스는 객체일 뿐입니다.

이것은 확실히 처음에는 약간 정신이 혼미하지만 여전히 이해해야 할 매우 중요한 개념입니다. 클래스는 Class의 인스턴스입니다. 그리고 class Bird; end Bird = Class.new를 작성하는 것과 동일합니다. . 더 혼란스럽게 만들려면 Class Module에서 상속 Object에서 상속 . 결과적으로 클래스와 모듈은 다른 개체와 동일한 메서드를 갖습니다. 클래스에서 사용하는 대부분의 메서드(예:attr_accessor 매크로)는 실제로 Module의 인스턴스 메서드입니다. .

클래스에서 인스턴스 변수 사용

Wrappable을 변경해 보겠습니다. 인스턴스 변수를 사용하는 구현. 좀 더 깔끔하게 유지하기 위해 wrappers를 소개합니다. 배열을 설정하거나 인스턴스 변수가 이미 있는 경우 기존 배열을 반환하는 메서드입니다. wrap도 수정합니다. 및 new 새로운 방법을 활용할 수 있도록 합니다.

module Wrappable
  def wrap(mod)
    wrappers << mod
  end
 
  def wrappers
    @wrappers ||= []
  end
 
  def new(*arguments, &block)
    instance = allocate
    wrappers.each { |mod| instance.singleton_class.include(mod) }
    instance.send(:initialize, *arguments, &block)
    instance
  end
end

모듈과 두 클래스의 인스턴스 변수를 확인할 때 BirdMachine 이제 자체 래핑 모듈 컬렉션을 유지 관리합니다.

Wrappable.instance_variables #=> []
Bird.instance_variables #=> [:@wrappers]
Machine.instance_variables #=> [:@wrappers]

당연히 이것은 이전에 관찰한 문제도 해결합니다. 이제 두 클래스 모두 고유한 개별 모듈로 래핑됩니다.

bird = Bird.new
bird.make_noise
# Started making noise
# Chirp, chirp!
# Finished making noise
 
machine = Machine.new
machine.make_noise
# Powering up
# Buzzzzzz
# Shutting down

상속 지원

이 모든 것은 상속이 도입될 때까지 훌륭하게 작동합니다. 우리는 클래스가 슈퍼클래스에서 래핑 모듈을 상속할 것으로 예상합니다. 그렇다면 확인해 봅시다.

module Flying
  def make_noise
    super
    puts "Is flying away"
  end
end
 
class Pigeon < Bird
  wrap Flying
 
  def make_noise
    puts "Coo!"
  end
end
 
pigeon = Pigeon.new
pigeon.make_noise
# Coo!
# Is flying away

보시다시피 Pigeon 때문에 예상대로 작동하지 않습니다. 또한 자체 포장 모듈 컬렉션을 유지 관리하고 있습니다. Pigeon에 대해 정의된 래핑 모듈은 이치에 맞습니다. Bird에 정의되지 않음 , 정확히 우리가 원하는 것이 아닙니다. 전체 상속 체인에서 모든 래퍼를 가져오는 방법을 알아보겠습니다.

다행스럽게도 Ruby는 Module#ancestors를 제공합니다. 클래스(또는 모듈)가 상속받은 모든 클래스와 모듈을 나열하는 메소드입니다.

Pigeon.ancestors # => [Pigeon, Bird, Object, Kernel, BasicObject]

grep 추가 호출하면 Wrappable로 실제로 확장되는 것을 선택할 수 있습니다. . 먼저 상위 체인의 래퍼로 인스턴스를 래핑하려면 .reverse를 호출합니다. 순서를 뒤집습니다.

Pigeon.ancestors.grep(Wrappable).reverse # => [Bird, Pigeon]

루비의 #=== 방법

Ruby의 마법 중 일부는 #=== (또는 대소문자 같음 ) 방법. 기본적으로 #==처럼 작동합니다. (또는 평등 ) 방법. 그러나 여러 클래스가 #===를 재정의합니다. case에서 다른 동작을 제공하는 메소드 진술. 정규 표현식(#=== #match?와 동일합니다. ) 또는 클래스(#=== #kind_of?와 동일합니다. ) 해당 진술에서. Enumerable#grep와 같은 메소드 , Enumerable#all? , 또는 Enumerable#any? 또한 대소문자 평등 방법에 의존합니다.

이제 flat_map(&:wrappers)를 호출할 수 있습니다. 단일 배열로 상속 체인에 정의된 모든 래퍼 목록을 가져옵니다.

Pigeon.ancestors.grep(Wrappable).reverse.flat_map(&:wrappers) # => [Logging]

남은 것은 inherited_wrappers로 압축하는 것뿐입니다. 모듈을 만들고 새 메서드를 약간 수정하여 wrappers 대신 해당 메서드를 사용합니다. 방법.

module Wrappable
  def inherited_wrappers
    ancestors
      .grep(Wrappable)
      .reverse
      .flat_map(&:wrappers)
  end
 
  def new(*arguments, &block)
    instance = allocate
    inherited_wrappers.each { |mod|instance.singleton_class.include(mod) }
    instance.send(:initialize, *arguments, &block)
    instance
  end
end

최종 테스트 실행은 이제 모든 것이 예상대로 작동하는지 확인합니다. 래핑 모듈은 적용되는 클래스(및 해당 하위 클래스)에만 적용됩니다.

bird = Bird.new
bird.make_noise
# Started making noise
# Chirp, chirp!
# Finished making noise
 
machine = Machine.new
machine.make_noise
# Powering up
# Buzzzzz
# Shutting down
 
pigeon = Pigeon.new
pigeon.make_noise
# Started making noise
# Coo!
# Finished making noise
# Is flying away

이제 끝입니다!

분명히, 이 시끄러운 새들은 약간의 이론적인 예입니다(트윗, 트윗). 그러나 상속 가능한 클래스 인스턴스 변수는 클래스 작동 방식을 이해하는 데만 멋진 것은 아닙니다. 이것은 클래스가 Ruby의 객체일 뿐이라는 좋은 예입니다.

그리고 우리는 상속 가능한 클래스 인스턴스 변수가 실생활에서 매우 유용할 수도 있다는 것을 인정할 것입니다. 예를 들어, 나중에 자체 검사할 수 있는 기능이 있는 모델에서 속성과 관계를 정의하는 것을 생각해 보십시오. 우리에게 마술은 이것을 가지고 놀고 어떻게 작동하는지 더 잘 이해하는 것입니다. 그리고 다음 단계의 솔루션을 위해 마음을 여십시오. 🧙🏼‍♀️

항상 그렇듯이, 이 패턴 또는 유사한 패턴을 사용하여 빌드한 내용을 듣고 싶습니다. Twitter에서 @AppSignal에 짹짹 소리를 내십시오.