動機
AWS のリソースの多くは key-value の組みをタグとして設定できるようになっているのですが。
例えば EC2 インスタンスを取得する aws-sdk のメソッド Aws::EC2::Client#describe_instances
のレスポンスは次のようになっています。
resp.reservations[0].instances[0].tags #=> Array resp.reservations[0].instances[0].tags[0].key #=> String resp.reservations[0].instances[0].tags[0].value #=> String
つまり辞書形式になっているわけではなく、単に key と value のペアを格納するクラスの配列にすぎません。
実装を紐解いてみると、次のような構造になっていました。
class Tag < Struct.new(:key, :value) end
key から value を引きたい場合、たとえば次のようなコードを書くことになります。
tags = [ Tag.new("foo", 123), Tag.new("bar", 456), Tag.new("baz", 789) ] pp tags.find { |tag| tag.key == "foo" }&.value # => 123 pp tags.find { |tag| tag.key == "bar" }&.value # => 456 pp tags.find { |tag| tag.key == "baz" }&.value # => 789 pp tags.find { |tag| tag.key == "hoge" }&.value # => nil
これがもう少しどうにかならないか、というのが今回のお題です。
クラスで包む
Tag
の配列を内部に持ち、key-value アクセスを容易にするメソッドを用意するパタンです。
class Tags def self.[](tags) new(tags) end def initialize(tags) @tags = tags end def [](key) @tags.find { |tag| tag.key == key }&.value end end pp Tags[tags]["foo"] # => 123 pp Tags[tags]["bar"] # => 456 pp Tags[tags]["baz"] # => 789 pp Tags[tags]["hoge"] # => nil
これは C++ のときに割と好んで利用していた方法です。
#include <algorithm> #include <iostream> #include <string> #include <vector> struct Tag { std::string key; int value; }; class Tags { public: Tags(const std::vector<Tag>& tags) : tags_(tags) {} int operator [] (const std::string& key) { auto tag = std::find_if(tags_.begin(), tags_.end(), [&](const Tag& tag) { return tag.key == key; }); return tag->value; } private: const std::vector<Tag>& tags_; }; int main(int, char*[]) { auto tags = { Tag { "foo", 123 }, Tag { "bar", 456 }, Tag { "baz", 789 } }; std::cout << Tags(tags)["foo"] << std::endl; std::cout << Tags(tags)["bar"] << std::endl; std::cout << Tags(tags)["baz"] << std::endl; }
これは、動的にクラスのメソッドを変更できない環境ではよくある方法と思いますが、Ruby では個々のオブジェクトにも固有のメソッドを追加できることを利用して、tags
オブジェクトにメソッドを追加してみようと思います。
オブジェクトを拡張する (1)
今回はメソッドを定義する Tags
をモジュールとして定義し、Object#extend
を利用して tags
オブジェクトにメソッドを組み込んでいます。
module Tags def [](key) find { |tag| tag.key == key }&.value end end tags.extend(Tags) pp tags["foo"] # => 123 pp tags["bar"] # => 456 pp tags["baz"] # => 789 pp tags["hoge"] # => nil
ただし #[]
を上書きしてしまうため Array
として機能しなくなるというかなり重度な欠点を抱えます。
Array
と被らない名前を選べばよい話ではあるのですが。
pp tags[0] # => nil pp tags[1] # => nil pp tags[2] # => nil
オブジェクトを拡張する (2)
より Ruby ならではの方法といえば、やはり BasicObject#method_missing
のオーバーライド。
アクセスできるキーがメソッドとして記述できる識別子に限られるという制約がありますが、オブジェクトの属性のように扱うことができるのでとても強力です。
module Tags def method_missing(name) key = name.to_s find { |tag| tag.key == key }&.value end end tags.extend(Tags) pp tags.foo # => 123 pp tags.bar # => 456 pp tags.baz # => 789 pp tags.hoge # => nil