西脇.rb & 東灘.rb(第2回)でつくってみたいもの(結果)
感想
いくまではいつものごとく人見知りすぎてお腹いたくなったけど、行ってみたら色々しゃべれて楽しかった。
特に同じ悩みを持つ人としゃべれてよかった。
なかなかもくもく会おすすめ。
あと、やっぱり意識高い人はセンスがいいなと思った。目的もって勉強するのとやみくもに勉強するのでは大分違うんだなと感じた。僕もガンバラナイト
今日やったことまとめ
前回書いたやつ分はおおむね実装できた感じ。
Gem https://rubygems.org/gems/mameconf
github https://rubygems.org/gems/mameconf
一応Gem化したのでご興味ある方はどうぞつかってみて下さいw
git で戻しながら今日やったことの復習
初期バージョン
mameconf.rb
require "mameconf/version" module Mameconf def self.included(base) # include で 特異メソッド足す為にhookでextend # ClassMethods つくって足すのはもう古い? base.extend ClassMethods end module ClassMethods # アクセッサ足すときに呼ばれる特異メソッド def mameconf(name, options={}) # デフォルト値をローカル変数にセット。正直いらない default_value = options[:default] # memeconfを呼ばれたときにクラスコンテキストを開いてインスタンスメソッドを定義する。 # ヒアドキュメントでRUBYって使うのはRailsから拝借 # default_value のinspectをつかっているのは # デフォルト値が Symbol なら :sym # 文字列なら "string" 数値なら 123 と文字列展開 # されるため、今回の用途に都合がよいから。 class_eval <<-RUBY, __FILE__, __LINE__ + 1 def #{name} @#{name} ||= #{default_value.inspect} end def #{name}=(val) @#{name} = val end RUBY end end end
mameconf_spec.rb
require "spec_helper" describe Mameconf do # テスト対象になるクラス class Included include Mameconf end describe "defualt value" do subject do # 実際にincludeしてincludeしたクラスを返す Included.class_eval do mameconf :host, default: "localhost" end Included end it "returns default valeu if not present" do subject.new.host.should eq "localhost" end it "allows override default value" do instance = subject.new instance.host = "google.com" instance.host.should eq "google.com" end end end
to_hashメソッドを追加ver
module ClassMethods # mameconf されたattributeを保持しておくためのアクセサ # インスタンス変数にじゃなくてアクセさにしたのは # インスタンスから self.class.** で呼べるようにするため。 + attr_accessor :mameconf_attr_names + + def mameconf_attr_names + @mameconf_attr_names ||= [] + end + def mameconf(name, options={}) + mameconf_attr_names << name + default_value = options[:default] class_eval <<-RUBY, __FILE__, __LINE__ + 1 def #{name} @@ -16,6 +24,15 @@ module Mameconf def #{name}=(val) @#{name} = val end + + def to_hash + ret = {} + self.class.mameconf_attr_names.each do |attr| + attr_sym = attr.to_sym + ret[attr_sym] = self.__send__(attr_sym) + end + ret + end RUBY end end
describe Mameconf do - class Included - include Mameconf - end - describe "defualt value" do subject do # うっ。インスタンスメソッド足すので毎回afterで消さないといけない事に気づく # 寿命をitに限定させるために Class.new 使う事にする - Included.class_eval do + Class.new do + include Mameconf mameconf :host, default: "localhost" end - Included end it "returns default valeu if not present" do subject.new.host.should eq "localhost" end - it "allows override default value" do # to いるんじゃない?英語得意じゃないのでわからない。。 + it "allows to override default value" do instance = subject.new instance.host = "google.com" instance.host.should eq "google.com" end end + + describe "#to_hash" do + subject do + Class.new do + include Mameconf + + mameconf :host, default: "localhost" + mameconf :port, default: 3337 + end.new + end + + it "returns hash that included default values" do + subject.to_hash.should eq ({ host: "localhost", port: 3337 }) + end + end end
継承したケースのテスト追加
今回は最初から想定していたので、class variable 使わずに実装したのでテストのみ。
+ describe "#inheritance" do + before do + @parent = Class.new do + include Mameconf + + mameconf :host, default: "localhost" + end + + @sub = Class.new(@parent) + end + + it "has mameconf attribute" do + @sub.new.host.should eq "localhost" + end + + context "add new attribute on Sub Class" do + before do + @sub.class_eval do + mameconf :sub, default: "inu" + end + end + + it "has different memory space" do + @sub.new.sub.should eq "inu" + # わざわざ 例外のテストしなくても respond_to? テストするだけでよさそう。 # まっちゃも用意されてそう + expect { + @parent.new.sub + }.to raise_error NoMethodError + end + end + end
nilをセット出来るようにする。
ここまで書いててセッターの作りが甘くて nil がセット出来ない事に気づく。
# nil だと毎回デフォルト値セットしてしまう。。。 "@#{name} ||= #{default_value.inspect}"
セッター呼ばれる時点でフラグ持たしてもいいけど、できたら
@abc = "aa" とかされたときにも対応したい。
色々考えて以下みたいにした
class_eval <<-RUBY, __FILE__, __LINE__ + 1 def #{name} - @#{name} ||= #{default_value.inspect} # 一度でもインスタンス変数として設定されていれば初期化しない + if !instance_variables.include?(:@#{name}) + @#{name} ||= #{default_value.inspect} + end + + @#{name} end
うまく動くかはもう少し使わないとわからない。
initializerを足す
module Mameconf def self.included(base) base.extend ClassMethods + base.__send__(:include, InstanceMethods) + end + + module InstanceMethods + def initialize_mameconf(options={}) + options.each do |key, value| + method = "#{key}=" + + __send__(method, value) if respond_to?(method) + end + end + + def initialize(options={}) + initialize_mameconf(options) + end end
+ + describe "#initialize" do + subject do + Class.new do + include Mameconf + + mameconf :host, default: "localhost" + end + end + + it "can initiate from constructor" do + subject.new(host: "sibainu.com").host.should eq "sibainu.com" + end + + context "override initializer on Sub Class" do + before do + subject.class_eval do + def initialize(options) + # TODO: 本当はココになにも書かず初期化したい。 + # でも多分無理。 + initialize_mameconf(options) + end + end + end + + it "can initiate from constructor" do + subject.new(host: "sibainu.com").host.should eq "sibainu.com" + end + end + end end
足したけど、やっぱりここでSub Class制限なしを実現できなかった。。。多分無理そうかな。。。
Review で tap 教えてもらいその場でリファクタ
def to_hash - ret = {} - self.class.mameconf_attr_names.each do |attr| - attr_sym = attr.to_sym - ret[attr_sym] = self.__send__(attr_sym) - end - ret + {}.tap {|ret| + self.class.mameconf_attr_names.each do |attr| + attr_sym = attr.to_sym + ret[attr_sym] = self.__send__(attr_sym) + end + } end
おぉ。ret とか宣言するのださいなと思いつつ書いてたのでいいこと教えてもらった(`・ω・´)
総評
すごい人のプレゼン聞くのも勉強になるけど、やっぱアウトプット楽しいし勉強になる。
もくもく会おすすめ(しつこいw)