Data 클래스
Data 클래스
값처럼(value-alike) 쓰이는 단순한 클래스를 손쉽게 정의하고 싶다면 Data 클래스가 제격이에요. 필드를 나열하면 그걸로 끝, 바로 값 객체가 만들어져요.
가장 간단한 사용 예시부터 볼게요:
Measure = Data.define(:amount, :unit)
# 위치 인자 생성자가 제공돼요
distance = Measure.new(100, 'km')
#=> #<data Measure amount=100, unit="km">
# 키워드 인자 생성자도 제공돼요
weight = Measure.new(amount: 50, unit: 'kg')
#=> #<data Measure amount=50, unit="kg">
# 객체를 만드는 또 다른 형태:
speed = Measure[10, 'mPh']
#=> #<data Measure amount=10, unit="mPh">
# 키워드 인자로도 동작해요:
area = Measure[amount: 1.5, unit: 'm^2']
#=> #<data Measure amount=1.5, unit="m^2">
# 인자 접근자(accessor)가 제공돼요:
distance.amount #=> 100
distance.unit #=> "km"
만들어진 객체에는 합리적인 == 연산자, to_h 해시 변환, 그리고 패턴 매칭에 쓰이는 deconstruct/deconstruct_keys 정의가 함께 마련돼 있어요.
::define 메서드는 선택적 블록을 받아서, 새로 정의된 클래스의 컨텍스트에서 그 블록을 평가해요. 이 덕분에 추가 메서드를 정의할 수 있죠:
Measure = Data.define(:amount, :unit) do
def <=>(other)
return unless other.is_a?(self.class) && other.unit == unit
amount <=> other.amount
end
include Comparable
end
Measure[3, 'm'] < Measure[5, 'm'] #=> true
Measure[3, 'm'] < Measure[5, 'kg']
# comparison of Measure with Measure failed (ArgumentError)
Data는 멤버 writer나 열거자(enumerator)를 제공하지 않아요. 불변의 원자적인 값들을 담아 두기 위한 것이기 때문이죠. 다만 멤버 중 일부가 가변 클래스라면 Data가 추가로 불변성을 강제하지는 않아요:
Event = Data.define(:time, :weekdays)
event = Event.new('18:00', %w[Tue Wed Fri])
#=> #<data Event time="18:00", weekdays=["Tue", "Wed", "Fri"]>
# #time= 이나 #weekdays= 접근자는 없지만, 변경은 여전히 가능해요:
event.weekdays << 'Sat'
event
#=> #<data Event time="18:00", weekdays=["Tue", "Wed", "Fri", "Sat"]>
비슷한 개념으로 Struct도 있어요. 다만 Struct는 더 컨테이너스러운 API를 제공해서 객체 내용을 바꾸고 열거할 수 있어요.
클래스 메서드
-
define(*symbols) → class— 새Data클래스를 정의해요.measure = Data.define(:amount, :unit) #=> #<Class:0x00007f70c6868498> measure.new(1, 'km') #=> #<data amount=1, unit="km"> # 새 클래스를 상수에 저장하면 #inspect에도 반영되고 더 자연스러워요: Measure = Data.define(:amount, :unit) #=> Measure Measure.new(1, 'km') #=> #<data Measure amount=1, unit="km">멤버가 없는
Data도 괜찮아요. 여러 동질의 데이터 클래스를 정의하는 유용한 기법이 될 수 있어요.class HTTPFetcher Response = Data.define(:body) NotFound = Data.define # ... implementation end이제
HTTPFetcher의 서로 다른 응답 종류들이 일관된 표현을 갖게 돼요. 그리고 패턴 매칭에도 쓰기 편리해요:case fetcher.get(url) in HTTPFetcher::Response(body) # process body variable in HTTPFetcher::NotFound # handle not found case end -
DataClass::members → array_of_symbols— 데이터 클래스의 멤버 이름 배열을 돌려줘요:Measure.members # => [:amount, :unit] -
new(*args) → instance,new(**kwargs) → instance,::[](*args) → instance,::[](**kwargs) → instance—::define으로 정의된 클래스의 생성자들은 위치 인자와 키워드 인자를 모두 받아요.Struct와 달리 모든 인자가 필수이고, 키워드 인자로 변환돼요.Measure = Data.define(:amount, :unit) Measure.new(1, 'km') #=> #<data Measure amount=1, unit="km"> Measure.new(amount: 1, unit: 'km') #=> #<data Measure amount=1, unit="km"> # [] 를 쓴 더 짧은 초기화 Measure[1, 'km'] #=> #<data Measure amount=1, unit="km"> Measure[amount: 1, unit: 'km'] #=> #<data Measure amount=1, unit="km">인자가 빠지면 오류예요:
Measure.new(amount: 1) # in `initialize': missing keyword: :unit (ArgumentError) Measure.new(1) # in `initialize': missing keyword: :unit (ArgumentError)Measure#initialize는 항상 키워드 인자를 받고, 필수 인자 검사는new가 아니라initialize에서 일어나요. 인자를 변환하거나 기본값을 주려고initialize를 재정의할 때 이 점이 중요해요:Measure = Data.define(:amount, :unit) class Measure NONE = Data.define def initialize(amount:, unit: NONE.new) super(amount: Float(amount), unit:) end end Measure.new('10', 'km') # => #<data Measure amount=10.0, unit="km"> Measure.new(10_000) # => #<data Measure amount=10000.0, unit=#<data Measure::NONE>>
인스턴스 메서드
-
self == other → true or false—other가self와 같은 클래스이고 모든 멤버가 같으면 true를 돌려줘요.Measure = Data.define(:amount, :unit) Measure[1, 'km'] == Measure[1, 'km'] #=> true Measure[1, 'km'] == Measure[2, 'km'] #=> false Measure[1, 'km'] == Measure[1, 'm'] #=> false Measurement = Data.define(:amount, :unit) # Measurement와 Measure가 같은 "모양"이어도 # 그 인스턴스들은 절대 같지 않아요 Measure[1, 'km'] == Measurement[1, 'km'] #=> false -
deconstruct → array— 패턴 매칭에 쓰이도록self의 값들을 배열로 돌려줘요.Measure = Data.define(:amount, :unit) distance = Measure[10, 'km'] distance.deconstruct #=> [10, "km"] # 사용 예시 case distance in n, 'km' # 내부적으로 #deconstruct 호출 puts "It is #{n} kilometers away" else puts "Don't know how to handle it" end # prints "It is 10 kilometers away"클래스 확인을 함께 쓰면 이렇게도 돼요:
case distance in Measure(n, 'km') puts "It is #{n} kilometers away" # ... end -
deconstruct_keys(array_of_names_or_nil) → hash— 패턴 매칭에 쓰이도록 이름/값 쌍의 해시를 돌려줘요.Measure = Data.define(:amount, :unit) distance = Measure[10, 'km'] distance.deconstruct_keys(nil) #=> {:amount=>10, :unit=>"km"} distance.deconstruct_keys([:amount]) #=> {:amount=>10} # 사용 예시 case distance in amount:, unit: 'km' # 내부적으로 #deconstruct_keys 호출 puts "It is #{amount} kilometers away" else puts "Don't know how to handle it" end # prints "It is 10 kilometers away" -
eql?(other) → true or false— 두 데이터 항목이Hash의 키로 쓰일 때 사용되는 동등성 검사예요.==와의 미묘한 차이는, 멤버도 각자의eql?메서드로 비교한다는 점이에요.Measure = Data.define(:amount, :unit) Measure[1, 'km'] == Measure[1.0, 'km'] #=> true, 값으로는 같아요 # ...하지만... Measure[1, 'km'].eql? Measure[1.0, 'km'] #=> false, 다른 해시 키를 나타내요 -
hash → integer—Object#hash를 재정의해서 (객체를Hash키로 구분할 때 쓰임) 같은 클래스의 같은 내용을 가진 데이터 객체가 같은hash값을 갖고 같은Hash키를 나타내도록 해요.Measure = Data.define(:amount, :unit) Measure[1, 'km'].hash == Measure[1, 'km'].hash #=> true Measure[1, 'km'].hash == Measure[10, 'km'].hash #=> false Measure[1, 'km'].hash == Measure[1, 'm'].hash #=> false Measure[1, 'km'].hash == Measure[1.0, 'km'].hash #=> false # 구조적으로 비슷해도 같은 해시 키로 보면 안 돼요 Measurement = Data.define(:amount, :unit) Measure[1, 'km'].hash == Measurement[1, 'km'].hash #=> false -
inspect → string(to_s의 별칭) —self의 문자열 표현을 돌려줘요.Measure = Data.define(:amount, :unit) distance = Measure[10, 'km'] p distance # 내부적으로 #inspect 사용 #<data Measure amount=10, unit="km"> puts distance # 내부적으로 #to_s 사용, 같은 표현 #<data Measure amount=10, unit="km"> -
members → array_of_symbols—self의 멤버 이름을 배열로 돌려줘요:distance.members #=> [:amount, :unit] -
to_h → hash,to_h {|name, value| ... } → hash— 데이터 객체의Hash표현을 돌려줘요.Measure = Data.define(:amount, :unit) distance = Measure[10, 'km'] distance.to_h #=> {:amount=>10, :unit=>"km"}Enumerable#to_h처럼 블록을 주면 키-값 쌍을 만들어 해시를 구성해 달라는 뜻이에요:distance.to_h { |name, val| [name.to_s, val.to_s] } #=> {"amount"=>"10", "unit"=>"km"}to_h와 초기화 사이에는 유용한 대칭이 있어요:distance2 = Measure.new(**distance.to_h) #=> #<data Measure amount=10, unit="km"> distance2 == distance #=> true -
with(**kwargs) → instance—self의 얕은 복사본을 돌려줘요. 키워드 인자를 주면 해당 필드 값이 그 키워드 값으로 갱신된 복사본을 만들어요.Data클래스가 멤버로 갖지 않는 키워드를 주면 오류예요.Point = Data.define(:x, :y) origin = Point.new(x: 0, y: 0) up = origin.with(x: 1) right = origin.with(y: 1) up_and_right = up.with(y: 1) p origin # #<data Point x=0, y=0> p up # #<data Point x=1, y=0> p right # #<data Point x=0, y=1> p up_and_right # #<data Point x=1, y=1> out = origin.with(z: 1) # ArgumentError: unknown keyword: :z some_point = origin.with(1, 2) # ArgumentError: expected keyword arguments, got positional arguments
더 알아보기
- Struct — 비슷하지만 더 컨테이너스러운 API를 제공하는 개념