西脇.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)