순서대로라면 callbacks.rb와 clean_logger.rb 를 봐야 할테지만 core_ext가 막 땡겨서 이거 먼저 보기로 했다.
core_ext는 이름에서도 알 수 있듯이 ruby 코어를 확장한 것이다. 루비의 클래스는 열려 있기 때문에 얼마든지 언어가 정의하고 있는 구현을 바꾸거나 추가할 수 있다.
첫번째는 정말 빠져서는 안될 자료구조, Array를 더 확장한다. 우왕국!
array.rb
ActiveSupport::CoreExtensions::Array::Access
Access 모듈은 from과 to를 구현한다. 이거, 진짜 자주 만들어내는 코든데-_-; 이런게 있었네?
이 코드에서는 저 간단한 코드보다도 명확한 주석이 눈에 확 들어온다. 아. 주석은 저렇게 다는거구나.
- # Returns the tail of the array from +position+.
#
# %w( a b c d ).from(0) # => %w( a b c d )
# %w( a b c d ).from(2) # => %w( c d )
# %w( a b c d ).from(10) # => nil
def from(position)
self[position..-1]
end
# Returns the beginning of the array up to +position+.
#
# %w( a b c d ).to(0) # => %w( a )
# %w( a b c d ).to(2) # => %w( a b c )
# %w( a b c d ).to(10) # => %w( a b c d )
def to(position)
self[0..position]
end
ActiveSupport::CoreExtensions::Array::Conversions
Conversions 모듈은 Array를 다른 것으로 바꾸는 일에 집중한다. to_sentence는 배열을 문자열로 만들어 주는데, 어떤 식으로 바꾸는 고 하니, %w(a b c)를 "a, b, and c" 로 바꾸는 식이다. 여기서 b 뒤의 ,를 빼느냐 마느냐의 옵션도 줄 수 있고, and 대신 뭘 쓸 것인가도 옵션으로 줄 수 있다. 물론 이것들은 전부 파라메터로.. 이 메서드는 코드는 좀 복잡해 보이지만, 역시 주석은 세줄로 끝난다. 주석은 이렇게 달아야 된다.-_-
- # Converts the array to a comma-separated sentence where the last element is joined by the connector word. Options:
# * <tt>:connector</tt> - The word used to join the last element in arrays with two or more elements (default: "and")
# * <tt>:skip_last_comma</tt> - Set to true to return "a, b and c" instead of "a, b, and c".
def to_sentence(options = {})
options.assert_valid_keys(:connector, :skip_last_comma)
options.reverse_merge! :connector => 'and', :skip_last_comma => false
options[:connector] = "#{options[:connector]} " unless options[:connector].nil? || options[:connector].strip == '' - case length
when 0
""
when 1
self[0].to_s
when 2
"#{self[0]} #{options[:connector]}#{self[1]}"
else
"#{self[0...-1].join(', ')}#{options[:skip_last_comma] ? '' : ','} #{options[:connector]}#{self[-1]}"
end
end
assert_valid_keys(*valid_keys)는 core_ext/hash에서 확장된 메서드인데, hash가 가진 키 중에 valid_keys 안에 없는 키가 존재하면 ArgumentError를 발생시킨다. 즉, 여기서는 options에 :connector와 skip_last_comma 외의 다른 키가 존재하면 에러를 내는 일을 하는 코드가 첫 줄이다.
그 아래 reverse_merge! 는 똑같은 일을 merge를 이용해서 하려면 아래처럼 되어야 한다.
- options = {:connector => 'and', :skip_last_comma => false}.merge(options)
난 아직 이 코드가 더 눈에 익숙하게 들어오지만, options가 두 번 등장한다는 점에서 reverse_merge!가 좀 더 나을수도 있겠다고 세뇌해야겠다. dry dry ...
그 아래 살짝 내려가 self.included(base) 는 to_s를 재정의할 수 있도록 한다. 코드는 이런 식이다.
- def self.included(base) #:nodoc:
base.class_eval do
alias_method :to_default_s, :to_s
alias_method :to_s, :to_formatted_s
end
end
to_s를 to_default_s로 바꾸고, to_formatted_s를 to_s로 바꾼다. to_formatted_s는 바로 아래에 있다.
- # Converts a collection of elements into a formatted string by calling
# <tt>to_s</tt> on all elements and joining them:
#
# Blog.find(:all).to_formatted_s # => "First PostSecond PostThird Post"
#
# Adding in the <tt>:db</tt> argument as the format yields a prettier
# output:
#
# Blog.find(:all).to_formatted_s(:db) # => "First Post,Second Post,Third Post"
def to_formatted_s(format = :default)
case format
when :db
if respond_to?(:empty?) && self.empty?
"null"
else
collect { |element| element.id }.join(",")
end
else
to_default_s
end
end
역시 주석이 최고. :db 라는 인자를 주면 , 로 join하여 돌려준다. 없으면, 그냥 to_default_s를 쓴다.
결과적으로 보면 :db 라는 format을 구현하기 위해 이렇게 한 것일텐데, super 대신 메서드 이름을 바꾸는 것이 얼마나 장점이 있는가가 조금 의문으로 다가온다. 이 코드를 읽기 않은 레일스 개발자가 Array의 to_s를 잘 못 확장하면 무한루프에 빠지는 등의 문제가 생길 수도 있기 때문이다. 흠. 더 읽다 보면 이해가 되려나?
그 아래 to_xml도 있다. to_xml도 꽤 많은 일을 하는데, 이에 대한 것은 다음에 필요할 때 읽기로 하고 넘어감. (주석도 코드도 많다)
ActiveSupport::CoreExtensions::Array::ExtractOptions
ExtractOptions 모듈은 extract_options! 메서드 하나를 정의한다. 이 메서드는 Array 마지막 아이템이 해시일 경우 그것을 리턴하고, 아니면 빈 해시를 리턴한다. 이 메서드는 레일스 전체에서 48곳에서나!!(2.1.0 현재) 사용되고 있다. 레일스 메서드 인자 관례에서 그만큼 많이 사용되고 있는 것이다.
코드의 길이대비 사용 빈도가 정말 최고가 아닐까? 다른 메서드는 어떤지 궁금하고나. ㅎㅎ
- # Extracts options from a set of arguments. Removes and returns the last
# element in the array if it's a hash, otherwise returns a blank hash.
#
# def options(*args)
# args.extract_options!
# end
#
# options(1, 2) # => {}
# options(1, 2, :a => :b) # => {:a=>:b} - def extract_options!
last.is_a?(::Hash) ? pop : {}
end
ActiveSupport::CoreExtensions::Array::Grouping
Grouping 모듈은 in_groups_of(number, fill_with = nil, &block), split(value = nil, &block) 메서드를 제공한다.
in_groups_of 는 배열 아이템들을 number 씩 묶어 주는 것이다. 같은 개수로 파티셔닝한다고 설명하면 될 것 같다. 이렇게 하고, 혹시 빈 칸이 있다면 fill_with 로 채워준다. 기본값은 nil이므로 nil로 채워지고, false를 전달하면 빈칸을 만들지 않는다. 코드를 보자.
- # Iterates over the array in groups of size +number+, padding any remaining
# slots with +fill_with+ unless it is +false+.
#
# %w(1 2 3 4 5 6 7).in_groups_of(3) {|g| p g}
# ["1", "2", "3"]
# ["4", "5", "6"]
# ["7", nil, nil]
#
# %w(1 2 3).in_groups_of(2, ' ') {|g| p g}
# ["1", "2"]
# ["3", " "]
#
# %w(1 2 3).in_groups_of(2, false) {|g| p g}
# ["1", "2"]
# ["3"]
def in_groups_of(number, fill_with = nil, &block)
if fill_with == false
collection = self
else
# size % number gives how many extra we have;
# subtracting from number gives how many to add;
# modulo number ensures we don't add group of just fill.
padding = (number - size % number) % number
collection = dup.concat([fill_with] * padding)
end
if block_given?
collection.each_slice(number, &block)
else
returning [] do |groups|
collection.each_slice(number) { |group| groups << group }
end
end
end
사실 array의 each_slice가 유사한 일을 한다. 여기서도 보면 알 수 있겠지만 fill_with가 false일 경우는 단순히 collection의, 즉 자기 자신의 each_slice 를 실행한다. each_slice는 padding을 뺀 모든 기능을 한다. 알고보면, in_groups_of는 each_slice 가 하는 일에 padding을 넣는 것만 더 하는 것이다. 흠. dup.과 concat은 잘 안 쓰는 메서드다. 게다가 약간 로직이 들어가는 padding을 만드는 부분을 위해 세 줄이나 주석을 달았다. 흠!
다음은 split(value = nil, &block) 이다. value 혹은 block이 주어짐에 따라 문자열 대신 배열을 split한다!
코드는 inject를 활용하였다. 좀 복잡하지만 한번 보자.
- # Divides the array into one or more subarrays based on a delimiting +value+
# or the result of an optional block.
#
# [1, 2, 3, 4, 5].split(3) # => [[1, 2], [4, 5]]
# (1..10).to_a.split { |i| i % 3 == 0 } # => [[1, 2], [4, 5], [7, 8], [10]]
def split(value = nil, &block)
block ||= Proc.new { |e| e == value }
inject([[]]) do |results, element|
if block.call(element)
results << []
else
results.last << element
end
results
end
end
첫 줄에서 만든 proc 혹은 블럭으로 넘어온 객체를 block.call(element) 해서 그 값이 참이면 split을 해야 하는 것이니 [[]] 에 []을 추가. 그러면 [[], []] 가 되고, 만약 block.call이 false가 되면 .last에 << element를 한다. 아항.
ActiveSupport::CoreExtensions::Array::RandomAccess
RandomAccess 에는 rand 메서드만 덜렁 하나 있다. 배열에서 아무 값이나 하나 임의로 추출하는 것이다. 특별한 것은 없음.
느낀점-_-
주석이 하는 일은 정말 대단하다. 전에 김기사님에게 이렇게 말했던 적이 있다. "코드가 무슨 일을 하는지 알고 보니 정말 잘 읽히더라." 처음엔 주석을 잘 써라라고 가르친다(입문서). 그 다음엔, 코드가 주석을 대신하라고 가르친다(리팩토링 등). 그 후, 내가 요즘 느끼는 것은 바로.. 코드는 구현을 잘 설명할 수 있도록 하고, 구현 외의 부분은 주석으로 설명해라.이다.
말처럼 쉽지는 않겠지만 노력하다 보면 또 내공이 올라가겠지.^^ 프로그래밍 누가 쉽다고 했던가.
이 글은 스프링노트에서 작성되었습니다.

