├── .ruby-version ├── .github ├── dependabot.yml └── workflows │ └── ruby.yml ├── Rakefile ├── 00_setup ├── Rakefile ├── 01_try_out.rb └── test │ └── test_try_out.rb ├── 03_method ├── Rakefile ├── 01_method_first_step.rb ├── 02_define.rb ├── test │ ├── test_method_first_step.rb │ ├── test_try_over3_3.rb │ └── test_define.rb └── 03_try_over3_3.rb ├── 04_block ├── Rakefile ├── test │ ├── test_block_first_step.rb │ ├── test_simple_bot.rb │ └── test_evil_mailbox.rb ├── 01_block_first_step.rb ├── 03_simple_bot.rb └── 02_evil_mailbox.rb ├── 02_object_model ├── Rakefile ├── 01_hoge.rb ├── test │ ├── test_hoge.rb │ └── test_hierarchy.rb └── 02_hierarchy.rb ├── 05_class_definition ├── Rakefile ├── 02_simple_mock.rb ├── 01_class_definition_first_step.rb └── test │ ├── test_class_definition_first_step.rb │ └── test_simple_mock.rb ├── 06_codes_generate_codes ├── Rakefile ├── 01_simple_model.rb └── test │ └── test_simple_model.rb ├── Gemfile ├── test └── test_helper.rb ├── Gemfile.lock ├── LICENSE ├── answers ├── 02_object_model │ ├── 01_hoge.rb │ └── 02_hierarchy.rb ├── 04_block │ ├── 02_evil_mailbox.rb │ ├── 01_block_first_step.rb │ └── 03_simple_bot.rb ├── 00_setup │ └── 01_try_out.rb ├── 03_method │ ├── 01_method_first_step.rb │ ├── 02_define.rb │ └── 03_try_over3_3.rb ├── 05_class_definition │ ├── 01_class_definition_first_step.rb │ └── 02_simple_mock.rb └── 06_codes_generate_codes │ └── 01_simple_model.rb └── README.md /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.4.3 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | 8 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "rake/testtask" 2 | 3 | Rake::TestTask.new(:test) do |t| 4 | t.libs << "test" 5 | t.test_files = FileList["**/test_*.rb"] 6 | end 7 | 8 | task :default => :test 9 | -------------------------------------------------------------------------------- /00_setup/Rakefile: -------------------------------------------------------------------------------- 1 | require "rake/testtask" 2 | 3 | Rake::TestTask.new(:test) do |t| 4 | t.libs << "test" 5 | t.libs << "../test" 6 | t.test_files = FileList["test/**/test_*.rb"] 7 | end 8 | 9 | task :default => :test 10 | -------------------------------------------------------------------------------- /03_method/Rakefile: -------------------------------------------------------------------------------- 1 | require "rake/testtask" 2 | 3 | Rake::TestTask.new(:test) do |t| 4 | t.libs << "test" 5 | t.libs << "../test" 6 | t.test_files = FileList["test/**/test_*.rb"] 7 | end 8 | 9 | task :default => :test 10 | -------------------------------------------------------------------------------- /04_block/Rakefile: -------------------------------------------------------------------------------- 1 | require "rake/testtask" 2 | 3 | Rake::TestTask.new(:test) do |t| 4 | t.libs << "test" 5 | t.libs << "../test" 6 | t.test_files = FileList["test/**/test_*.rb"] 7 | end 8 | 9 | task :default => :test 10 | -------------------------------------------------------------------------------- /02_object_model/Rakefile: -------------------------------------------------------------------------------- 1 | require "rake/testtask" 2 | 3 | Rake::TestTask.new(:test) do |t| 4 | t.libs << "test" 5 | t.libs << "../test" 6 | t.test_files = FileList["test/**/test_*.rb"] 7 | end 8 | 9 | task :default => :test 10 | -------------------------------------------------------------------------------- /05_class_definition/Rakefile: -------------------------------------------------------------------------------- 1 | require "rake/testtask" 2 | 3 | Rake::TestTask.new(:test) do |t| 4 | t.libs << "test" 5 | t.libs << "../test" 6 | t.test_files = FileList["test/**/test_*.rb"] 7 | end 8 | 9 | task :default => :test 10 | -------------------------------------------------------------------------------- /06_codes_generate_codes/Rakefile: -------------------------------------------------------------------------------- 1 | require "rake/testtask" 2 | 3 | Rake::TestTask.new(:test) do |t| 4 | t.libs << "test" 5 | t.libs << "../test" 6 | t.test_files = FileList["test/**/test_*.rb"] 7 | end 8 | 9 | task :default => :test 10 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } 6 | 7 | gem "minitest", "~> 5.13" 8 | gem "minitest-reporters" 9 | 10 | gem "rake", "~> 13.0" 11 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | require "minitest/reporters" 3 | Minitest::Reporters.use! 4 | paths = ENV["CI"] ? "../../answers/[0-9]*" : "../../[0-9]*" 5 | Dir.glob(File.expand_path(paths, __FILE__)).each { |path| $LOAD_PATH.unshift(path) } 6 | -------------------------------------------------------------------------------- /.github/workflows/ruby.yml: -------------------------------------------------------------------------------- 1 | name: Ruby 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | env: 9 | CI: true 10 | steps: 11 | - uses: actions/checkout@v6 12 | - uses: ruby/setup-ruby@v1 13 | - name: Build and test with Rake 14 | run: | 15 | bundle install --jobs 4 --retry 3 16 | bundle exec rake 17 | -------------------------------------------------------------------------------- /02_object_model/01_hoge.rb: -------------------------------------------------------------------------------- 1 | # Q1. 2 | # Hogeクラスは次の仕様を持つ 3 | # "hoge" という文字列の定数Hogeを持つ 4 | # "hoge" という文字列を返すhogehogeメソッドを持つ 5 | # HogeクラスのスーパークラスはStringである 6 | # 自身が"hoge"という文字列である時(HogeクラスはStringがスーパークラスなので、当然自身は文字列である)、trueを返すhoge?メソッドが定義されている 7 | 8 | class Hoge 9 | end 10 | 11 | # Q2. 12 | # 次に挙げるクラスのいかなるインスタンスからも、hogeメソッドが呼び出せるようにする 13 | # それらのhogeメソッドは、全て"hoge"という文字列を返す 14 | # - String 15 | # - Integer 16 | # - Numeric 17 | # - Class 18 | # - Hash 19 | # - TrueClass 20 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | ansi (1.5.0) 5 | builder (3.3.0) 6 | minitest (5.25.5) 7 | minitest-reporters (1.7.1) 8 | ansi 9 | builder 10 | minitest (>= 5.0) 11 | ruby-progressbar 12 | rake (13.2.1) 13 | ruby-progressbar (1.13.0) 14 | 15 | PLATFORMS 16 | ruby 17 | 18 | DEPENDENCIES 19 | minitest (~> 5.13) 20 | minitest-reporters 21 | rake (~> 13.0) 22 | 23 | BUNDLED WITH 24 | 2.4.1 25 | -------------------------------------------------------------------------------- /00_setup/01_try_out.rb: -------------------------------------------------------------------------------- 1 | class TryOut 2 | # このクラスの仕様 3 | # コンストラクタは、2つまたは3つの引数を受け付ける。引数はそれぞれ、ファーストネーム、ミドルネーム、ラストネームの順で、ミドルネームは省略が可能。 4 | # full_nameメソッドを持つ。これは、ファーストネーム、ミドルネーム、ラストネームを半角スペース1つで結合した文字列を返す。ただし、ミドルネームが省略されている場合に、ファーストネームとラストネームの間には1つのスペースしか置かない 5 | # first_name=メソッドを持つ。これは、引数の内容でファーストネームを書き換える。 6 | # upcase_full_nameメソッドを持つ。これは、full_nameメソッドの結果をすべて大文字で返す。このメソッドは副作用を持たない。 7 | # upcase_full_name! メソッドを持つ。これは、upcase_full_nameの副作用を持つバージョンで、ファーストネーム、ミドルネーム、ラストネームをすべて大文字に変え、オブジェクトはその状態を記憶する 8 | end 9 | -------------------------------------------------------------------------------- /03_method/01_method_first_step.rb: -------------------------------------------------------------------------------- 1 | # Q1. 2 | # 次の動作をする F1 class を実装する 3 | # - 1. "def"キーワードを使わずにF1クラスにhelloインスタンスメソッドを定義すること 4 | # 戻り値は "hello" であること 5 | # - 2. "def"キーワードを使わずにF1クラスにworldクラスメソッドを定義すること 6 | # 戻り値は "world" であること 7 | # - 3. 定義していないメソッドを実行したときにエラーを発生させず、"NoMethodError"という文字列を返すこと 8 | # - 4. `F1.new.respond_to?(定義していないメソッド名)` を実行したときにtrueを返すこと 9 | 10 | class F1 11 | end 12 | 13 | # Q2. 14 | # 次の動作をする F2 classを実装する 15 | # - 1. 実行するとhiインスタンスメソッドを定義するadd_hiメソッドを定義すること 16 | 17 | class F2 18 | end 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (C) 2022 kinoppyd 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. 14 | -------------------------------------------------------------------------------- /answers/02_object_model/01_hoge.rb: -------------------------------------------------------------------------------- 1 | # Q1. 問題の解説 2 | # 3 | # ほぼ特筆するべきところがないですが、hoge?メソッドの実装は少し悩むかもしれません。 4 | # 自身を参照するにはselfを使います。 5 | # 6 | class Hoge < String 7 | Hoge = 'hoge' 8 | 9 | def hogehoge 10 | 'hoge' 11 | end 12 | 13 | def hoge? 14 | self == 'hoge' 15 | end 16 | end 17 | 18 | # Q2. 問題の解説 19 | # 20 | # 回答例ではObjectクラスにhogeメソッドを定義しました。仕様としてあげられているクラスはすべて 21 | # Objectクラスのサブクラスなので、Objectクラスのインスタンスメソッドとしてhogeを定義すると仕様を満たせます。 22 | # Objectクラスではなく、仕様としてあげられていた各クラス(String, Integer, Numeric, Class, Hash, TrueClass) 23 | # に対してそれぞれ個別にhogeメソッドを定義しても問題ありません。 24 | class Object 25 | def hoge 26 | 'hoge' 27 | end 28 | end -------------------------------------------------------------------------------- /03_method/02_define.rb: -------------------------------------------------------------------------------- 1 | # Q1. 2 | # 次の動作をする A1 class を実装する 3 | # - "//" を返す "//"メソッドが存在すること 4 | 5 | # Q2. 6 | # 次の動作をする A2 class を実装する 7 | # - 1. "SmartHR Dev Team"と返すdev_teamメソッドが存在すること 8 | # - 2. initializeに渡した配列に含まれる値に対して、"hoge_" をprefixを付与したメソッドが存在すること 9 | # - 2で定義するメソッドは下記とする 10 | # - 受け取った引数の回数分、メソッド名を繰り返した文字列を返すこと 11 | # - 引数がnilの場合は、dev_teamメソッドを呼ぶこと 12 | # - また、2で定義するメソッドは以下を満たすものとする 13 | # - メソッドが定義されるのは同時に生成されるオブジェクトのみで、別のA2インスタンスには(同じ値を含む配列を生成時に渡さない限り)定義されない 14 | 15 | # Q3. 16 | # 次の動作をする OriginalAccessor モジュール を実装する 17 | # - OriginalAccessorモジュールはincludeされたときのみ、my_attr_accessorメソッドを定義すること 18 | # - my_attr_accessorはgetter/setterに加えて、boolean値を代入した際のみ真偽値判定を行うaccessorと同名の?メソッドができること 19 | -------------------------------------------------------------------------------- /answers/04_block/02_evil_mailbox.rb: -------------------------------------------------------------------------------- 1 | # 問題の解説 2 | # 3 | # 仕様の「邪悪な機能」をクロージャを使って実装することに気付けるかどうかを問う問題です。 4 | # initializeメソッドの中でdefine_singleton_methodを利用してsend_mailメソッドを定義することで、 5 | # initializeメソッドのローカル変数として第2引数を扱います。こうすることで、 6 | # send_mailメソッドの中でしか参照できない変数ができあがります。 7 | 8 | class EvilMailbox 9 | def initialize(obj, str = nil) 10 | @obj = obj 11 | @obj.auth(str) if str 12 | 13 | define_singleton_method(:send_mail) do |to, body, &block| 14 | result = obj.send_mail(to, body + str.to_s) 15 | block.call(result) if block 16 | nil 17 | end 18 | end 19 | 20 | def receive_mail 21 | obj.receive_mail 22 | end 23 | 24 | private 25 | 26 | attr_reader :obj 27 | end -------------------------------------------------------------------------------- /03_method/test/test_method_first_step.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'securerandom' 3 | require '01_method_first_step' 4 | 5 | class TestMethodFirstStep < Minitest::Test 6 | def test_hello 7 | assert_equal F1.new.hello, 'hello' 8 | end 9 | 10 | def test_world 11 | assert_equal F1.world, 'world' 12 | end 13 | 14 | def test_method_missing 15 | assert_equal F1.new.send(SecureRandom.alphanumeric), 'NoMethodError' 16 | end 17 | 18 | def test_respond_to 19 | assert F1.new.respond_to?(SecureRandom.alphanumeric) 20 | end 21 | 22 | def test_add_hi 23 | f2 = F2.new 24 | refute f2.respond_to?(:hi) 25 | f2.add_hi 26 | assert f2.respond_to?(:hi) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /04_block/test/test_block_first_step.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class AcceptBlock 4 | class << self 5 | attr_accessor :result 6 | end 7 | 8 | def self.call(&block) 9 | @result = block == MY_LAMBDA 10 | end 11 | end 12 | 13 | require '01_block_first_step' 14 | 15 | class TestBlockFirstStep < Minitest::Test 16 | def test_my_math 17 | assert_equal 4, MyMath.new.two_times { 2 } 18 | end 19 | 20 | def test_accept_block 21 | assert AcceptBlock.result 22 | end 23 | 24 | def test_my_block 25 | assert_equal(MY_LAMBDA, MyBlock.new.block_to_proc(&MY_LAMBDA)) 26 | end 27 | 28 | def test_my_closure 29 | m1 = MyClosure.new 30 | m2 = MyClosure.new 31 | assert_equal(1, m1.increment) 32 | assert_equal(2, m2.increment) 33 | assert_equal(3, m1.increment) 34 | MyClosure 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /answers/00_setup/01_try_out.rb: -------------------------------------------------------------------------------- 1 | # 問題の解説 2 | # 3 | # ミドルネームが渡されないことがある、というのをどう扱うかがこの問題のポイントです。 4 | # `def initialize(first_name, middle_name = nil, last_name)`のようにメソッドを定義することで 5 | # 簡潔に仕様を満たすことができます。 6 | # あとはスペースで各要素を区切るやり方としてArray#joinを使っているのもポイントです。 7 | # これ以外にも複数の解法があります。この回答通りになっていなくても問題ありません。 8 | class TryOut 9 | attr_writer :first_name 10 | 11 | def initialize(first_name, middle_name = nil, last_name) 12 | @first_name = first_name 13 | @middle_name = middle_name 14 | @last_name = last_name 15 | end 16 | 17 | def full_name 18 | [@first_name, @middle_name, @last_name].compact.join(' ') 19 | end 20 | 21 | def upcase_full_name 22 | full_name.upcase 23 | end 24 | 25 | def upcase_full_name! 26 | @first_name.upcase! 27 | @middle_name&.upcase! 28 | @last_name.upcase! 29 | full_name 30 | end 31 | end -------------------------------------------------------------------------------- /answers/03_method/01_method_first_step.rb: -------------------------------------------------------------------------------- 1 | # Q1. 問題の解説 2 | # 3 | # define_methodとdefine_singleton_methodとmethod_missingの素振り用の問題です。 4 | # define_singleton_methodは3章にはまだ出てきていませんが、これを知らないと3章の問題を解くのが難しくなるので覚えておいてください 5 | # respond_to_missing?は、respond_to?メソッド実行時にメソッドが定義されていない場合に呼ばれるメソッドです。method_missingを定義する場合は 6 | # 必ず定義しておきましょう。 7 | # 8 | class F1 9 | define_method :hello do 10 | 'hello' 11 | end 12 | 13 | define_singleton_method :world do 14 | 'world' 15 | end 16 | 17 | def method_missing(*args) 18 | 'NoMethodError' 19 | end 20 | 21 | def respond_to_missing?(*args) 22 | true 23 | end 24 | end 25 | 26 | # Q2. 問題の解説 27 | # 28 | # メソッドを実行したら新しいメソッドができる、ということを実感してもらうための問題です。この回答のようなdefがネストする実装は普通はやりませんが、 29 | # 「特定の処理を実行する時に動的にメソッドを生やす」という場面は、メタプロをしていればそれなりにあります。 30 | # 31 | class F2 32 | def add_hi 33 | def hi 34 | end 35 | end 36 | end -------------------------------------------------------------------------------- /06_codes_generate_codes/01_simple_model.rb: -------------------------------------------------------------------------------- 1 | # 次の仕様を満たす、SimpleModelモジュールを作成してください 2 | # 3 | # 1. include されたクラスがattr_accessorを使用すると、以下の追加動作を行う 4 | # 1. 作成したアクセサのreaderメソッドは、通常通りの動作を行う 5 | # 2. 作成したアクセサのwriterメソッドは、通常に加え以下の動作を行う 6 | # 1. 何らかの方法で、writerメソッドを利用した値の書き込み履歴を記憶する 7 | # 2. いずれかのwriterメソッド経由で更新をした履歴がある場合、 `true` を返すメソッド `changed?` を作成する 8 | # 3. 個別のwriterメソッド経由で更新した履歴を取得できるメソッド、 `ATTR_changed?` を作成する 9 | # 1. 例として、`attr_accessor :name, :desc` とした時、このオブジェクトに対して `obj.name = 'hoge` という操作を行ったとする 10 | # 2. `obj.name_changed?` は `true` を返すが、 `obj.desc_changed?` は `false` を返す 11 | # 3. 参考として、この時 `obj.changed?` は `true` を返す 12 | # 2. initializeメソッドはハッシュを受け取り、attr_accessorで作成したアトリビュートと同名のキーがあれば、自動でインスタンス変数に記録する 13 | # 1. ただし、この動作をwriterメソッドの履歴に残してはいけない 14 | # 3. 履歴がある場合、すべての操作履歴を放棄し、値も初期状態に戻す `restore!` メソッドを作成する 15 | 16 | module SimpleModel 17 | end 18 | -------------------------------------------------------------------------------- /answers/04_block/01_block_first_step.rb: -------------------------------------------------------------------------------- 1 | # Q1. 問題の解説 2 | # 3 | # yieldもしくはcallメソッドを使うメソッド実装の練習です。Railsでアプリケーションを書いているとそれほどブロックを取る 4 | # メソッドを書く機会はないのですが、素振りをしておいていざというときに使えるようにしておくと役に立つ時が来るかもしれません 5 | class MyMath 6 | def two_times 7 | yield * 2 8 | end 9 | end 10 | 11 | # Q2. 問題の解説 12 | # 13 | # Procオブジェクトをブロック引数として渡す練習です。実引数を渡すところで`&`を使うと、Procからブロックへの変換ができます。 14 | MY_LAMBDA = -> { 3 } 15 | AcceptBlock.call(&MY_LAMBDA) 16 | 17 | # Q3. 問題の解説 18 | # 19 | # Q2と反対に、ブロックからProcオブジェクトの変換をする練習です。仮引数で&を使うとブロックからProcオブジェクトへの変換ができます。 20 | class MyBlock 21 | def block_to_proc(&block) 22 | block 23 | end 24 | end 25 | 26 | 27 | # Q4. 問題の解説 28 | # 29 | # クロージャを実装してみる練習です。ブロックを利用するとスコープゲートなしで束縛を利用できるのでしたね。メソッド定義をdefではなく 30 | # define_method にすることで外側のローカル変数への参照を持ち続けることができます。 31 | class MyClosure 32 | count = 0 33 | 34 | define_method :increment do 35 | count += 1 36 | end 37 | end -------------------------------------------------------------------------------- /04_block/01_block_first_step.rb: -------------------------------------------------------------------------------- 1 | # Q1. 2 | # MyMathクラスに、ブロックを実行した結果(数値)を2倍にして返すtwo_timesインスタンスメソッドを定義しましょう 3 | # 実行例: MyMath.new.two_times { 2 } #=> 4 4 | 5 | class MyMath 6 | end 7 | 8 | # Q2. 9 | # AcceptBlockクラスにcallクラスメソッドが予め定義されており、このメソッドがブロックをとるとします。 10 | # 実行例: AcceptBlock.call { 2 } 11 | # このメソッドを、下で用意されているMY_LAMBDAをブロック引数として渡して実行してみてください。 12 | # AcceptBlockクラスは問題側で用意している(テスト中に実装している)ため実装の必要はありません。 13 | 14 | MY_LAMBDA = -> { 3 } 15 | 16 | # Q3. 17 | # MyBlockクラスにblock_to_procインスタンスメソッドを定義しましょう。block_to_procインスタンスメソッドはブロックを受け取り、 18 | # そのブロックをProcオブジェクトにしたものを返します 19 | 20 | class MyBlock 21 | end 22 | 23 | # Q4. 24 | # MyClosureクラスにincrementインスタンスメソッドを定義しましょう。このincrementメソッドは次のように数値を1ずつインクリメントして返します 25 | # my = MyClosure.new 26 | # my.increment #=> 1 27 | # my.increment #=> 2 28 | # my.increment #=> 3 29 | # それに加えて、複数のインスタンスでカウンターを共有しているという特性があります。 30 | # my1 = MyClosure.new 31 | # my2 = MyClosure.new 32 | # my1.increment #=> 1 33 | # my2.increment #=> 2 34 | # my1.increment #=> 3 35 | # さらなる制限として、カウンターとして利用する変数はローカル変数を利用してください(これはテストにはないですが頑張ってローカル変数でテストを通るようにしてみてください) 36 | 37 | class MyClosure 38 | end -------------------------------------------------------------------------------- /04_block/03_simple_bot.rb: -------------------------------------------------------------------------------- 1 | # 次の仕様を満たすSimpleBotクラスとDSLを作成してください 2 | # 3 | # # これは、作成するSimpleBotクラスの利用イメージです 4 | # class Bot < SimpleBot 5 | # setting :name, 'bot' 6 | # respond 'keyword' do 7 | # "response #{settings.name}" 8 | # end 9 | # end 10 | # 11 | # Bot.new.ask('keyword') #=> 'respond bot' 12 | # 13 | # 1. SimpleBotクラスを継承したクラスは、クラスメソッドrespond, setting, settingsを持ちます 14 | # 1. settingsメソッドは、任意のオブジェクトを返します 15 | # 2. settingsメソッドは、後述するクラスメソッドsettingによって渡された第一引数と同名のメソッド呼び出しに応答します 16 | # 2. SimpleBotクラスのサブクラスのインスタンスは、インスタンスメソッドaskを持ちます 17 | # 1. askは、一つの引数をとります 18 | # 2. askに渡されたオブジェクトが、後述するrespondメソッドで設定したオブジェクトと一致する場合、インスタンスは任意の返り値を持ちます 19 | # 3. 2のケースに当てはまらない場合、askメソッドの戻り値はnilです 20 | # 3. クラスメソッドrespondは、keywordとブロックを引数に取ります 21 | # 1. respondメソッドの第1引数keywordと同じ文字列が、インスタンスメソッドaskに渡された時、第2引数に渡したブロックが実行され、その結果が返されます 22 | # 4. クラスメソッドsettingは、引数を2つ取り、1つ目がキー名、2つ目が設定する値です 23 | # 1. settingメソッドに渡された値は、クラスメソッド `settings` から返されるオブジェクトに、メソッド名としてアクセスすることで取り出すことができます 24 | # 2. e.g. クラス内で `setting :name, 'bot'` と実行した場合は、respondメソッドに渡されるブロックのスコープ内で `settings.name` の戻り値は `bot` の文字列になります 25 | -------------------------------------------------------------------------------- /04_block/02_evil_mailbox.rb: -------------------------------------------------------------------------------- 1 | # 次の仕様を満たすクラス、EvilMailboxを作成してください 2 | # 3 | # 基本機能 4 | # 1. EvilMailboxは、コンストラクタで一つのオブジェクトを受け取る(このオブジェクトは、メールの送受信機能が実装されているが、それが何なのかは気にする必要はない) 5 | # 2. EvilMailboxは、メールを送るメソッド `send_mail` を持ち、引数として宛先の文字列、本文の文字列を受け取る。結果の如何に関わらず、メソッドはnilをかえす。 6 | # 3. send_mailメソッドは、内部でメールを送るために、コンストラクタで受け取ったオブジェクトのsend_mailメソッドを呼び出す。このときのシグネチャは同じである。また、このメソッドはメールの送信が成功したか失敗したかをbool値で返す。 7 | # 4. EvilMailboxは、メールを受信するメソッド `receive_mail` を持つ 8 | # 5. receive_mailメソッドは、メールを受信するためにコンストラクタで受け取ったオブジェクトのreceive_mailメソッドを呼び出す。このオブジェクトのreceive_mailは、送信者と本文の2つの要素をもつ配列を返す。 9 | # 6. receive_mailメソッドは、受け取ったメールを送信者と本文の2つの要素をもつ配列として返す 10 | # 11 | # 応用機能 12 | # 1. send_mailメソッドは、ブロックを受けとることができる。ブロックは、送信の成功/失敗の結果をBool値で引数に受け取ることができる 13 | # 2. コンストラクタは、第2引数として文字列を受け取ることができる(デフォルトはnilである) 14 | # 3. コンストラクタが第2引数として文字列を受け取った時、第1引数のオブジェクトはその文字列を引数にしてauthメソッドを呼び出す 15 | # 4. 第2引数の文字列は、秘密の文字列のため、EvilMailboxのオブジェクトの中でいかなる形でも保存してはいけない 16 | # 17 | # 邪悪な機能 18 | # 1. send_mailメソッドは、もしも”コンストラクタで受け取ったオブジェクトがauthメソッドを呼んだ”とき、勝手にその認証に使った文字列を、送信するtextの末尾に付け加える 19 | # 2. つまり、コンストラクタが第2引数に文字列を受け取った時、その文字列はオブジェクト内に保存されないが、send_mailを呼び出したときにこっそりと勝手に送信される 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # reading-metaprogramming-ruby 2 | 3 | ## これはなに 4 | 5 | このリポジトリは、[メタプログラミングRuby 第2版](https://www.oreilly.co.jp/books/9784873117430/)を読んだ人向けの練習問題集です。本を読んだだけだとなかなか身につかないRubyのメタプログラミングの知識を、手を動かして理解することを目的にしています。 6 | 7 | ## 始め方 8 | 9 | まずこのリポジトリをcloneしてbundle installまでしておきます。対象としているRubyのバージョンは3.4です。 10 | 11 | `02_object_model`のように、頭に章番号がふってあるディレクトリが、その章で学んだ内容を問う問題にあたります。`00_setup`は練習問題の解き方を練習するための問題です。 12 | 13 | 各章のディレクトリ中には練習問題があります。各ファイルには`01_method_first_step.rb`のように先頭に番号が振ってあるので、番号順で解くのをオススメしています。ファイルの中には日本語で満たすべき仕様と、いくらかのコードが記述されています。 14 | 15 | 各ディレクトリには問題だけでなく、テストも付属しています。各章のディレクトリに移動して`bundle exec rake`とするとテストを実行できます。プロジェクトのルートディレクトリで`bundle exec rake`をすると、すべての問題のテストを実行します。 16 | 17 | 問題文に示された仕様が満たされるとテストがパスするようになっています。頑張ってテストをパスするコードを書いてみましょう。もしテストと仕様の文章が違うなどの不備を見つけたらプルリクエストを送ってもらえると嬉しいです。 18 | 19 | 単体でテストを実行したい場合は`bundle exec ruby -I../test test/test_method_first_step.rb`のようにするとできます。 20 | 21 | ## 解答例 22 | 23 | 問題を解こうとしたけれどよくわからなかった、とかテストはパスしたけれどこれが良いコードなのかわからない、という人のために解答例と解説をanswersディレクトリ配下に置いています。適宜参考にしつつメタプログラミングの理解を深めてください。 24 | 25 | もっと良い解答例がある場合はプルリクエストを送ってもらえると嬉しいです。 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /05_class_definition/02_simple_mock.rb: -------------------------------------------------------------------------------- 1 | # 次の仕様を満たすモジュール SimpleMock を作成してください 2 | # 3 | # SimpleMockは、次の2つの方法でモックオブジェクトを作成できます 4 | # 特に、2の方法では、他のオブジェクトにモック機能を付与します 5 | # この時、もとのオブジェクトの能力が失われてはいけません 6 | # また、これの方法で作成したオブジェクトを、以後モック化されたオブジェクトと呼びます 7 | # 1. 8 | # ``` 9 | # SimpleMock.new 10 | # ``` 11 | # 12 | # 2. 13 | # ``` 14 | # obj = SomeClass.new 15 | # SimpleMock.mock(obj) 16 | # ``` 17 | # 18 | # モック化したオブジェクトは、expectsメソッドに応答します 19 | # expectsメソッドには2つの引数があり、それぞれ応答を期待するメソッド名と、そのメソッドを呼び出したときの戻り値です 20 | # ``` 21 | # obj = SimpleMock.new 22 | # obj.expects(:imitated_method, true) 23 | # obj.imitated_method #=> true 24 | # ``` 25 | # モック化したオブジェクトは、expectsの第一引数に渡した名前のメソッド呼び出しに反応するようになります 26 | # そして、第2引数に渡したオブジェクトを返します 27 | # 28 | # モック化したオブジェクトは、watchメソッドとcalled_timesメソッドに応答します 29 | # これらのメソッドは、それぞれ1つの引数を受け取ります 30 | # watchメソッドに渡した名前のメソッドが呼び出されるたび、モック化したオブジェクトは内部でその回数を数えます 31 | # そしてその回数は、called_timesメソッドに同じ名前の引数が渡された時、その時点での回数を参照することができます 32 | # ``` 33 | # obj = SimpleMock.new 34 | # obj.expects(:imitated_method, true) 35 | # obj.watch(:imitated_method) 36 | # obj.imitated_method #=> true 37 | # obj.imitated_method #=> true 38 | # obj.called_times(:imitated_method) #=> 2 39 | # ``` 40 | -------------------------------------------------------------------------------- /02_object_model/test/test_hoge.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require '01_hoge' 3 | 4 | class TestHoge < Minitest::Test 5 | def test_hoge_in_string 6 | assert_equal "hoge","hoge".hoge 7 | end 8 | 9 | def test_hoge_in_integer 10 | assert_equal "hoge", 1.hoge 11 | end 12 | 13 | def test_hoge_in_numeric 14 | assert_equal "hoge", (1.1).hoge 15 | end 16 | 17 | def test_hoge_in_class 18 | assert_equal "hoge", Class.hoge 19 | end 20 | 21 | def test_hoge_in_hash 22 | assert_equal "hoge", ({hoge: :foo}).hoge 23 | end 24 | 25 | def test_hoge_in_true_class 26 | assert_equal "hoge", true.hoge 27 | end 28 | 29 | def test_hoge_const 30 | assert_equal "hoge", Hoge::Hoge 31 | end 32 | 33 | def test_hogehoge_method_exists_in_hoge_class 34 | assert Hoge.instance_methods(false).include?(:hogehoge) 35 | end 36 | 37 | def test_hogehoge_method_returns_hoge 38 | assert_equal "hoge", Hoge.new.hogehoge 39 | end 40 | 41 | def test_hoge_super_class_is_string 42 | assert_equal String, Hoge.superclass 43 | end 44 | 45 | def test_ask_hoge_myself_true 46 | assert Hoge.new("hoge").hoge? 47 | end 48 | 49 | def test_ask_hoge_myself_false 50 | refute Hoge.new("foo").hoge? 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /answers/04_block/03_simple_bot.rb: -------------------------------------------------------------------------------- 1 | # 問題の解説 2 | # 3 | # respondクラスメソッドで定義したブロックを、askインスタンスメソッドからどうやって参照するか、というのが 4 | # この問題の難所です。クラスメソッドで定義したインスタンス変数はクラスインスタンス変数としてクラスそのものに 5 | # 紐づくインスタンス変数になるので、インスタンスメソッドから参照するには、回答例のように 6 | # `self.class.instance_variable_get(インスタンス変数名)`のようにします。 7 | # クラス変数を利用するとクラスメソッド、インスタンスメソッドどちらからでも`@@respond`のようにアクセスできるので 8 | # 一見便利ですが、意図せず別のクラスとクラス変数が共有される可能性があるため、推奨しません。 9 | # 10 | # SimpleBotとそのサブクラスで利用イメージのように定義されたブロックは、settingsクラスメソッドにアクセスできます。 11 | # settingsクラスメソッドは、settingクラスメソッドで登録したキーと値をそれぞれメソッド名とその返り値に持つオブジェクトを返すと 12 | # 仕様を満たせます。メソッドが定義できればどんなオブジェクトを返しても仕様を満たせるため、この回答例では 13 | # 特異メソッドを定義したObjectインスタンスを返しています。必ずしもObjectインスタンスである必要はありません。 14 | # 15 | class SimpleBot 16 | class << self 17 | def respond(keyword, &block) 18 | @respond ||= {} 19 | @respond[keyword] = block 20 | end 21 | 22 | def setting(key, value) 23 | @settings ||= {} 24 | @settings[key] = value 25 | end 26 | 27 | def settings 28 | obj = Object.new 29 | 30 | @settings&.each do |key, value| 31 | obj.define_singleton_method(key) do 32 | value 33 | end 34 | end 35 | obj 36 | end 37 | end 38 | 39 | def ask(keyword) 40 | block = self.class.instance_variable_get(:@respond)[keyword] 41 | block.call if block 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /00_setup/test/test_try_out.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require '01_try_out' 3 | 4 | class TestTryOut < Minitest::Test 5 | def test_first_last_name 6 | target = TryOut.new("John", "Wick") 7 | assert_equal "John Wick", target.full_name 8 | end 9 | 10 | def test_first_middle_last_name 11 | target = TryOut.new("Keanu", "Charies", "Reeves") 12 | assert_equal "Keanu Charies Reeves", target.full_name 13 | end 14 | 15 | def test_first_name_accessor 16 | target = TryOut.new("Henrik", "Vanger") 17 | target.first_name = "Martin" 18 | assert_equal "Martin Vanger", target.full_name 19 | end 20 | 21 | def test_upcase_full_name 22 | target = TryOut.new("Arthur", "Fleck") 23 | assert_equal "ARTHUR FLECK", target.upcase_full_name 24 | end 25 | 26 | def test_upcase_full_name_no_side_effect 27 | target = TryOut.new("Lorraine", "Broughton") 28 | target.upcase_full_name 29 | assert_equal "Lorraine Broughton", target.full_name 30 | end 31 | 32 | def test_upcase_full_name_bang 33 | target = TryOut.new("Earl", "Stone") 34 | assert_equal "EARL STONE", target.upcase_full_name! 35 | end 36 | 37 | def test_upcase_full_name_bang_has_side_effect 38 | target = TryOut.new("Murphy", "McManus") 39 | target.upcase_full_name! 40 | assert_equal "MURPHY MCMANUS", target.full_name 41 | end 42 | 43 | def test_too_few_arguments 44 | assert_raises (ArgumentError) {TryOut.new("John")} 45 | end 46 | 47 | def test_too_many_arguments 48 | assert_raises (ArgumentError) {TryOut.new("John", "Milton", "Cage", "Jr")} 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /answers/05_class_definition/01_class_definition_first_step.rb: -------------------------------------------------------------------------------- 1 | # Q1. 問題の解説 2 | # e2オブジェクトの特異メソッドとしてhelloを定義する練習です。特異メソッドは対象のオブジェクトだけが利用可能なメソッドです。 3 | # 4 | class ExClass 5 | end 6 | 7 | e1 = ExClass.new 8 | e2 = ExClass.new 9 | 10 | def e2.hello 11 | end 12 | 13 | Judgement.call(e1, e2) 14 | 15 | # Q2. 問題の解説 16 | # Class.newでクラスを作る練習です。Class.newで作ったクラスは定数にアサインされるまで無名クラス(nameメソッドがnilを返す)になります。 17 | # 18 | e = Class.new(ExClass) 19 | Judgement2.call(e) 20 | 21 | # Q3. 問題の解説 22 | # クラスマクロを作ってみる練習でした。クラスメソッドとしてmeta_attr_accessorを定義し、その中でゲッターとセッターを定義します。 23 | # 24 | class MetaClass 25 | class << self 26 | def meta_attr_accessor(name) 27 | meta_name = "meta_#{name}" 28 | attr_writer(meta_name) 29 | define_method(meta_name) do 30 | 'meta ' + instance_variable_get("@#{meta_name}") 31 | end 32 | end 33 | end 34 | end 35 | 36 | # Q4. 問題の解説 37 | # クラスインスタンス変数を使ってみる練習問題です。クラスメソッド用とインスタンスメソッド用のセッターゲッターを作り、 38 | # それぞれで同じクラスインスタンス変数を参照するようにします。 39 | # 40 | class ExConfig 41 | class << self 42 | attr_accessor :config 43 | end 44 | 45 | def config 46 | self.class.instance_variable_get(:@config) 47 | end 48 | 49 | def config=(value) 50 | self.class.instance_variable_set(:@config, value) 51 | end 52 | end 53 | 54 | # Q5. 問題の解説 55 | # アラウンドエイリアスの練習でした。この回答例ではprependを利用しています。 56 | # 57 | module Hook 58 | def hello 59 | before 60 | super 61 | after 62 | end 63 | end 64 | 65 | class ExOver 66 | prepend Hook 67 | end 68 | 69 | # Q6. 問題の解説 70 | # class_evalを使う練習でした。ブロックを使うと、ブロックの外側のローカル変数にアクセスできます。 71 | # 72 | class MyGreeting 73 | end 74 | 75 | toplevellocal = 'hi' 76 | 77 | MyGreeting.class_eval do 78 | define_method :say do 79 | toplevellocal 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /04_block/test/test_simple_bot.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'securerandom' 3 | require '03_simple_bot' 4 | 5 | class TestSimpleBot < Minitest::Test 6 | def bot_for_test(&block) 7 | Class.new(SimpleBot, &block) 8 | end 9 | 10 | def test_response 11 | klass = bot_for_test do 12 | respond 'hello' do 13 | 'Yo' 14 | end 15 | end 16 | 17 | assert_equal 'Yo', klass.new.ask('hello') 18 | end 19 | 20 | def test_no_response 21 | klass = bot_for_test do 22 | respond 'yo' do 23 | 'yo' 24 | end 25 | end 26 | 27 | assert_nil klass.new.ask("hello") 28 | end 29 | 30 | def test_global_setting 31 | klass = bot_for_test do 32 | setting :name, 'bot' 33 | respond 'what is your name?' do 34 | "i'm #{settings.name}" 35 | end 36 | end 37 | 38 | assert_equal "i'm bot", klass.new.ask("what is your name?") 39 | end 40 | 41 | def test_global_setting_random 42 | code = SecureRandom.hex 43 | 44 | klass = bot_for_test do 45 | setting :code, code 46 | respond 'tell me your code' do 47 | "code is #{settings.code}" 48 | end 49 | end 50 | 51 | assert_equal "code is #{code}", klass.new.ask('tell me your code') 52 | end 53 | 54 | def test_global_setting_multiple_call 55 | klass = bot_for_test do 56 | setting :name, 'bot' 57 | setting :age, 10 58 | respond 'what is your name?' do 59 | "i'm #{settings.name}" 60 | end 61 | respond 'how old are you?' do 62 | "i'm #{settings.age} years old" 63 | end 64 | end 65 | 66 | assert_equal "i'm bot", klass.new.ask("what is your name?") 67 | assert_equal "i'm 10 years old", klass.new.ask("how old are you?") 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /05_class_definition/01_class_definition_first_step.rb: -------------------------------------------------------------------------------- 1 | # 1. ExClassクラスのオブジェクトが2つあります。これらをJudgement.callに渡しています。 2 | # Judement.callはテスト側で定義するので実装は不要です。この状況でe2オブジェクト"だけ"helloメソッドを 3 | # 使えるようにしてください。helloメソッドの中身は何でも良いです。 4 | 5 | class ExClass 6 | end 7 | 8 | e1 = ExClass.new 9 | e2 = ExClass.new 10 | 11 | Judgement.call(e1, e2) 12 | 13 | # 2. ExClassを継承したクラスを作成してください。ただし、そのクラスは定数がない無名のクラスだとします。 14 | # その無名クラスをそのままJudgement2.call の引数として渡してください(Judgement2.callはテスト側で定義するので実装は不要です) 15 | 16 | 17 | # 3. 下のMetaClassに対し、次のように`meta_`というプレフィックスが属性名に自動でつき、ゲッターの戻り値の文字列にも'meta 'が自動でつく 18 | # attr_accessorのようなメソッドであるmeta_attr_accessorを作ってください。セッターに文字列以外の引数がくることは考えないとします。 19 | # 20 | # 使用例: 21 | # 22 | # class MetaClass 23 | # # meta_attr_accessor自体の定義は省略 24 | # meta_attr_accessor :hello 25 | # end 26 | # meta = MetaClass.new 27 | # meta.meta_hello = 'world' 28 | # meta.meta_hello #=> 'meta world' 29 | 30 | class MetaClass 31 | end 32 | 33 | # 4. 次のようなExConfigクラスを作成してください。ただし、グローバル変数、クラス変数は使わないものとします。 34 | # 使用例: 35 | # ExConfig.config = 'hello' 36 | # ExConfig.config #=> 'hello' 37 | # ex = ExConfig.new 38 | # ex.config #=> 'hello' 39 | # ex.config = 'world' 40 | # ExConfig.config #=> 'world' 41 | 42 | 43 | class ExConfig 44 | end 45 | 46 | # 5. 47 | # ExOver#helloというメソッドがライブラリとして定義されているとします。ExOver#helloメソッドを実行したとき、 48 | # helloメソッドの前にExOver#before、helloメソッドの後にExOver#afterを実行させるようにExOverを変更しましょう。 49 | # ただしExOver#hello, ExOver#before, ExOver#afterの実装はそれぞれテスト側で定義しているので実装不要(変更不可)です。 50 | # 51 | 52 | class ExOver 53 | end 54 | 55 | # 6. 次の toplevellocal ローカル変数の中身を返す MyGreeting#say を実装してみてください。 56 | # ただし、下のMyGreetingは編集しないものとします。toplevellocal ローカル変数の定義の下の行から編集してください。 57 | # ヒント: スコープゲートを乗り越える方法について書籍にありましたね 58 | 59 | class MyGreeting 60 | end 61 | 62 | toplevellocal = 'hi' 63 | -------------------------------------------------------------------------------- /05_class_definition/test/test_class_definition_first_step.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Judgement 4 | def self.call(e1, e2) 5 | @e1 = e1 6 | @e2 = e2 7 | end 8 | end 9 | 10 | class Judgement2 11 | def self.call(klass) 12 | @klass = klass 13 | end 14 | end 15 | 16 | class ExOver 17 | attr_accessor :result 18 | 19 | def initialize 20 | self.result = '' 21 | end 22 | 23 | def before 24 | result << 'before' 25 | end 26 | 27 | def hello 28 | result << 'hello' 29 | end 30 | 31 | def after 32 | result << 'after' 33 | end 34 | end 35 | 36 | require '01_class_definition_first_step' 37 | 38 | class TestClassDefinitionFirstStep < Minitest::Test 39 | def test_judgement 40 | e1 = Judgement.instance_variable_get(:@e1) 41 | e2 = Judgement.instance_variable_get(:@e2) 42 | assert e1.is_a?(ExClass) 43 | assert e2.is_a?(ExClass) 44 | refute e1.respond_to?(:hello) 45 | assert e2.respond_to?(:hello) 46 | end 47 | 48 | def test_judgement2 49 | klass = Judgement2.instance_variable_get(:@klass) 50 | assert klass.name.nil? 51 | assert klass.superclass == ExClass 52 | end 53 | 54 | def test_metaclass 55 | MetaClass.class_eval do 56 | meta_attr_accessor :hello 57 | end 58 | 59 | meta = MetaClass.new 60 | meta.meta_hello = 'hello' 61 | assert_equal 'meta hello', meta.meta_hello 62 | end 63 | 64 | def test_exconfig 65 | ExConfig.config = 'hello' 66 | assert_equal 'hello', ExConfig.config 67 | ex = ExConfig.new 68 | assert_equal 'hello', ex.config 69 | ex.config = 'world' 70 | assert_equal 'world', ExConfig.config 71 | end 72 | 73 | def test_exover 74 | exover = ExOver.new 75 | assert_equal 'beforehelloafter', exover.hello 76 | end 77 | 78 | def test_mygreeting 79 | assert_equal 'hi', MyGreeting.new.say 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /answers/05_class_definition/02_simple_mock.rb: -------------------------------------------------------------------------------- 1 | # 問題の解説 2 | # まずmockメソッドの実装から考えます。「もとのオブジェクトの能力が失われてはいけない」という仕様から、引数として受け付けたオブジェクトに 3 | # SimpleMockをextendすることでモック化に必要なメソッドであるexpects, watch, called_timesを追加するようにします。 4 | # 5 | # expectsメソッドを実行したとき、レシーバとなるオブジェクトにだけメソッドを追加したいのでdefine_singleton_methodを利用して動的にメソッドを追加します。 6 | # メソッドの内容は、次のようにexpectsメソッドに続けてwatchメソッドが実行されたときに備えて、 7 | # カウンター用のインスタンス変数`@counter`(キーがexpectsで指定されたメソッド名、値が実行回数のハッシュ)を用意して 8 | # watchが実行されていたら(つまり対応する`@counter`の値があれば)それをインクリメントするようにします。 9 | # 10 | # ```ruby 11 | # obj = Object.new 12 | # obj = SimpleMock(obj) 13 | # obj.expects(:hoge, true) 14 | # obj.watch(:hoge) 15 | # obj.hoge #=> true 16 | # ```` 17 | # 18 | # また、watchを実行したときにexpects経由で定義したメソッドを上書きしないように、expectsしたメソッド名を`@expects`に配列として保存しておきます。 19 | # watchでは`@expects`を見て、すでにexpectsで定義済みであればメソッドを上書きしないようにします。 20 | # そうしないとwatchメソッドを実行したときに、モックメソッドの戻り値の情報が失われてしまいます。 21 | # 22 | # 次にnewメソッドの実装を考えます。仕様から、SimpleMockはモジュールであることを求められていますが、 23 | # 同時にモジュールには存在しないnewメソッドを持つようにも求められています。 24 | # これを、クラスメソッドのnewを明示的に定義することで満たします。このとき何らかのオブジェクトをmockメソッドの引数にして、 25 | # 戻り値を返すようにすれば要件は満たせますが、モック用のオブジェクトとしては余計なメソッドをなるべく持たない方が扱いやすいので、 26 | # Object.newをmockメソッドの引数にしています。 27 | # 28 | module SimpleMock 29 | def self.mock(obj) 30 | obj.extend(SimpleMock) 31 | obj 32 | end 33 | 34 | def self.new 35 | obj = Object.new 36 | mock(obj) 37 | end 38 | 39 | def expects(name, value) 40 | define_singleton_method(name) do 41 | @counter[name] += 1 if @counter&.key?(name) 42 | value 43 | end 44 | @expects ||= [] 45 | @expects.push(name.to_sym) 46 | end 47 | 48 | def watch(name) 49 | (@counter ||= {})[name] = 0 50 | 51 | return if @expects&.include?(name.to_sym) 52 | 53 | define_singleton_method(name) do 54 | @counter[name] += 1 55 | end 56 | end 57 | 58 | def called_times(name) 59 | @counter[name] 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /answers/06_codes_generate_codes/01_simple_model.rb: -------------------------------------------------------------------------------- 1 | # 問題の解説 2 | # 3 | # includeされたクラスのattr_accessorメソッドの挙動を変更するために、まずincludedフックメソッドを利用します。 4 | # 5 | # 初期値を管理する`_histories`と`_initial`属性をattr_accessorで用意しておきます。 6 | # historiesやinitialといった名前はクラスのメソッド定義などと衝突する可能性が高いので、`_`を先頭につけて回避するようにしています。 7 | # `_histories`は、writerメソッドを呼び出した時に、その値を記憶するためのハッシュです。キーは属性名、値はその属性に対する書き込み履歴の配列です。 8 | # `_initial`は、初期値を記憶するためのハッシュです。キーは属性名、値はその属性の初期値です。 9 | # 10 | # includedの中で対象のクラスをextendして、クラスメソッドであるattr_accessorメソッドを再定義します。 11 | # readerメソッドは通常通りの動作を行う、と仕様にあるのでattr_readerを呼び出しています。 12 | # writerメソッドは、通常に加え以下の動作を行うと仕様にあるので、独自に定義します。writerメソッドの中で、`_histories`に書き込み履歴を追記させています。 13 | # そのうえで、instance_variable_setで属性の値を書き換えています。 14 | # 15 | # initializeメソッドを定義し、`_initial`と`_histories`の初期化と`_initial`への初期値の記憶を行っています。 16 | # 残りの`restore`, `changed?`, `ATTR_changed?`メソッドは、`_initial`と`_histories`を活用することで問題なく実装できるはずです。 17 | # 18 | module SimpleModel 19 | def self.included(klass) 20 | klass.attr_accessor :_histories, :_initial 21 | klass.extend(ClassMethods) 22 | end 23 | 24 | def initialize(args = {}) 25 | self._initial = args 26 | self._histories = {} 27 | args.each do |key, value| 28 | instance_variable_set("@#{key}", value) 29 | end 30 | end 31 | 32 | def restore! 33 | self._histories = {} 34 | _initial.each do |key, value| 35 | instance_variable_set("@#{key}", value) 36 | end 37 | end 38 | 39 | def changed? 40 | !_histories.empty? 41 | end 42 | 43 | module ClassMethods 44 | def attr_accessor(*syms) 45 | syms.each { |sym| attr_reader sym } 46 | syms.each do |sym| 47 | define_method "#{sym}=" do |value| 48 | (_histories[sym] ||= []).push(value) 49 | instance_variable_set("@#{sym}", value) 50 | end 51 | 52 | define_method "#{sym}_changed?" do 53 | !!_histories[sym] 54 | end 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /05_class_definition/test/test_simple_mock.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require '02_simple_mock' 3 | require 'securerandom' 4 | 5 | class TestSimpleMock < Minitest::Test 6 | class ClassForMockTest 7 | def hoge; "hoge"; end 8 | end 9 | 10 | def test_mock_initialize 11 | obj = SimpleMock.new 12 | assert_kind_of SimpleMock, obj 13 | end 14 | 15 | def test_mock_extend 16 | obj = ClassForMockTest.new 17 | SimpleMock.mock(obj) 18 | 19 | assert_kind_of SimpleMock, obj 20 | end 21 | 22 | def test_mock_retuns_setted_value_when_instance 23 | obj = SimpleMock.new 24 | expected = SecureRandom.hex 25 | obj.expects(:imitated_method, expected) 26 | 27 | assert_equal obj.imitated_method, expected 28 | end 29 | 30 | def test_mock_returns_setted_value_when_extended 31 | obj = ClassForMockTest.new 32 | SimpleMock.mock(obj) 33 | expected = SecureRandom.hex 34 | obj.expects(:imitated_method, expected) 35 | 36 | assert_equal obj.imitated_method, expected 37 | end 38 | 39 | def test_mock_counts_how_many_times_called_method 40 | obj = SimpleMock.mock(ClassForMockTest.new) 41 | obj.watch(:hoge) 42 | 43 | obj.hoge 44 | obj.hoge 45 | obj.hoge 46 | 47 | assert_equal 3, obj.called_times(:hoge) 48 | end 49 | 50 | def test_mock_counts_how_many_times_called_mocked_method 51 | obj = SimpleMock.new 52 | obj.expects(:imitated_method, true) 53 | obj.watch(:imitated_method) 54 | 55 | obj.imitated_method 56 | obj.imitated_method 57 | 58 | assert_equal 2, obj.called_times(:imitated_method) 59 | end 60 | 61 | def test_mock_returns_value_and_counts_how_many_times 62 | obj = SimpleMock.new 63 | obj.expects(:imitated_method, 'hoge') 64 | obj.watch(:imitated_method) 65 | 66 | assert_equal('hoge', obj.imitated_method) 67 | assert_equal('hoge', obj.imitated_method) 68 | 69 | assert_equal 2, obj.called_times(:imitated_method) 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /answers/03_method/02_define.rb: -------------------------------------------------------------------------------- 1 | # Q1. 2 | # 3 | # 問題の解説 4 | # defだとSyntaxErrorになってしまうようなメソッド名でも、define_methodを使うことでメソッドとして定義することができます。 5 | # 6 | class A1 7 | define_method '//' do 8 | '//' 9 | end 10 | end 11 | 12 | # Q2 13 | # 14 | # 問題の解説 15 | # defind_singleton_methodを利用して動的に特異メソッドを定義することで、条件2を満たしています。 16 | # define_methodはModuleのインスタンスメソッドなので、initializeメソッド中では使えません。 17 | # A2.define_methodのようにすれば使えますが、それだとA2クラスのインスタンスメソッドになるので 18 | # すべてのA2インスタンスで利用できてしまい、 19 | # 「メソッドが定義されるのは同時に生成されるオブジェクトのみで、別のA2インスタンスには(同じ値を含む配列を生成時に渡さない限り)定義されない」 20 | # という仕様を満たすことができません。 21 | # 22 | class A2 23 | def initialize(ary) 24 | ary.each do |name| 25 | method_name = "hoge_#{name}" 26 | 27 | define_singleton_method method_name do |times| 28 | if times.nil? 29 | dev_team 30 | else 31 | method_name * times 32 | end 33 | end 34 | end 35 | end 36 | 37 | def dev_team 38 | 'SmartHR Dev Team' 39 | end 40 | end 41 | 42 | # Q3. 43 | # 44 | # 問題の解説 45 | # 3章にはまだ登場していない概念ですが、includedフックを利用してモジュールがincludeされたときの振る舞いを記述しています。 46 | # my_attr_accessorメソッドはクラスメソッドに相当するため、includedメソッドの引数として渡されてきたクラスに直接define_singleton_methodでメソッドを追加しています。 47 | # さらにmy_attr_accessorメソッド実行時にインスタンスメソッドを追加するためにdefine_methodを利用しています。 48 | # セッターで定義した値を格納するために`@my_attr_accessor`をハッシュとして定義して利用しています。 49 | # `?`つきのメソッドを定義するために、セッター実行時にdefine_aingleton_methodでメソッドを追加しています。 50 | # 51 | module OriginalAccessor 52 | def self.included(base) 53 | base.define_singleton_method(:my_attr_accessor) do |attr| 54 | base.define_method attr do 55 | @my_attr_accessor&.fetch(attr) { nil } 56 | end 57 | 58 | base.define_method "#{attr}=" do |value| 59 | (@my_attr_accessor ||= {})[attr] = value 60 | 61 | if value.is_a?(TrueClass) || value.is_a?(FalseClass) 62 | define_singleton_method "#{attr}?" do 63 | !!value 64 | end 65 | end 66 | end 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /02_object_model/02_hierarchy.rb: -------------------------------------------------------------------------------- 1 | module M1 2 | def name 3 | 'M1' 4 | end 5 | end 6 | 7 | module M2 8 | def name 9 | 'M2' 10 | end 11 | end 12 | 13 | module M3 14 | def name 15 | 'M3' 16 | end 17 | end 18 | 19 | module M4 20 | def name 21 | 'M4' 22 | end 23 | end 24 | 25 | # NOTE: これより上の行は変更しないこと 26 | 27 | 28 | # Q1. 29 | # 次の動作をする C1 class を実装する 30 | # - C1.ancestors.first(2) が [C1, M1] となる 31 | # - C1.new.name が 'C1' を返す 32 | class C1 33 | def name 34 | 'C1' 35 | end 36 | end 37 | 38 | 39 | # Q2. 40 | # 次の動作をする C2 class を実装する 41 | # - C2.ancestors.first(2) が [M1, C2] となる 42 | # - C2.new.name が 'M1' を返す 43 | class C2 44 | def name 45 | 'C2' 46 | end 47 | end 48 | 49 | 50 | # Q3. 51 | # 次の動作をする C3 class, MySuperClass class を実装する 52 | # - C3.ancestors.first(6) が [M1, C3, M2, M3, MySuperClass, M4] となる 53 | # - C3.new.name が 'M1' を返す 54 | class C3 55 | def name 56 | 'C3' 57 | end 58 | end 59 | 60 | 61 | # Q4. 62 | # 次の動作をする C4 class のメソッド increment を実装する 63 | # - increment メソッドを呼ぶと value が +1 される 64 | # - また、increment メソッドは value を文字列にしたものを返す 65 | # c4 = C4.new 66 | # c4.increment # => "1" 67 | # c4.increment # => "2" 68 | # c4.increment # => "3" 69 | # - 定義済みのメソッド (value, value=) は private のままとなっている 70 | # - incrementメソッド内で value, value=を利用する 71 | class C4 72 | private 73 | 74 | attr_accessor :value 75 | end 76 | 77 | # Q5. 78 | # 次の動作をする M1Refinements module を実装する 79 | # - M1Refinements は M1 の name インスタンスメソッドをリファインし, 80 | # リファインされた name メソッドは "Refined M1" を返す 81 | # - C5.new.another_name が文字列 "M1" を返す 82 | # - C5.new.other_name が文字列 "Refined M1" を返す 83 | module M1Refinements 84 | end 85 | 86 | class C5 87 | include M1 88 | 89 | def another_name 90 | name 91 | end 92 | 93 | using M1Refinements 94 | 95 | def other_name 96 | name 97 | end 98 | end 99 | 100 | 101 | # Q6. 102 | # 次の動作をする C6 class を実装する 103 | # - M1Refinements は Q5 で実装したものをそのまま使う 104 | # - C6.new.name が 'Refined M1' を返すように C6 に name メソッドを実装する 105 | class C6 106 | include M1 107 | using M1Refinements 108 | end 109 | -------------------------------------------------------------------------------- /02_object_model/test/test_hierarchy.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require '02_hierarchy' 3 | 4 | class TestHierarchy < Minitest::Test 5 | def test_c1_ancestors 6 | assert_equal [C1, M1], C1.ancestors.first(2) 7 | end 8 | 9 | def test_c1_name 10 | assert_equal 'C1', C1.new.name 11 | end 12 | 13 | def test_c2_ancestors 14 | assert_equal [M1, C2], C2.ancestors.first(2) 15 | end 16 | 17 | def test_c2_name 18 | assert_equal 'M1', C2.new.name 19 | end 20 | 21 | def test_c3_ancestors 22 | assert_equal [M1, C3, M2, M3, MySuperClass, M4], C3.ancestors.first(6) 23 | end 24 | 25 | def test_c3_name 26 | assert_equal 'M1', C3.new.name 27 | end 28 | 29 | def test_c3_super_class 30 | assert MySuperClass.kind_of?(Class) 31 | end 32 | 33 | def test_c4_increment 34 | c4 = C4.new 35 | assert_equal "1", c4.increment 36 | assert_equal "2", c4.increment 37 | assert_equal "3", c4.increment 38 | end 39 | 40 | def test_c4_value_called 41 | c4 = C4.new 42 | c4.singleton_class.class_eval do 43 | private 44 | 45 | def value=(x) 46 | @called_setter = true 47 | @value = x 48 | end 49 | 50 | def value 51 | @called_getter = true 52 | if defined?(@value) 53 | @value 54 | else 55 | nil 56 | end 57 | end 58 | end 59 | c4.instance_variable_set(:"@called_setter", nil) 60 | c4.instance_variable_set(:"@called_getter", nil) 61 | 62 | assert_equal "1", c4.increment 63 | assert c4.instance_variable_get(:"@called_setter") 64 | assert c4.instance_variable_get(:"@called_getter") 65 | end 66 | 67 | def test_c4_value_methods 68 | assert C4.private_instance_methods.include?(:value) 69 | assert C4.private_instance_methods.include?(:value=) 70 | end 71 | 72 | def test_c5_another_name 73 | assert_equal "M1", C5.new.another_name 74 | end 75 | 76 | def test_c5_other_name 77 | assert_equal "Refined M1", C5.new.other_name 78 | end 79 | 80 | def test_c6_name 81 | assert_equal "Refined M1", C6.new.name 82 | 83 | C6.include(Module.new do 84 | def name = "other" 85 | end) 86 | assert_equal "other", C6.new.name 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /answers/02_object_model/02_hierarchy.rb: -------------------------------------------------------------------------------- 1 | module M1 2 | def name 3 | 'M1' 4 | end 5 | end 6 | 7 | module M2 8 | def name 9 | 'M2' 10 | end 11 | end 12 | 13 | module M3 14 | def name 15 | 'M3' 16 | end 17 | end 18 | 19 | module M4 20 | def name 21 | 'M4' 22 | end 23 | end 24 | 25 | # NOTE: これより上の行は変更しないこと 26 | 27 | 28 | # Q1. 問題の解説 29 | # 30 | # M1をC1にincludeすると、継承ツリーはC1の次にM1が位置することになり、仕様を満たせます。 31 | # 32 | class C1 33 | include M1 34 | 35 | def name 36 | 'C1' 37 | end 38 | end 39 | 40 | 41 | # Q2. 問題の解説 42 | # 43 | # M1をC2にprependすると、継承ツリーはM2の次にC2が位置することになり、仕様を満たせます。 44 | # 45 | class C2 46 | prepend M1 47 | 48 | def name 49 | 'C2' 50 | end 51 | end 52 | 53 | 54 | # Q3. 問題の解説 55 | # 56 | # モジュールを複数includeしたり、スーパークラスを明示的に定義したときの 57 | # 継承ツリーがどうなるかの理解を問う問題です 58 | # 59 | class MySuperClass 60 | include M4 61 | end 62 | 63 | class C3 < MySuperClass 64 | prepend M1 65 | include M3 66 | include M2 67 | 68 | def name 69 | 'C3' 70 | end 71 | end 72 | 73 | 74 | # Q4. 問題の解説 75 | # 76 | # privateメソッドとして定義していると、レシーバを明示的に指定したメソッド呼び出しができません。 77 | # しかしこれには例外があり、レシーバがselfであれば問題ありません。 78 | # この仕様はRuby2.7からのものであり、2.7未満はセッターメソッド(=が末尾についているもの)のみがselfをつけて呼び出し可能でした。 79 | class C4 80 | def increment 81 | self.value ||= 0 82 | self.value += 1 83 | value.to_s 84 | end 85 | private 86 | 87 | attr_accessor :value 88 | end 89 | 90 | # Q5. 問題の解説 91 | # 92 | # refinementsの練習問題です。 93 | # refineしたメソッドの影響範囲はusingがクラス内であれば、そのusingしたクラス内でのみ、かつusing以降の行です。 94 | module M1Refinements 95 | refine M1 do 96 | def name 97 | 'Refined M1' 98 | end 99 | end 100 | end 101 | 102 | class C5 103 | include M1 104 | 105 | def another_name 106 | name 107 | end 108 | 109 | using M1Refinements 110 | 111 | def other_name 112 | name 113 | end 114 | end 115 | 116 | 117 | # Q6. 問題の解説 118 | # 119 | # Q5の解説でも書いたように、refineしたメソッドの影響範囲はusingがクラス内であれば、そのusingしたクラス内でのみ、かつusing以降の行です。 120 | # なので、問題として用意したコードのままだとなにもrefineされず、もともとのC6#nameは'M1'を返します。 121 | # using以降の行でM1#nameを呼び出すC6#nameを定義するとrefineした実装が呼び出されます。 122 | # 123 | class C6 124 | include M1 125 | using M1Refinements 126 | 127 | def name 128 | super 129 | end 130 | end 131 | -------------------------------------------------------------------------------- /06_codes_generate_codes/test/test_simple_model.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require '01_simple_model' 3 | require 'securerandom' 4 | 5 | class TestSimpleModel < Minitest::Test 6 | class Product 7 | include SimpleModel 8 | 9 | attr_accessor :name, :description 10 | end 11 | 12 | def test_accessor 13 | obj = Product.new(name: 'SmarterHR', description: 'more smart SmartHR') 14 | assert_equal 'SmarterHR', obj.name 15 | assert_equal 'more smart SmartHR', obj.description 16 | end 17 | 18 | def test_writer 19 | obj = Product.new(name: 'SmarterHR', description: 'more smart SmartHR') 20 | obj.name = 'Ultra SmarterHR' 21 | obj.description = 'more smart SmarterHR' 22 | assert_equal 'Ultra SmarterHR', obj.name 23 | assert_equal 'more smart SmarterHR', obj.description 24 | end 25 | 26 | def test_watching_not_changes_attrs 27 | obj = Product.new(name: 'SmarterHR', description: 'more smart SmartHR') 28 | assert_equal false, obj.changed? 29 | end 30 | 31 | def test_watching_changes_attrs 32 | obj = Product.new(name: 'SmarterHR', description: 'more smart SmartHR') 33 | obj.name = 'SuperSmarterHR' 34 | assert_equal true, obj.changed? 35 | end 36 | 37 | def test_watching_changes_each_attrs 38 | obj = Product.new(name: 'SmarterHR', description: 'more smart SmartHR') 39 | obj.name = 'SuperSmarterHR' 40 | assert_equal true, obj.name_changed? 41 | assert_equal false, obj.description_changed? 42 | end 43 | 44 | def test_restore_changes 45 | obj = Product.new(name: 'SmarterHR', description: 'more smart SmartHR') 46 | obj.name = 'Ultra SmarterHR' 47 | obj.description = 'more smart SmarterHR' 48 | obj.restore! 49 | assert_equal 'SmarterHR', obj.name 50 | assert_equal 'more smart SmartHR', obj.description 51 | assert_equal false, obj.changed? 52 | end 53 | 54 | def test_random_read 55 | name = SecureRandom.hex 56 | desc = SecureRandom.hex 57 | obj = Product.new(name: name, description: desc) 58 | assert_equal name, obj.name 59 | assert_equal desc, obj.description 60 | end 61 | 62 | def test_random_write 63 | name = SecureRandom.hex 64 | desc = SecureRandom.hex 65 | obj = Product.new(name: 'SmarterHR', description: 'more smart SmartHR') 66 | obj.name = name 67 | obj.description = desc 68 | assert_equal name, obj.name 69 | assert_equal desc, obj.description 70 | end 71 | 72 | class MultipleAccessorsProduct 73 | include SimpleModel 74 | 75 | attr_accessor :name 76 | attr_accessor :description 77 | end 78 | 79 | def test_multiple_accessors 80 | obj = MultipleAccessorsProduct.new(name: 'SmarterHR', description: 'more smart SmartHR') 81 | assert_equal 'SmarterHR', obj.name 82 | assert_equal 'more smart SmartHR', obj.description 83 | end 84 | 85 | def test_multiple_accessors_writer 86 | obj = MultipleAccessorsProduct.new(name: 'SmarterHR', description: 'more smart SmartHR') 87 | obj.name = 'Ultra SmarterHR' 88 | obj.description = 'more smart SmarterHR' 89 | assert_equal 'Ultra SmarterHR', obj.name 90 | assert_equal 'more smart SmarterHR', obj.description 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /04_block/test/test_evil_mailbox.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require '02_evil_mailbox' 3 | require 'securerandom' 4 | 5 | class TestEvilMailbox < Minitest::Test 6 | def evil_mailbox(&block) 7 | mock = Minitest::Mock.new 8 | mock.instance_eval(&block) if block_given? 9 | [EvilMailbox.new(mock), mock] 10 | end 11 | 12 | def test_send_mail 13 | mb, mock = evil_mailbox do 14 | expect :send_mail, true, ["ppyd", "hello"] 15 | end 16 | mb.send_mail("ppyd", "hello") 17 | mock.verify 18 | end 19 | 20 | def test_send_mail_returns_nil 21 | mb, _ = evil_mailbox do 22 | expect :send_mail, true, ["ppyd", "hello"] 23 | end 24 | assert_nil mb.send_mail("ppyd", "hello") 25 | end 26 | 27 | def test_receive_mail 28 | mb, mock = evil_mailbox do 29 | expect :receive_mail, ["kino", "Yo"] 30 | end 31 | f, t = mb.receive_mail 32 | mock.verify 33 | assert_equal "kino", f 34 | assert_equal "Yo", t 35 | end 36 | 37 | def test_send_mail_exec_block_with_result_true 38 | mb, _ = evil_mailbox do 39 | expect :send_mail, true, ["ppyd", "hello"] 40 | end 41 | ret = nil 42 | mb.send_mail("ppyd", "hello") do |res| 43 | ret = res 44 | end 45 | assert_equal true, ret 46 | end 47 | 48 | def test_send_mail_exec_block_with_result_false 49 | mb, _ = evil_mailbox do 50 | expect :send_mail, false, ["ppyd", "hello"] 51 | end 52 | ret = nil 53 | mb.send_mail("ppyd", "hello") do |res| 54 | ret = res 55 | end 56 | assert_equal false, ret 57 | end 58 | 59 | def test_mail_object_auth 60 | secret_string = SecureRandom.hex 61 | mock = Minitest::Mock.new 62 | mock.expect :auth, true, [String] 63 | EvilMailbox.new(mock, secret_string) 64 | mock.verify 65 | end 66 | 67 | def test_send_mail_with_secret_string 68 | secret_string = SecureRandom.hex 69 | mock = Minitest::Mock.new 70 | mock.expect :auth, true, [String] 71 | mock.expect :send_mail, true, ["ppyd", "hello#{secret_string}"] 72 | mb = EvilMailbox.new(mock, secret_string) 73 | 74 | mb.send_mail("ppyd", "hello") 75 | mock.verify 76 | end 77 | 78 | def test_no_secret_string_in_object 79 | secret_string = SecureRandom.hex 80 | mock = Minitest::Mock.new 81 | mock.expect :auth, true, [String] 82 | mb = EvilMailbox.new(mock, secret_string) 83 | 84 | mock.verify 85 | mb.class.send(:class_variables).each do |cv| 86 | assert_equal false, secret_string == mb.class.get_class_variable(cv) 87 | end 88 | mb.send(:instance_variables).each do |iv| 89 | assert_equal false, secret_string == mb.instance_variable_get(iv) 90 | end 91 | end 92 | 93 | def evil_mailbox_with_secret_string(secret_string, &block) 94 | mock = Minitest::Mock.new 95 | mock.instance_eval(&block) if block_given? 96 | [EvilMailbox.new(mock, secret_string), mock] 97 | end 98 | 99 | def test_send_mail_exec_block_with_result_true_and_secret_string 100 | secret_string = SecureRandom.hex 101 | mb, mock = evil_mailbox_with_secret_string(secret_string) do 102 | expect :auth, true, [String] 103 | expect :send_mail, true, ["ppyd", "hello#{secret_string}"] 104 | end 105 | 106 | ret = nil 107 | mb.send_mail("ppyd", "hello") do |res| 108 | ret = res 109 | end 110 | mock.verify 111 | assert_equal true, ret 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /03_method/03_try_over3_3.rb: -------------------------------------------------------------------------------- 1 | TryOver3 = Module.new 2 | # Q1 3 | # 以下要件を満たすクラス TryOver3::A1 を作成してください。 4 | # - run_test というインスタンスメソッドを持ち、それはnilを返す 5 | # - `test_` から始まるインスタンスメソッドが実行された場合、このクラスは `run_test` メソッドを実行する 6 | # - `test_` メソッドがこのクラスに実装されていなくても `test_` から始まるメッセージに応答することができる 7 | # - TryOver3::A1 には `test_` から始まるインスタンスメソッドが定義されていない 8 | 9 | 10 | # Q2 11 | # 以下要件を満たす TryOver3::A2Proxy クラスを作成してください。 12 | # - TryOver3::A2Proxy は initialize に TryOver3::A2 のインスタンスを受け取り、それを @source に代入する 13 | # - TryOver3::A2Proxy は、@sourceに定義されているメソッドが自分自身に定義されているように振る舞う 14 | class TryOver3::A2 15 | def initialize(name, value) 16 | instance_variable_set("@#{name}", value) 17 | self.class.attr_accessor name.to_sym unless respond_to? name.to_sym 18 | end 19 | end 20 | 21 | 22 | # Q3. 23 | # 02_define.rbのQ3ではOriginalAccessor の my_attr_accessor で定義した getter/setter に 24 | # boolean の値が入っている場合には #{name}? が定義されるようなモジュールを実装しました。 25 | # 今回は、そのモジュールに boolean 以外が入っている場合には #{name}? メソッドが存在しないようにする変更を加えてください。 26 | # (以下のコードに変更を加えてください) 27 | # 28 | module TryOver3::OriginalAccessor2 29 | def self.included(mod) 30 | mod.define_singleton_method :my_attr_accessor do |name| 31 | define_method name do 32 | @attr 33 | end 34 | 35 | define_method "#{name}=" do |value| 36 | if [true, false].include?(value) && !respond_to?("#{name}?") 37 | self.class.define_method "#{name}?" do 38 | @attr == true 39 | end 40 | end 41 | @attr = value 42 | end 43 | end 44 | end 45 | end 46 | 47 | 48 | # Q4 49 | # 以下のように実行できる TryOver3::A4 クラスを作成してください。 50 | # TryOver3::A4.runners = [:Hoge] 51 | # TryOver3::A4::Hoge.run 52 | # # => "run Hoge" 53 | # このとき、TryOver3::A4::Hogeという定数は定義されません。 54 | 55 | 56 | # Q5. チャレンジ問題! 挑戦する方はテストの skip を外して挑戦してみてください。 57 | # 58 | # TryOver3::TaskHelper という include すると task というクラスマクロが与えられる以下のようなモジュールがあります。 59 | module TryOver3::TaskHelper 60 | def self.included(klass) 61 | klass.define_singleton_method :task do |name, &task_block| 62 | new_klass = Class.new do 63 | define_singleton_method :run do 64 | puts "start #{Time.now}" 65 | block_return = task_block.call 66 | puts "finish #{Time.now}" 67 | block_return 68 | end 69 | end 70 | new_klass_name = name.to_s.split("_").map{ |w| w[0] = w[0].upcase; w }.join 71 | const_set(new_klass_name, new_klass) 72 | end 73 | end 74 | end 75 | 76 | # TryOver3::TaskHelper は include することで以下のような使い方ができます 77 | class TryOver3::A5Task 78 | include TryOver3::TaskHelper 79 | 80 | task :foo do 81 | "foo" 82 | end 83 | end 84 | # irb(main):001:0> TryOver3::A3Task::Foo.run 85 | # start 2020-01-07 18:03:10 +0900 86 | # finish 2020-01-07 18:03:10 +0900 87 | # => "foo" 88 | 89 | # 今回 TryOver3::TaskHelper では TryOver3::A5Task::Foo のように Foo クラスを作らず 90 | # TryOver3::A5Task.foo のようにクラスメソッドとして task で定義された名前のクラスメソッドでブロックを実行するように変更したいです。 91 | # 現在 TryOver3::TaskHelper のユーザには TryOver3::A5Task::Foo.run のように生成されたクラスを使って実行しているユーザが存在します。 92 | # 今回変更を加えても、その人たちにはこれまで通り生成されたクラスのrunメソッドでタスクを実行できるようにしておいて、 93 | # warning だけだしておくようにしたいです。 94 | # TryOver3::TaskHelper を修正してそれを実現してください。 なお、その際、クラスは実行されない限り生成されないものとします。 95 | # 96 | # 変更後想定する使い方 97 | # メソッドを使ったケース 98 | # irb(main):001:0> TryOver3::A5Task.foo 99 | # start 2020-01-07 18:03:10 +0900 100 | # finish 2020-01-07 18:03:10 +0900 101 | # => "foo" 102 | # 103 | # クラスのrunメソッドを使ったケース 104 | # irb(main):001:0> TryOver3::A5Task::Foo.run 105 | # Warning: TryOver3::A5Task::Foo.run is deprecated 106 | # start 2020-01-07 18:03:10 +0900 107 | # finish 2020-01-07 18:03:10 +0900 108 | # => "foo" 109 | -------------------------------------------------------------------------------- /answers/03_method/03_try_over3_3.rb: -------------------------------------------------------------------------------- 1 | TryOver3 = Module.new 2 | # Q1. 問題の解説 3 | # 4 | # method_missingを利用してゴーストメソッドを作る問題です。 5 | # respond_to_missing?はなくてもテストはパスしますが、method_missingを作るときにはセットで 6 | # 定義しておくのがお作法なので回答例にはrespond_to_missing?も定義しています。 7 | # 8 | class TryOver3::A1 9 | def run_test 10 | end 11 | 12 | def method_missing(name, *) 13 | if name.to_s.start_with?('test_') 14 | run_test 15 | else 16 | super 17 | end 18 | end 19 | 20 | def respond_to_missing?(name, _) 21 | name.to_s.start_with?('test_') 22 | end 23 | end 24 | 25 | # Q2. 問題の解説 26 | # 27 | # method_missingとsendを使って動的プロキシを作る問題です。 28 | # Q1と違い、こちらはrespond_to_missing?がないとテストが失敗します。 29 | # 30 | class TryOver3::A2 31 | def initialize(name, value) 32 | instance_variable_set("@#{name}", value) 33 | self.class.attr_accessor name.to_sym unless respond_to? name.to_sym 34 | end 35 | end 36 | 37 | class TryOver3::A2Proxy 38 | def initialize(source) 39 | @source = source 40 | end 41 | 42 | def method_missing(...) 43 | @source.send(...) 44 | end 45 | 46 | def respond_to_missing?(name, include_all) 47 | @source.respond_to?(name, include_all) 48 | end 49 | end 50 | 51 | # Q3. 52 | # Module#remove_methodを利用するとメソッドを削除できます。これを使い、 53 | # 「boolean 以外が入っている場合には #{name}? メソッドが存在しないようにする」を実現します。 54 | # なお、メソッドを削除するメソッドはremove_methodの他にundef_methodも存在します。こちらでもテストはパスします。 55 | # remove_methodとundef_methodの違いが気になる方はドキュメントを読んでみてください。 56 | # 57 | module TryOver3::OriginalAccessor2 58 | def self.included(mod) 59 | mod.define_singleton_method :my_attr_accessor do |name| 60 | define_method name do 61 | @attr 62 | end 63 | 64 | define_method "#{name}=" do |value| 65 | if [true, false].include?(value) && !respond_to?("#{name}?") 66 | self.class.define_method "#{name}?" do 67 | @attr == true 68 | end 69 | else 70 | mod.remove_method "#{name}?" if respond_to? "#{name}?" 71 | end 72 | @attr = value 73 | end 74 | end 75 | end 76 | end 77 | 78 | 79 | # Q4. 問題の解説 80 | # 81 | # const_missingを利用して、runners=で定義した定数を参照したときにrunメソッドを持つオブジェクトを返すことで 82 | # 仕様を満たしています。回答例ではObject.newでオブジェクトを生成しましたが、runメソッドを持つオブジェクトであれば 83 | # どんなクラスのインスタンスでもOKです。 84 | # 85 | class TryOver3::A4 86 | def self.const_missing(const) 87 | if @consts.include?(const) 88 | obj = Object.new 89 | obj.define_singleton_method(:run) { "run #{const}" } 90 | obj 91 | else 92 | super 93 | end 94 | end 95 | 96 | def self.runners=(consts) 97 | @consts = consts 98 | end 99 | end 100 | 101 | # Q5. 問題の解説 102 | # 103 | # これまで解いてきた問題の解法と、仕様を読み解く知識が問われる問題です。 104 | # 2種類の書き方で同一の処理を行うが、そのうち1つは追加でdeprecation warningを出します。 105 | # メソッドの実態はdefine_singleton_methodで定義し、もう1つはQ4と同様にconst_misisingを使い、 106 | # runメソッド実行時にsendでもともとの定義を呼びだします。 107 | # 108 | # taskで定義されていないタスク名を定数として参照したときは既存のconst_missingの処理を継続させたいので 109 | # superを実行しています。 110 | 111 | module TryOver3::TaskHelper 112 | def self.included(klass) 113 | klass.define_singleton_method :task do |name, &task_block| 114 | define_singleton_method name do 115 | puts "start #{Time.now}" 116 | block_return = task_block.call 117 | puts "finish #{Time.now}" 118 | block_return 119 | end 120 | 121 | define_singleton_method(:const_missing) do |const| 122 | super(const) unless klass.respond_to?(const.downcase) 123 | 124 | obj = Object.new 125 | obj.define_singleton_method :run do 126 | warn "Warning: TryOver3::A5Task::#{const}.run is deprecated" 127 | klass.send name 128 | end 129 | obj 130 | end 131 | end 132 | end 133 | end 134 | 135 | class TryOver3::A5Task 136 | include TryOver3::TaskHelper 137 | 138 | task :foo do 139 | "foo" 140 | end 141 | end 142 | -------------------------------------------------------------------------------- /03_method/test/test_try_over3_3.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "03_try_over3_3" 3 | 4 | class TestTryOver03Q1 < Minitest::Test 5 | def test_q1_called_run_test 6 | a1 = TryOver3::A1.new 7 | mock = Minitest::Mock.new 8 | a1.stub(:run_test, mock) do 9 | a1.test_hoge 10 | end 11 | assert mock.verify 12 | end 13 | 14 | def test_q1_run_raise_error 15 | assert_raises(NoMethodError) { TryOver3::A1.new.testhoge } 16 | end 17 | 18 | def test_q1_methods_not_included_test 19 | assert_equal false, TryOver3::A1.instance_methods(false).any? { |method_name| method_name.to_s.start_with?("test_") } 20 | end 21 | 22 | def test_q2_proxy_foo 23 | source = TryOver3::A2.new("foo", "foofoo") 24 | assert_equal "foofoo", TryOver3::A2Proxy.new(source).foo 25 | end 26 | 27 | def test_q2_proxy_hoge_writer 28 | source = TryOver3::A2.new("foo", "foo") 29 | proxy = TryOver3::A2Proxy.new(source) 30 | proxy.foo = "foofoo" 31 | assert_equal "foofoo", proxy.foo 32 | end 33 | 34 | def test_q2_proxy_rand 35 | name = alpha_rand 36 | source = TryOver3::A2.new(name, "foo") 37 | assert_equal "foo", TryOver3::A2Proxy.new(source).public_send(name) 38 | end 39 | 40 | def test_q2_proxy_respond_to_foo 41 | source = TryOver3::A2.new("foo", "foofoo") 42 | assert_respond_to TryOver3::A2Proxy.new(source), :foo 43 | end 44 | 45 | def test_q2_proxy_methods_not_included_foo 46 | source = TryOver3::A2.new("foo", "foofoo") 47 | refute_includes TryOver3::A2Proxy.new(source).methods, :foo 48 | end 49 | 50 | def test_q3_original_accessor_boolean_method 51 | instance = orignal_accessor_included_instance 52 | instance.hoge = true 53 | assert_equal(true, instance.hoge?) 54 | instance.hoge = "hoge" 55 | assert_raises(NoMethodError) { instance.hoge? } 56 | refute_includes(instance.methods, :hoge?) 57 | end 58 | 59 | def test_q3_original_accessor_boolean_method_reverse 60 | instance = orignal_accessor_included_instance 61 | instance.hoge = "hoge" 62 | assert_raises(NoMethodError) { instance.hoge? } 63 | refute_includes(instance.methods, :hoge?) 64 | instance.hoge = true 65 | assert_equal(true, instance.hoge?) 66 | end 67 | 68 | def test_q4_call_class 69 | TryOver3::A4.runners = [:Hoge] 70 | assert_equal "run Hoge", TryOver3::A4::Hoge.run 71 | end 72 | 73 | def test_q4_raise_error_when_called_not_runner_class 74 | TryOver3::A4.runners = [:Hoge] 75 | assert_raises(NameError) { TryOver3::A4::Foo } 76 | end 77 | 78 | def test_q4_not_exists_runner_class 79 | TryOver3::A4.runners = [:Hoge] 80 | refute_includes(TryOver3::A4.constants, :Hoge) 81 | end 82 | 83 | def test_q5_task_helper_call_method 84 | skip unless ENV["CI"] 85 | assert_equal("foo", TryOver3::A5Task.foo) 86 | end 87 | 88 | def test_q5_task_helper_not_exists_class 89 | skip unless ENV["CI"] 90 | refute_includes TryOver3::A5Task.constants, :Foo 91 | end 92 | 93 | def test_q5_task_helper_call_class 94 | skip unless ENV["CI"] 95 | assert_equal("foo", TryOver3::A5Task::Foo.run) 96 | end 97 | 98 | def test_q5_task_helper_call_class_with_warn 99 | skip unless ENV["CI"] 100 | _, err = capture_io do 101 | TryOver3::A5Task::Foo.run 102 | end 103 | assert_match "Warning: TryOver3::A5Task::Foo.run is deprecated", err 104 | end 105 | 106 | def test_q5_error_when_called_not_defined_task_class 107 | assert_raises(NameError) { TryOver3::A5Task::Bar.run } 108 | end 109 | 110 | private 111 | 112 | def alpha_rand(size = 8) 113 | alphabets = [*"a".."z"] 114 | (0..size).map { alphabets[rand(alphabets.size)] }.join 115 | end 116 | 117 | def orignal_accessor_included_instance 118 | Class.new do 119 | include TryOver3::OriginalAccessor2 120 | my_attr_accessor :hoge 121 | end.new 122 | end 123 | end 124 | -------------------------------------------------------------------------------- /03_method/test/test_define.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require '02_define' 3 | require 'securerandom' 4 | 5 | class TestDefine < Minitest::Test 6 | 7 | begin 8 | class A3 9 | include OriginalAccessor 10 | my_attr_accessor :hoge 11 | my_attr_accessor :fuga 12 | end 13 | rescue 14 | end 15 | 16 | def test_answer_a1 17 | assert_equal "//", A1.new.send("//".to_sym) 18 | end 19 | 20 | def test_answer_a1_define 21 | assert_equal true, A1.new.methods.include?("//".to_sym) 22 | end 23 | 24 | def test_answer_a2 25 | instance = A2.new(["hoge", "fuga"]) 26 | 27 | assert_equal true, instance.methods.include?(:dev_team) 28 | assert_equal "SmartHR Dev Team", instance.hoge_hoge(nil) 29 | assert_equal "hoge_hogehoge_hoge", instance.hoge_hoge(2) 30 | assert_equal "hoge_fugahoge_fugahoge_fuga", instance.hoge_fuga(3) 31 | 32 | another_instance = A2.new([]) 33 | assert_equal false, another_instance.methods.include?(:hoge_hoge) 34 | assert_equal false, another_instance.methods.include?(:hoge_fuga) 35 | end 36 | 37 | def test_answer_a2_number 38 | instance = A2.new([1, 2]) 39 | 40 | assert_equal true, instance.methods.include?(:dev_team) 41 | assert_equal "SmartHR Dev Team", instance.hoge_1(nil) 42 | assert_equal "hoge_1hoge_1", instance.hoge_1(2) 43 | assert_equal "hoge_2hoge_2hoge_2", instance.hoge_2(3) 44 | 45 | another_instance = A2.new([]) 46 | assert_equal false, another_instance.methods.include?(:hoge_1) 47 | assert_equal false, another_instance.methods.include?(:hoge_2) 48 | end 49 | 50 | def test_answer_a2_random_name 51 | value_one = SecureRandom.hex 52 | value_two = SecureRandom.hex 53 | 54 | instance = A2.new([value_one, value_two]) 55 | assert_equal true, instance.methods.include?(:dev_team) 56 | assert_equal "SmartHR Dev Team", instance.send("hoge_#{value_one}".to_sym, nil) 57 | assert_equal "hoge_#{value_one}hoge_#{value_one}", instance.send("hoge_#{value_one}".to_sym, 2) 58 | assert_equal "hoge_#{value_two}hoge_#{value_two}hoge_#{value_two}", instance.send("hoge_#{value_two}".to_sym, 3) 59 | 60 | another_instance = A2.new([]) 61 | assert_equal false, another_instance.methods.include?("hoge_#{value_one}".to_sym) 62 | assert_equal false, another_instance.methods.include?("hoge_#{value_two}".to_sym) 63 | end 64 | 65 | def test_answer_a2_called_dev_team 66 | instance = A2.new([1]) 67 | 68 | @called_dev_team = false 69 | trace = TracePoint.new(:call) do |tp| 70 | @called_dev_team = tp.event == :call && tp.method_id == :dev_team unless @called_dev_team 71 | end 72 | trace.enable 73 | instance.hoge_1(nil) 74 | trace.disable 75 | 76 | assert_equal true, @called_dev_team 77 | end 78 | 79 | def test_answer_a3_define 80 | assert_equal true, A3.methods.include?(:my_attr_accessor) 81 | end 82 | 83 | def test_answer_a3_string 84 | instance = A3.new 85 | instance.hoge = "1" 86 | 87 | assert_equal false, instance.methods.include?(:hoge?) 88 | assert_equal "1", instance.hoge 89 | end 90 | 91 | def test_answer_a3_number 92 | instance = A3.new 93 | instance.hoge = 1 94 | 95 | assert_equal false, instance.methods.include?(:hoge?) 96 | assert_equal 1, instance.hoge 97 | end 98 | 99 | def test_answer_a3_array 100 | instance = A3.new 101 | instance.hoge = [1, 2] 102 | 103 | assert_equal false, instance.methods.include?(:hoge?) 104 | assert_equal [1, 2], instance.hoge 105 | end 106 | 107 | def test_answer_a3_boolean_true 108 | instance = A3.new 109 | instance.hoge = true 110 | assert_equal true, instance.methods.include?(:hoge?) 111 | assert_equal true, instance.hoge? 112 | end 113 | 114 | def test_answer_a3_boolean_false 115 | instance = A3.new 116 | instance.hoge = false 117 | assert_equal true, instance.methods.include?(:hoge?) 118 | assert_equal false, instance.hoge? 119 | end 120 | 121 | def test_answer_a3_multiple 122 | instance = A3.new 123 | instance.hoge = "hoge" 124 | instance.fuga = "fuga" 125 | assert_equal "hoge", instance.hoge 126 | assert_equal "fuga", instance.fuga 127 | end 128 | end 129 | --------------------------------------------------------------------------------