Methods
-
자바나 C처럼 강타입 언어는 컴파일러가 모든 메서드 호출에 대해 수신 객체가 일치하는 메서드를 갖고있는지 확인한다.
- 정적 유형 검사라고 하며 정적 타입 언어가 이를 채택해서 사용함
- 파이썬이나 루비같은 동적 언어들은 컴파일러가 확인하지 않음
- 루비에서는 boilerplate method가 문제가 안되는데 이에 대해 알아보자~
중복 문제
- 99$를 이상을 쓰는 컴퓨터 장비를 찾기
레거시 시스템
- DS(Data Source)라는 이름의 클래스 뒤에 레거시 시스템에 데이터들이 저장되어있다
class DS
def initialize # connect to data source...
def get_cpu_info(workstation_id) # ...
def get_cpu_price(workstation_id) # ...
def get_mouse_info(workstation_id) # ...
def get_mouse_price(workstation_id) # ...
def get_keyboard_info(workstation_id) # ...
def get_keyboard_price(workstation_id) # ...
def get_display_info(workstation_id) # ...
def get_display_price(workstation_id) # ...
# ...and so on
DS#initialize
새 DS object를 만들 때 디비와 연결 된다.- 다른 메서드들(수십 개의)은 워크스테이션 id를 갖고 컴퓨터 부품에 대한 설명과 가격을 반환한다.
ds = DS.new
ds.get_cpu_info(42) # => "2.9 Ghz quad-core"
ds.get_cpu_price(42) # => 120
ds.get_mouse_info(42) # => "Wireless Touch"
ds.get_mouse_price(42) # => 60
Double, Treble,... Trouble
- 각 어플리케이션에 맞는 객체로 DB를 래핑해야한다.
- 즉, 각 컴퓨터가 객체여야 한다.
- 이 객체는 각 구성요소에 대해 단일 메서드를 갖고있고 해당 구성 요소의 가격을 모두 설명하는 문자열을 반환한다.
- 100$이상이면 주의를 끌기위해 별을 붙인다.
#methods/computer/duplicated.rb
class Computer
def initialize(computer_id, data_source)
@id = computer_id
@data_source = data_source
end
def mouse
info = @data_source.get_mouse_info(@id)
price = @data_source.get_mouse_price(@id)
result = "Mouse: #{info} ($#{price})"
return "* #{result}" if price >= 100
result
end
def cpu
info = @data_source.get_cpu_info(@id)
price = @data_source.get_cpu_price(@id)
result = "Cpu: #{info} ($#{price})"
return "* #{result}" if price >= 100
result
end
def keyboard
info = @data_source.get_keyboard_info(@id)
price = @data_source.get_keyboard_price(@id)
result = "Keyboard: #{info} ($#{price})"
return "* #{result}" if price >= 100
result
end
# ...
end
- 중복이 많다.
- 메서드가 많다
- 각 메서드에 대한 테스트를 작성해야한다.
동적 메서드
동적으로 메서드를 호출하고 정의하는 방법과 중복 코드를 제거하는 방법
함수 동적으로 호출하기
class MyClass
def my_method(my_arg)
my_arg * 2
end
end
obj = MyClass.new
obj.my_method(3) # => 6
- 다른 방법으로
MyClass#my_method
대신Object#send
을 사용할 수 있다.
obj.send(:my_method, 3)
- 똑같이 my_method를 호출하지만 send를 사용해서 호출된다.
- send를 사용하면 호출할 메서드를 결정하기 위해 마지막까지 기다릴 수 있다.
- 이를 동적 디스패치라고 한다.
Pry 예시
- Pry는 irb의 좋은 대안 ( command line interpreter )
- Pry 객체는 인터프리터의 설정을 갖고있음 - memory_size나 quite같은
require "pry"
pry = Pry.new
pry.memory_size = 101
pry.memory_size
pry.quiet = true
# => 101
Pry.memory_size # 각 속성의 기본값을 반환하는 메서드도 있다.
- Pry 인스턴스를 설정하려면
Pry#refresh
라는 메서드를 호출하면 된다. - 속성 이름을 새 값에 매핑하는 해시를 사용
pry.refresh(:memory_size => 99, :quiet => false)
pry.memory_size # => 99
pry.quiet # => false
-
refresh는 할 일이 많음
- 각 속성을 (self.memory_size 등) 검토하고 기본값으로 초기화해야하며
- 해시 인수에 동일한 특성에 대한 새 값이 있나 확인 후 있으면 설정
def refresh(options={})
defaults[:memory_size] = Pry.memory_size
self.memory_size = options[:memory_size] if options[:memory_size]
defaults[:quiet] = Pry.quiet
self.quiet = options[:quiet] if options[:quiet]
# same for all the other attributes...
end
- 이 두 줄은 각 속성마다 반복되어야 한다.
def refresh(options={})
defaults = {}
attributes = [ :input, :output, :commands, :print, :quiet,
:exception_handler, :hooks, :custom_completions,
:prompt, :memory_size, :extra_sticky_locals ]
attributes.each do |attribute|
defaults[attribute] = Pry.send attribute
end
# ...
defaults.merge!(options).each do |key, value|
send("#{key}=", value) if respond_to?("#{key}=")
end
true
end
- send로 기본 값을 읽는다.
- 옵션 해시와 merge한다
- memory_size=value 꼴로 call attribute accessor을 사용
- respondto? 는 Pry#memorysize= 이 있으면 true를 반환
Privacy Matters
Object#send
는 private method를 포함하여 모든 메서드를 쓸수 있다.- 이러한 종류의 캡슐화 위반.... 불편하면
public_send
를 사용한다. - 하지만 야생의 루비코드는 이러한 우려를 거의 신경쓰지않음....
- 오히려 많은 루비 프로그래머들이 send를 private method쓰려고 씀...
- 동적 호출 봤고 동적 정의 보자~
메서드를 동적으로 정의하기
class MyClass
define_method :my_method do |my_arg|
my_arg * 3
end
end
obj = MyClass.new
obj.my_method(2) # => 6
require_relative '../test/assertions'
assert_equals 6, obj.my_method(2)
Module#define_method
를 사용, method name 하고 block만 넘기면 된다.Myclass
내에서 실행 되므로 MyClass의 인스턴스 메서드로 정의됨- 동적 메서드라고 한다
- define_method 키워드를 쓰면 런타임에 정의된 메서드의 이름을 결정 가능
Computer 클래스 리팩터링
class Computer
def initialize(computer_id, data_source)
@id = computer_id
@data_source = data_source
end
def mouse
info = @data_source.get_mouse_info(@id)
price = @data_source.get_mouse_price(@id)
result = "Mouse: #{info} ($#{price})"
return "* #{result}" if price >= 100
result
end
def cpu
info = @data_source.get_cpu_info(@id)
price = @data_source.get_cpu_price(@id)
result = "Cpu: #{info} ($#{price})"
return "* #{result}" if price >= 100
result
end
def keyboard
info = @data_source.get_keyboard_info(@id)
price = @data_source.get_keyboard_price(@id)
result = "Keyboard: #{info} ($#{price})"
return "* #{result}" if price >= 100
result
end
# ...
end
Step 1: 다이나믹 디스패치 추가
class Computer
def initialize(computer_id, data_source)
@id = computer_id
@data_source = data_source
end
def mouse
component :mouse
end
def cpu
component :cpu
end
def keyboard
component :keyboard
end
def component(name)
info = @data_source.send "get_#{name}_info", @id
price = @data_source.send "get_#{name}_price", @id
result = "#{name.capitalize}: #{info} ($#{price})"
return "* #{result}" if price >= 100
result
end
end
- 각 메서드를 component 메서드에 위임한다.
- 하지만 아직도 중복이 많다.
Step 2: 동적 메서드 정의
class Computer
def initialize(computer_id, data_source)
@id = computer_id
@data_source = data_source
end
def self.define_component(name)
define_method(name) do
info = @data_source.send "get_#{name}_info", @id
price = @data_source.send "get_#{name}_price", @id
result = "#{name.capitalize}: #{info} ($#{price})"
return "* #{result}" if price >= 100
result
end
end
define_component :mouse
define_component :cpu
define_component :keyboard
end
- 여기서 self는 Computer임
define_component
는 클래스 메서드
Step 3: 인트로스펙션으로 뿌리기
- 최소한의 중복만 있지만 완전히 제거가 가능하다.
define_component
에 대한 호출을 다 지운다.- 인트로스펙션으로 모든 컴포넌트의 이름을 추출한다..
class Computer
def initialize(computer_id, data_source)
@id = computer_id
@data_source = data_source
data_source.methods.grep(/^get_(.*)_info$/) { Computer.define_component $1 }
end
def self.define_component(name)
define_method(name) do
#...
end
end
end
- 블록을 grep에
$1
에 저장 된다. - 중복 코드가 다 사라졌다.
- 요소를 추가하거나 유지 관리할 필요가 없으며 DS에 새 컴포넌트를 추가하면 컴퓨터 클래스에서 자동으로 지원한다
method_missing
- Ghost Method와 Dynamic Proxies...
- 루비에서는 컴파일러가 메서드 정의를 강제하지 않으니 없는 메서드를 호출할 수도있다.
class Lawyer; end
nick = Lawyer.new
nick.talk_simple
❮ NoMethodError: undefined method `talk_simple' for #<Lawyer:0x007f801aa81938>
-
메서드 조회가 어떤식으로 진행되는지 기억하나?
- nick의 클래스로 들어가 인스턴스 메서드를 탐색
- Object로 이동한다음 ... BasicObject로 이동...
- method_missing..
nick.send :method_missing, :my_method
❮ NoMethodError: undefined method `my_method' for #<Lawyer:0x007f801b0f4978>
Overriding method_missing
- unknown messages를 인터셉트하여 override가 가능하다
- 각 메시지는
method_missing
에서 함수 이름과 매개변수 그리고 호출시 불러진 블록을 포함한다.
class Lawyer
def method_missing(method, *args)
puts "You called: #{method}(#{args.join(', ')})"
puts "(You also passed it a block)" if block_given?
end
end
bob = Lawyer.new
bob.talk_simple('a', 'b') do
# a block
end
❮ You called: talk_simple(a, b)
(You also passed it a block)
고스트 메서드
- 유사한 메서드를 많이 정의해야 할 경우 method_missing을 통해 직접 정의를 내리고 호출에 응답할 수 있다.
- 만약 뭔가를 물었는데 이해 못하면 이렇게 하세요 라고 하는 것과 같다.
- 호출자 쪽에선 method_missing에 의해 처리되는 일반 호출처럼 보인다.
- 수신자에선 이에 상응하는 메서드가 없다..
- 이를 고스트메서드라고 한다.
The Hashie Example
- Hashie Gem.... Hashie::Mash ..
-
Mash는 루비의 더 강력한 버전의 OpenStruct class
- 루비의 변수처럼 동작하는 hash같은 객체
- 만약 새 속성이 필요하면 지정만 하면 사용이 가능함
require 'hashie'
icecream = Hashie::Mash.new
icecream.flavor = "strawberry"
icecream.flavor # => "strawberry"
module Hashie
class Mash < Hashie::Hash
def method_missing(method_name, *args, &blk)
return self.[](method_name, &blk) if key?(method_name)
match = method_name.to_s.match(/(.*?)([?=!]?)$/)
case match[2]
when "="
self[match[1]] = args.first
# ...
else
default(method_name, *args, &blk)
end
end
# ...
end
end
- method이름이 =으로 끝나면 method_missing은 속성의 값을 갖고오기 위해 '=' 을 잘라낸 다음 값을 저장.
- 호출된 메서드의 이름이 일치하지 않으면 기본값만 반환한다.
다이나믹 프록시
- 고스트 메서드는 좋긴 한데 어떤 객체들은 거의 전적으로 의존함
- 이런 객체들은 다른 언어로 작성된 객체, 웹서비스 의 래퍼이기도 함
- 메서드 호출을 method_missing을 통해 수집하여 래핑된 객체에 전달한다.
The Ghee Example
- Ghee를 통해 gist에 접근하는 방법
require "ghee"
gh = Ghee.basic_auth("usr", "pwd") # Your GitHub username and password
all_gists = gh.users("nusco").gists
a_gist = all_gists[20]
a_gist.url # => "https://api.github.com/gists/535077"
a_gist.description # => "Spell: Dynamic Proxy"
a_gist.star
- nusco의 gist를 찾아 측정 gist를 선택
- url, desc를 찍고 star를 남김
- github의 api는 gist 외에도 수십 가지 유형의 객체를 노출한다.
- ghee는 이 모든 객체를 지원해야한다
class Ghee
class ResourceProxy
# ...
def method_missing(message, *args, &block)
subject.send(message, *args, &block)
end
def subject
@subject ||= connection.get(path_prefix){|req| req.params.merge!params }.body
end
end
end
- 먼저 어떻게 사용하는지를 알아야함
- github의 각 유형에 Ghee는 하위 클래스를 정의한다. (
Ghee::ResourceProxy
)
class Ghee
module API
module Gists
class Proxy < ::Ghee::ResourceProxy
def star
connection.put("#{path_prefix}/star").status == 204
end
# ...
end
end
end
- 만약 객체의 상태를 변경하는 메서드를 호출할 때 ( star처럼 ) Ghee는 해당 github url http 호출을 한다.
- 하지만 url이나 desc같이 속성에서 읽기만 하는 호출은
Ghee::ResourceProxy#method_missing
에서 끝난다. - missing_method는 메시지를 subject에게 포워드한다.
- subject의 구현을 자세히 보면 이 메서드가 github api http 호출을 한다.
- JSON형식의 github 객체를 받아서 해시 같은 객체로 변환함
- 그럼 다시 method_missing에서 url attribute를 반환함
음 우아하지만 메타프로그래밍이 너무 많이 들어가 있어서 헷갈린다.
- Ghee는 github 객체를 동적 해시로 저장한다. 액세스 할 수있다.
- Ghost Method를 호출하여 이러한 해시 특성을 확인할 수 있다.
-
Ghee는 또한 이러한 해시를 프록시 객체 안에 래핑하여 추가적인 메소드를 통해 더욱 풍부하게 한다.
- 프록시는 특정 코드가 필요한 메소드를 구현 ( star 처럼 )
- url과 같이 데이터만 읽는 메서드를 래핑된 해시에 전달
이러한 2단계 디자인 덕에 코드를 매우 압축적으로 유지한다.
github api 의 변화에 자동 적응
Refactoring the Computer Class (Again)
class Computer
def initialize(computer_id, data_source)
@id = computer_id
@data_source = data_source
end
def method_missing(name)
super if !@data_source.respond_to?("get_#{name}_info")
info = @data_source.send("get_#{name}_info", @id)
price = @data_source.send("get_#{name}_price", @id)
result = "#{name.capitalize}: #{info} ($#{price})"
return "* #{result}" if price >= 100
result
end
end
respondtomissing?
- 만약 컴퓨터에 respond_to? 를 사용하면 ..
cmp = Computer.new(0, DS.new)
cmp.respond_to?(:mouse) # => false
- 문제가 될 수 있음
- 다행히 이를 위한 메커니즘
class Computer
def respond_to_missing?(method, include_private = false)
@data_source.respond_to?("get_#{method}_info") || super
end
end
const_missing
- Rake에서 충돌 가능성이 높은 클래스 이름을 모듈로 변경했다는 것..
- 이름 변경 후 이전 버전, 현재 버전 몽키패치
class Module
def const_missing(const_name)
case const_name
when :Task
Rake.application.const_warning(const_name)
Rake::Task
when :FileTask
Rake.application.const_warning(const_name)
Rake::FileTask
when :FileCreationTask
end
end
end
- 존재하지 않는 상수를 참조할 때 루비는 상수의 이름을 심볼로 전달
- 클래스 이름은 상수이므로 ...
- 더이상 사용되지 않는 클래스 이름을 사용중임을 경고
require 'rake'
task_class = Task
❮ WARNING: Deprecated reference to top-level constant 'Task' found [...] Use --classic-namespace on rake command
or 'require "rake/classic_namespace"' in Rakefile
task_class # => Rake::Task
리팩터링 마무리
- 두 가지 다른 방법으로 풀었다.
- 동적 메서드와 동적 디스패치를 사용
- 고스트 메서드를 사용
빈 슬레이트
- 디스플레이가 작동하지 않는다..
my_computer = Computer.new(42, DS.new).
my_computer.display # => 0
Object.instance_method.grep /^d/ #=> [:dup, display, ...]
- 이미 있으므로 metho_missing에 도달하지 않는다.
BasicObject
- 베이직오브젝트는 소수의 인스턴스 메서드만 있음
im = BasicObject.instance_methods
im # => [:==, :equal?, :!, :!=, :instance_eval, :instance_exec, :__send__, :__id__]
- 슈퍼 클래스를 지정하지 않으면 Object에서 상속되며
- 빈 슬레이트를 원하면 대신 BasicObject에서 직접 상속할 수 있다.
- 하지만 특정 메서드를 제거하는게 더 빠를때도
Removing Methods
Module#undef_method
혹은Module#remove_method
를 사용하여 클래스에서 메서드를 제거할 수 있음undef_method
는 상속된 메서드를 모함한 모든 메서드를 제거함
The Builder Example
- Builder gem은 XML 제너레이터
require 'builder'
xml = Builder::XmlMarkup.new(:target=>STDOUT, :indent=>2)
xml.coder {
xml.name 'Matsumoto', :nickname => 'Matz'
xml.language 'Ruby'
}
#This code produces the following snippet of XML:
❮ <coder>
<name nickname="Matz">Matsumoto</name> <language>Ruby</language>
</coder>
- 빌더는 중첩된 태그, 속성 등을 지원하기위해 ...
- 핵심 아이디어는 이름과 언어와 같은 호출은 모든 호출에 대해 XML태그를 생성하는
XmlMarkup#method_missing
에 의해 처리됨
xml.semester {
xml.class 'Egyptology'
xml.class 'Ornithology'
}
❮ <semester> <class>Egyptology</class> <class>Ornithology</class>
</semester>
XmlMarkup이 Object의 하위 클래스인 경우 충돌하지만 메서드를 제거하는 Blank Slate를 상속함
class BlankSlate
# Hide the method named +name+ in the BlankSlate class. Don't
# hide +instance_eval+ or any method beginning with "__".
def self.hide(name)
# ...
if instance_methods.include?(name._blankslate_as_name) &&
name !~ /^(__|instance_eval$)/
undef_method name
end
end
# ...
instance_methods.each { |m| hide(m) }
end
- 모든 메서드를 다 제거하진 않는다? instance_eval하고 루비에 의해 예약 메소드를 보관 - send
- 제거는 가능하지만 제거 안함
Computer Class 고치기
class Computer < BasicObject
BasicObject에는 response_to?
가 없음
response_to_missing
다시 제거
마무리
- 동적 메서드 및 독적 디스패치로 리팩토링 하였고
- 고스트 메서드로도 수정함,
-
고스트 메서드는 위험할 수 있음
- super를 항상 호출 하고, responsetomissing 재정의 를 하면 방지
- 가끔 버그를 일으킬 수 있음
- 메서드목록에서 반환 안됨
- 하지만 고스트 메서드가 유일한 실행 가능한 선택일수도..
- 메서드가 많거나 런타임에 필요한 메서드 호출을 모를때
- Builder - XML 예제처럼
- 스타일에 따라 다르긴 하겠지만 의심스러우면 동적 방법, 필요하면 고스트