見出し画像

.make_hash_list_in_arrayと.stdio_coverageをリファクタリングして、ちょっとだけ格好良くしてみました。

別の投稿の記事を書いていたら、ふと表題の二つのメソッドをもう少し
格好良く出来ないかと願望が芽生えてきたので、やってみました。

以前の記事は、こちら。

変更方針:
 呼び出しが、どちらも、
  make_hash_list_in_array(hash_list)
  stdio_coverage(hash_list)
 と関数的でオブジェクト指向っぽくなかったので、
  has_list.make_hash_list_in_array
  hash_list.stdio_coverage
 と使えるようにする。

手にすることが出来る知識:
 例によって泥臭い手法
 refine/usingと云う保険の使い方

Step1.
  make_hash_list_in_array(hash_list)
をどうしたら、
  has_list.make_hash_list_in_array
と使えるようになるのか?

簡単である。レシーバーにメッセージが通るようにすればいい。
hash_listは、
[{fruit: 'apple', price: 300}, {fruit: 'banana', price: 100}, {fruit: 'cherry', price: 500}]
といった、複数のハッシュの要素を持つ配列なので、要は、Arrayクラスにメッセージが通るようにすれば良いことになる。

早速、改造してみる。

require 'stringio'
module TOOLS
  def make_hash_list_in_array                             # 引数listを削除
    ary, *arys = *self                                    # listをselfに変更
    key, *value = *ary.zip(*arys)
    values = value.map { |v| key.zip(v).to_h }
                  .to_a
    if block_given?
      values.map do |h|
        yield(h)
      end
    end
    values
  end

  def stdio_coverage(buf_in = '', buf_out = '')           # 引数aryを削除
    $stdin  = StringIO.new(buf_in)
    $stdout = StringIO.new(buf_out)
    
    each do |h|                                           # レシーバーaryを削除
      yield h, buf_in, buf_out
      
      buf_in  = ''; $stdin.string  = buf_in
      buf_out = ''; $stdout.string = buf_out
    end
    
    $stdin  = STDIN
    $stdout = STDOUT
  end
end

class MyClass
  def initialize
    @asking         = 'あなたは男性ですか?女性ですか?(man/woman)'
    @reply_to_man   = 'さようなら。'
    @reply_to_woman = 'こんにちは。'
  end
  
  def greeting
    your_reply = ask
    my_reply(your_reply)
  end
  
private
  
  def ask
    print "#{@asking} ==> "
    gets.chomp
  end
  
  def my_reply(your_reply)
    if your_reply == 'woman'
      print "#{@reply_to_woman}\n"
    else
      print "#{@reply_to_man}\n"
    end
  end
end
                                                      # ココ重要
class Array; include TOOLS; end                       # ArrayクラスにTOOLSをインクルード

require 'minitest/autorun'
class MyClassTest < MiniTest::Test
  # include TOOLS                                     # includeをコメントアウト
  
  def setup
    @my_class = MyClass.new
  end
  
  def test_greeting
    question  = [
      :question,
      'あなたは男性ですか?女性ですか?(man/woman) ==> ',
      'あなたは男性ですか?女性ですか?(man/woman) ==> '
    ]
    typing    = [:typing,   "man\n", "woman\n"]
    reply     = [:reply,    "さようなら。\n", "こんにちは。\n"]
    list      = [question, typing, reply]
    hash_list = list.make_hash_list_in_array do |h|       # 引数listがレシーバーになる
      h[:exp] = h[:question] + h[:reply]
    end

    hash_list.stdio_coverage do |h, buf_in, buf_out|      # 引数has_listがレシーバーになる
      buf_in << h[:typing]
      @my_class.greeting
      assert_equal h[:exp],
                   buf_out
    end
  end
end

こんな感じになりました。
class Array; include TOOLS; end
の位置が、微妙にブサイク。

以前の記事のコメントは、理解の妨げになるので、削除してあります。

Step2.
ただなぁ。。。許されているとはいえ、こんな大もとのクラスに
こんなことしていいのかなぁ。不安。。。
大した悪さをするはずもないのだけど、何か対策を検討する。

メソッドを削除したり、未定義にしたりと検討したものの再定義の仕掛けが大変そうです。かなりの大工事の予感。

何か抜け道は無いかと探したら、ありました!
  refine
これは定義したいクラス/モジュールに適用すれば、usingを使用したクラス/モジュールの中でだけ、有効になるのだそうです。

消したり、無効にできなければ、使いたい場所でだけ動けばいい。
これ使いましょ。

使い方も簡単そうです。
モジュールをrefineで包んで、includeしていたコードをusingで置き換えてお終いです。楽チン。

require 'stringio'
module TOOLS
  refine Array do                                         # ここにrefineを被せます。
    def make_hash_list_in_array                           # 引数listを削除
      ary, *arys = *self                                  # listをselfに変更
      key, *value = *ary.zip(*arys)
      values = value.map { |v| key.zip(v).to_h }
                    .to_a
      if block_given?
        values.map do |h|
          yield(h)
        end
      end
      values
    end

    def stdio_coverage(buf_in = '', buf_out = '')         # 引数aryを削除
      $stdin  = StringIO.new(buf_in)
      $stdout = StringIO.new(buf_out)

      each do |h|                                         # レシーバーaryを削除
        yield h, buf_in, buf_out

        buf_in  = ''; $stdin.string  = buf_in
        buf_out = ''; $stdout.string = buf_out
      end

      $stdin  = STDIN
      $stdout = STDOUT
    end
  end
end

class MyClass
  def initialize
    @asking         = 'あなたは男性ですか?女性ですか?(man/woman)'
    @reply_to_man   = 'さようなら。'
    @reply_to_woman = 'こんにちは。'
  end

  def greeting
    your_reply = ask
    my_reply(your_reply)
  end

private

  def ask
    print "#{@asking} ==> "
    gets.chomp
  end

  def my_reply(your_reply)
    if your_reply == 'woman'
      print "#{@reply_to_woman}\n"
    else
      print "#{@reply_to_man}\n"
    end
  end
end

require 'minitest/autorun'
class MyClassTest < MiniTest::Test
  using TOOLS                                             # includeをusingに置き換え
  
  def setup
    @my_class = MyClass.new
  end

  def test_greeting
    question  = [
      :question,
      'あなたは男性ですか?女性ですか?(man/woman) ==> ',
      'あなたは男性ですか?女性ですか?(man/woman) ==> '
    ]
    typing    = [:typing,   "mn\n",         "woman\n"]
    reply     = [:reply,    "さようなら。\n", "こんにちは。\n"]
    list      = [question,  typing,         reply]
    hash_list = list.make_hash_list_in_array do |h|       # 引数listがレシーバーになる
      h[:exp] = h[:question] + h[:reply]
    end

    hash_list.stdio_coverage do |h, buf_in, buf_out|      # 引数has_listがレシーバーになる
      buf_in << h[:typing]
      @my_class.greeting
      assert_equal h[:exp],
                   buf_out
    end
  end
end

メソッドの前にレシーバを付けて、オブジェクト指向っぽい作りになりました。ポンコツのわたしには、これで十分満足!

追記 (2022/6/3):お詫びと仕様上の注意
レシーバからメソッドを呼び出す形式にしたため、過去版では、
 exp = 'something message.'
 stdio_coverage do |_, _, buf_out|
  @something_obj.something_method
  assert_equal exp, buf_out
 end
と、引数を持たない使い方を許容していました。
今回の版でこのような使い方をするとエラーになります。
 exp = 'something message.'
 [{}].stdio_coverage do |_, _, buf_out|
  @something_obj.something_method
  assert_equal exp, buf_out
 end
と、[{}]のように空のレシーバーを与えるか、buf_outと比較するexpが必ず存在するはずなので、
 [{ exp: 'something message.' }].stdio_coverage do |h, _, buf_out|
  @something_obj.something_method
  assert_equal h[:exp], buf_out
 end
として、使うようお願いします。
スンマセンです。




この記事が気に入ったらサポートをしてみませんか?