2012년 12월 25일 화요일

루비에서의 Delegation 방법

이전 글에서는 루비온레일스에서의 delegate 메소드를 사용하는 방법에 대해서 정리했었다.
사실 루비에 대한 delegation 방법을 먼저 알아보는 것이 우선이 되어야 함이 당연하겠지만, 그 나름대로 도움이 되는 것 같다.

Khell 이 2009년에 블로그에 올린 글이 도움이 된다. 루비에서의 Delegation 방법은 두가지가 있다고 한다.

  • 정적인 코드로부터 동적인 코드를 분리하라
  • 상속보다는 composition을 사용하라.

이 두개의 일반적인 개발원칙은 객체지향 프로그래밍 세계에 입문하는 개발자들이 준수해야 할 것들이다. 첫번째 원칙에 대해서는 이의의 여지가 없지만, 두번째 원칙에 대해서는 이의를 제기한다면 그 이유를 아래에 예를 들어 설명해 보겠다.

하나의 heat sensor를 가지는 임의의 Robot 모델이 있다고 가정한다면 아래와 같이 아주 간단한 UML을 작성할 수 있을 것이다.


이 디자인은 몇가지 문제점을 가지고 있다.

  1. heat sensor가 없는 다른 종류의 Robot이 존재할 수 있다는 것이며 이것은 첫번째 원칙에 위배되는 것이다. 
  2. heat sensor에 대한 기능개선이 필요할 때마다 Robot 클래스를 수정해야하는데 이 또한 첫번째 원칙에 위배되는 것이다. 
  3. heat sensor 메소드가 Robot 클래스내로 노출된다는 것이다.

이 클래스에 약간의 기능개선을 추가해 보자.


이것은 상속에 근거해서 다시 디자인한 것인데 이 또한 위에서 언급한 첫번째 문제는 해결할 수는 있지만 heat sensor에 관련된 나머지 두가지 문제는 여전히 해결할 수 없게 된다. 또 다시 기능개선을 해 보자.


바로 이러한 형태의 디자인이 상속보다는 composition에 근거해서 작업한 것이 되며, 비로소 세가지 문제점들을 모두 해결할 수 있게 되는 것이다. 또한 덤으로 새로운 것을 얻게 되는데, 미래에 사용하기 위한 HeatSensor라는 클래스라는 것이다.

그렇다면 `delegation`이란 무엇인가?

여기서 delegation이란 `기능위임` 정도로 번역할 수 있을 것이다. delegation이란 contained part(?)로 기능을 위임하는 과정을 말한다. 마지막 UML을 자세히 보면, VolcanoRobot 클래스는 heat sensor와 관련된 세개의 메소드를 가지게 되는데, 이들은 래퍼(wrapper) 메소드로서의 역할을 하게 된다. 즉, HeatSensor 클래스의 해당 메소드를 호출하는 일 외에는 아무런 추가작업을 하지 않는다는 말이다. 바로 이것이 delegation이 하는 일이라고 보면 될 것이다. VolcanoRobot 클래스의 세가지 메소드는 contained part(delegates:HeatSensor)로 각각의 작업을 위임하게 되다는 것이다. delegation을 composition으로 구현해 줌으로써 위에서 언급한 바와 같이 유연하고 멋진 해결책을 제공할 수 있게 되고 또한 첫번째 개발원칙인 정적인 것으로부터 동적인 코드를 분리할 수 있게 되는 것이다. 그러나 여기서 약간의 댓가를 치려야 하는데, 바로 래퍼(wrapper) 메소드를 구현해 주어야 하며 이 래퍼 메소드 호출을 처리하는데 별도의 시간이 소요된다는 것이다.

루비에서의 delegation 처리방법

설명을 위해서 아래에 코드 예를 보도록 하자.
여기에는 로봇팔과 열감지를 가지고 박스 포장하기, 박스 쌓아두기, 열측정하기와 같은 여러가지 일을 하는 다용도 Robot 클래스를 정의해 놓았다.

class Robot
  def initialize
    @heat_sensor = HeatSensor.new
    @arm = RobotArm.new
  end
 
  def measure_heat(scale="c")
    @heat_sensor.measure(scale)
  end
 
  def stack(boxes_number=1)
    @arm.stack(boxes_number)
  end
 
  def package
    @arm.package
  end
end
 
class HeatSensor
  #Celsius or Fahrenheit scale
  def measure(scale="c")
    t = rand(100)
    t = scale=="c" ? t : t * (9/5)
    puts "Heat is #{t}° #{scale.upcase}"
  end
end
 
class RobotArm
  def stack(boxes_number=1)
    puts "Stacking #{boxes_number} box(es)"
  end
 
  def package
    puts "Packaging"
  end
end
 
robo = Robot.new #=>#, @heat_sensor=#>
robo.stack 2 #=>Stacking 2 box(es)
robo.package #=>Packaging
robo.measure_heat #=> Heat is 59° C

보다시피, Robot 클래스에는 stack, package, measure_heat 세개의 메소드가 정의되어 있고 이들은 contained 객체에 해당하는 메소드를 호출하는 일 외에는 아무런 추가작업을 하지 않는다. 이것은 contained 객체가 많을 때 특히, 번잡스러운 일이 아닐 수 없다. 그러나, 루비에는 이런 경우에 해결방안으로 두가지 라이브러리를 제공해 준다. Forwardable과 Delegate. 각각에 대해서 알아 보도록 한다.

Forwardable Lib

Forwardable lib은 delegation을 지원하는 라이브러리이며 두개의 모듈이 있다. 하나는 `Fo rwardable`,  다른 하나는 `SingleForwardable` 이다.

Forwardable 모듈

Forwardable 모듈은, def_delegatordef_delegators 메소드를 이용해서, 특정 메소드를 지정 객체에 위임하는 방법을 제공해 준다.

def_delegator(obj, method, alias = method) : obj로 위임할 delegate 메소드를 정의한다. alias를 지정하여 delegate 메소드명을 변경할 수 있다.

def_delegators(obj, *methods) : 복수개의 delegator 메소드를 정의하지만 이 때는 alias 기능이 없다.

위의 Robot 클래스를 Forwardable 모듈을 사용해서 변경해 보도록 하자.

require 'forwardable'
class Robot
  # Extending provides class methods
  extend Forwardable
  # Use of  def_delegators
  def_delegators :@arm,:package,:stack
  # Use of  def_delegator
  def_delegator :@heat_sensor, :measure ,:measure_heat
  def initialize
    @heat_sensor = HeatSensor.new
    @arm = RobotArm.new
  end
end
 
class HeatSensor
  #Celsius or Fahrenheit scale
  def measure(scale="c")
    t = rand(100)
    t = scale=="c" ? t : t * (9/5)
    puts "Heat is #{t}° #{scale.upcase}"
  end
end
 
class RobotArm
  def stack(boxes_number=1)
    puts "Stacking #{boxes_number} box(es)"
  end
 
  def package
    puts "Packaging"
  end
end

보다시피 코드가 더 산뜻해졌다.

SingleForwardable 모듈

SingleForwardable 모듈은, def_delegators 메소드를 이용하여, 특정 메소드를 지정 객체에 위임하는 방법을 제공해 준다. 이 모듈은 Forwardable과 비슷해 보이지만, 클래스내에서가 아니라 객체에 대해서 작동한다는 것이 차이점이다.

require "forwardable"
require "date"
date = Date.today #=> #
# Prepare object for delegation
date.extend SingleForwardable #=> #
# Add delegation for Time.now
date.def_delegator :Time, "now","with_time"
puts date.with_time #=>Thu Jan 01 23:03:04 +0200 2009

Delegate Lib

Delegate lib은 기능위임을 제공하는 또다른 라이브러리이다. 두가지 사용 방법이 있다.

DelegateClass 메소드

최상위 DelegateClass 메소드를 이용하면 클래스 상속을 통해서 delegation을 쉽게 할 수 있게 된다. 아래의 예에서, CurrentDate라는 새로운 클래스를 정의하게 되는데, 이 클래스는 date 객체에 기능위임을 하자마자, 현재의 날짜 정보와 몇가지 추가 메소드를 가지게 된다.

require "delegate"
require "date"
# Notice the class definition
class CurrentDate < DelegateClass(Date)
  def initialize
    @date = Date.today
    # Pass the object to be delegated to the superclass. 
    super(@date)
  end
 
  def to_s
    @date.strftime "%Y/%m/%d"
  end
 
  def with_time
    Time.now
  end
end
 
cdate = CurrentDate.new
# Notice how delegation works
# Instead of doing cdate.date.day and defining attr_accessor for the date , i'm doing c.day
puts cdate.day #=>1
puts cdate.month #=>1
puts cdate.year #=>2009
# Testing added methods
# to_s
puts cdate #=> 2009/01/01
puts cdate.with_time #=> Thu Jan 01 23:22:20 +0200 2009

SimpleDelegator 클래스

이것은 변경될 수 있는 객체에 대해서 위임할 수 있도록 해 준다.

require "delegate"
require "date"
today = Date.today #=> #
yesterday = today - 1 #=> #
date = SimpleDelegator.new(today) #=> #
puts date #=>2009-01-01
# Use __setobj__ to change the delegate
date.__setobj__(yesterday)#=> #
puts date #=>2008-12-31

보다시피, 2개의 객체를 만들어 각각 기능위임을 했다.

루비온레일스에 대해서는...

레일스는 `delegate`라는 새로운 기능을 추가해 준다. 즉, delegate 클래스 메소드를 제공해 주어 contained 객체를 자신의 메소드로 쉽게 노출되도록 해 준다. 하나 또는 복수개의 메소드(심볼 또는 문자열로 명시)와 `:to` 옵션에 대상 객체의 이름을 넘겨 주면 된다. 최소한 하나의 메소드와 `:to` 옵션은 반드시 명시해 주어야 한다.

더미 프로젝트를 만든 후에 콘솔을 연다.

$ rails dummy 
  ...
$ cd dummy
$ruby script/console
Loading development environment (Rails 2.2.2)
>> Person = Struct.new(:name, :address)
=> Person
>> class Invoice < Struct.new(:client)
>>   delegate :name, :address, :to => :client
>> end
=> [:name, :address]
>> john_doe = Person.new("John Doe", "Vimmersvej 13")
=> #
>> invoice = Invoice.new(john_doe)
=> #>
>> invoice.name
=> John Doe
>> invoice.address
=>Vimmersvej 13

ActiveRecord에서 효과적으로 사용하는 방법을 알기 위해서 반드시 레일스 API 문서에서 코드 예문을 찾아 보도록 한다.

마지막으로, 마치기 전에, 아래의 레일스 API 문서 중의 delegate 메소드 코드를 보기 바란다. 여기에는 코드에 대한 설명을 코멘트로 추가해 두었다.

class Module
  # Delegate method 
  # It expects an array of arguments that contains the methods to be delegated 
  # and a hash of options
  def delegate(*methods)
    # Pop up the options hash from arguments array
    options = methods.pop
    # Check the availability of the options hash and more specifically the :to option
    # Raises an error if one of them is not there
    unless options.is_a?(Hash) && to = options[:to]
      raise ArgumentError, "Delegation needs a target. Supply an options hash with a :to key as the last argument (e.g. delegate :hello, :to => :greeter)."
    end
 
    # Make sure the :to option follows syntax rules for method names 
    if options[:prefix] == true && options[:to].to_s =~ /^[^a-z_]/
      raise ArgumentError, "Can only automatically set the delegation prefix when delegating to a method."
    end
 
    # Set the real prefix value 
    prefix = options[:prefix] && "#{options[:prefix] == true ? to : options[:prefix]}_"
 
   # Here comes the magic of ruby :) 
   # Reflection techniques are used here:
   # module_eval is used to add new methods on the fly which:
   # expose the contained methods' objects
    methods.each do |method|
      module_eval("def #{prefix}#{method}(*args, &block)\n#{to}.__send__(#{method.inspect}, *args, &block)\nend\n", "(__DELEGATION__)", 1)
    end
  end
end

이글이 루비의 delegation에 대한 개념을 잡는데 조금이나마 도움이 되었기 바라면서 글을 마친다.

Khell에게 감사하며...


참고원문 : Khell's Blog

댓글 없음:

댓글 쓰기