ブロック、コールバックの使い方の一例

>どうもaono1234と申します。記事がいいなと思ったらtwitterのフォローもお待ちしております‼
https://twitter.com/takeshi_program


皆さん、ブロック（コールバックメソッド）は使用されてますでしょうか？
自分は今日まで使ったことがありませんでした。😥

どうも使いどころが分からないんですよね～
しかし、やっと使いどころが1つ明らかになりましたので共有させて頂きます。

## はじめに
本記事は以下の読者様が対象です。
<br>

* <b>プログラミング初心者

* <b>コールバックメソッド（ブロック）について知りたい


本記事ではRubyを題材に例題コードを作成しているため、コールバックではなくブロックと呼称させて頂きます。

## 先に結論
* ブロックを使うことで複数のメソッドをつなぎ合わせることができる。
* メソッドを拡張する際によりブロックを使えば、抽象的な名前に変更しなくてよくなる。

## 背景
私はオリジナルのスクレイピングコードを書いており、
以下のような自作メソッドを定義しておりました。

このメソッドはスクレイピングするページのurl集合`url_list`を配列形式で渡された時、
そのページに特定の文字があればそのページのindex番号を返すというメソッドです。
削除ページ番号を教えてくれるメソッド

```ruby:delete_pages.select.rb
def delete_pages_select(driver, url_list, element_type, element_name, *filter)
    element_type = element_type.to_sym if element_type.class == String #element_typeが文字列だったらシンボルに変換する
    delete_index = []
    block_result = []
    url_list.each_with_index{|url, index| #
      puts "execute #{url} #{index}/#{url_list.size} "
      driver.get url                                   
      sleep(1)
      puts "get page"
      article = driver.find_element(element_type, element_name).text
      puts "find element"
      delete_index << index if article.include?(filter, all: false)
    }
    puts "seleted delete_index"
    delete_index
  end
```
::: note
include?メソッドは通常引数に配列は取れないのですが、以下のページを参考に配列形式でも引数として取得できるように拡張させています。(配列内のどれかのワードが含まれていればtrueを返すように)
https://qiita.com/takaram/items/fdc2b1897d68b3f2d848
:::


この自作メソッド`delete_pages_select`を以下のようにメインクラスで使っておりました。

```ruby:main
delete_list = []
delete_list = delete_pages_select(driver, url_list, "class", "css-1hbyyce", "アウトソーシング", "派遣", "SES")
```

その後、別の機能も実装したいと思いました。

その機能とは「ページに特定のキーワードがあれば知らせる」というものです。
例えばこんな感じ↓
```ruby:search_word_at_pag.rb
def search_word_at_page(driver, url, element_type, element_name, *search_words)
    element_type = element_type.to_sym if element_type.class == String    
    driver.get url    
    text = driver.find_element(element_type, element_name).text
    result_index =[]
    search_words.each{|search_word|
      result_index << search_word if text.include?(search_word)
    }
    result_index
  end
```

これで2つのメソッドを定義できたのであとはmain.rbでがっしゃんこすればOKかな～😎
ということで以下のように書きました。
```ruby:main
delete_list = []
search_result_list = []
delete_list = delete_pages_select(driver, url_list, "class", "test", "アウトソーシング", "派遣", "SES")
url_list.each{
    search_result_list = search_word_at_page(driver, url, "class", "test", "1年以上")
}
```
これでちゃんと動作はしたのですが、1つ問題がありました。

## 問題
上のmain.rbの問題とは`delete_pages_select`も`search_word_at_page`もどちらも`driver.get`を使っている所です。
driver.getとは何かというと seleniumでよく使うメソッドでgetメソッドでページを読み込むメソッドです。

>参考記事: https://qiita.com/mochio/items/dc9935ee607895420186


どうなるかというと、例えば1000ページ分の内容をmain.rbでスキャンしたとします。そうすると、ページは2倍の2000回getリクエストされます。
なぜならば`delete_pages_selectメソッド`と`search_words_at_pageメソッド`2つのメソッドが各自でページをgetしているからです。😢

なのでget 1回で`delete_pages_selectメソッド`の処理と`search_words_at_pageメソッド`の処理ができるようにしたいと思います。
### 複数getリクエストの解決方法
```ruby:main.rb
result_list = []
result_list = delete_pages_select(driver, url_list, "class", "test", "アウトソーシング", "派遣", "SES")
```

```ruby:delete_pages.select.rb
def delete_pages_select(driver, url_list, element_type, element_name, *filter)
    element_type = element_type.to_sym if element_type.class == String
    delete_index = []
    block_result = []
    url_list.each_with_index{|url, index|
      puts "execute #{url} #{index}/#{url_list.size} "
      driver.get url     #http GETリクエスト
      search_word_at_page(driver, url, "class", "test", "1年以上") #追加コード
      sleep(1)
      puts "get page"
      article = driver.find_element(element_type, element_name).text
      puts "find element"
      delete_index << index if article.include?(filter, all: false)
    }
    puts "seleted delete_index"
    delete_index
  end
```

```ruby:search_word_at_pag.rb
def search_word_at_page(driver, url, element_type, element_name, *search_words)
    element_type = element_type.to_sym if element_type.class == String
    text = driver.find_element(element_type, element_name).text
    result_index =[]
    search_words.each{|search_word|
      result_index << search_word if text.include?(search_word)
    }
    result_index
  end
```
`delete_pages.selectメソッド`の中で`search_words_at_pageメソッド`を呼び出すようにしました！！🤗


…ただ、このコードも問題があります。😱
これ、メソッド名はdeleteするものを選ぶぜ！😎って言っているのに、見えないところで検索もしてるよん！😘って感じで
名前とやっている事が乖離しているんですね～（詐欺やん！）

なのでメソッド名を命名し直したりする必要があるのですが、
自分はブロック（コールバックメソッド）を使うことで、楽に対応できました。

## ブロックによる解決案
```ruby:main.rb
result_list = delete_pages_select(driver, url_list, "class", "css-1hbyyce", "アウトソーシング", "派遣", "SES"){|driver, url| search_word_at_page(driver, url, "class", "css-1m9s1sn", "1年以上")}
```

```ruby:delete_pages.select.rb
def delete_pages_select(driver, url_list, element_type, element_name, *filter, &block) ######ブロックを追加
    element_type = element_type.to_sym if element_type.class == String
    delete_index = []
    block_result = []
    url_list.each_with_index{|url, index|
      puts "execute #{url} #{index}/#{url_list.size} "
      driver.get url
      sleep(1)
      puts "get page"
      article = driver.find_element(element_type, element_name).text
      puts "find element"
      delete_index << index if article.include?(filter, all: false)
      block_result << block.call(driver, url) if block_given? ######ブロックを呼び出す
    }
    puts "seleted delete_index"
    [delete_index, block_result]
end
```

```ruby:search_word_at_page.rb
def search_word_at_page(driver, url, element_type, element_name, *search_words)
    element_type = element_type.to_sym if element_type.class == String
    text = driver.find_element(:id, "__next").text
    result_index =[]
    search_words.each{|search_word|
      result_index << search_word if text.include?(search_word)
    }
    result_index
end
```

ブロックを追加することで`delete_pages.select`を`main.rb`で呼び出す際に`search.word_at_page`も引数として渡せるようになりました。blockを使うことで大工事する必要がなくなりました！

## おわりに
この経験からブロック（コールバック）とは何か？というのが少し見えてきました。
ブロックとは…メソッドの定義を大幅に変えることなく、拡張することができる便利な仕組みという捉え方をするようになりました。

また発見があれば記事にしたいと思います。
ありがとうございました。👍