RuboCop導入〜プログラム修正まで

## はじめに
今回、学習のためにブラックジャックゲームを作成したのですが、その際にコードが長く複雑になり過ぎてしまったためRuboCopを使用して修正を図っていきました。
その際の操作を備忘録として残したいと思い、本記事に書き起こしていこうと思います。

## RuboCopとは？
RuboCopは、Rubyプログラムの静的コード解析ツールのことで、コードの品質をチェックし、一貫性のあるコーディングスタイルを維持することを目的としています。
詳しくは[こちら](https://rubocop.readthedocs.io/en/latest/)をご参照ください。

## 基本的な使い方
それではRuboCopをインストールしていきたいと思います。
今回はRubyアプリに組み込むことを想定して、Gemfileに記述する方法をご紹介します。

最初にGemfileがなかったので雛形を作成するために`bundle init`を実行します。

```terminal
% bundle init
Writing new Gemfile to /Users/[UserName]/Desktop/milliontech-sample-pj/Gemfile
```
ちなみにgemfileの中身は以下のようになっています。

```ruby:Gemfile
# frozen_string_literal: true

source 'https://rubygems.org'

git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }

# gem "rails"
```
ここに`gem 'rubocop', require:false`を追加します。

require:falseは、**Rubocopがインストールされた後に自動的にロードされないようにするため**のオプションです。このオプションを使用することで、**プロジェクトの起動時にRubocopが実行されるのを防止し、必要なときに手動でRubocopを実行することができます。**

追加したら`bundle install`を実行しましょう。


そして、下記がRuboCopの基本コマンドです。
```terminal
% rubocop
# RuboCopによるチェックがスタートし、結果が表示される。

% rubocop -a
# RuboCopによる自動修正が行われる。

% rubocop --help
# その他オプションの確認

```
それでは上記に則って実行してみます。
```terminal
% rubocop
zsh: command not found: rubocop
```

先ほどの手順でRubyのコード解析ツール「Rubocop」の利用を試みましたが、エラーが出てしまい実行できませんでした・・・
[こちらの記事](https://qiita.com/KazuyaInoue/items/54d2f73a52891d7f5637)によるとrbenvを利用している場合、`bundle install`を実行しただけでは**shimディレクトリが更新されない**ため、実行できないみたいです。

そのため、`rbenv rehash`を実行することで、どのディレクトリにいてもrubocopコマンドが使えるようになるみたいです。

```terminal
% rbenv rehash
% rubocop
Inspecting 44 files ・・・
```
無事に導入が完了しました。
それでは本題であるRuboCopによるプログラムの修正を図っていきます。

## プログラムの修正 
今回は下記のプログラムの修正を図っていきます。

```ruby:blackjack.rb
class Card 
  attr_reader :suit,:value

  def initialize(suit, value) 
    @suit = suit
    @value = value
  end

  def to_s 
    "#{suit}の#{value}"
  end

  def point 
    case value
    when "A"
      11
    when "J", "Q", "K"
      10
    else
      value.to_i
    end
  end
end

class Deck
  SUITS = ["ハート", "ダイヤ", "クラブ", "スペード"]
  VALUES = (2..10).to_a.map { |num| num.to_s }+ %w[J Q K A] 
  
  def initialize
    @cards = SUITS.product(VALUES).map { |suit, value| Card.new(suit, value) }
    @cards.shuffle!
  end

  def draw
    @cards.pop
  end
end

class Player
  attr_reader :hand, :score

  def initialize
    @hand = []
    @score = 0
  end

  def hit(card)
    @hand << card
    update_score(card)
  end

  private

  def update_score(card)
    @score += card.point
    adjust_for_ace if @score > 21 && aces_in_hand?
  end

  def aces_in_hand?
    @hand.any? { |card| card.value == "A" }
  end

  def adjust_for_ace
    @hand.each do |card|
      if card.value == "A" && @score > 21
        @score -= 10
      end
    end
  end
end

def play_blackjack
  puts "ブラックジャックを開始します。"
  deck = Deck.new
  player = Player.new
  dealer = Player.new

  2.times do
    player.hit(deck.draw)
    dealer.hit(deck.draw)
  end

  puts "あなたの引いたカードは#{player.hand[0]}です。"
  puts "あなたの引いたカードは#{player.hand[1]}です。"
  puts "ディーラーの引いたカードは#{dealer.hand[0]}です。"
  puts "ディーラーの引いた2枚目のカードはわかりません。"

  loop do
      puts "あなたの現在の得点は#{player.score}です。カードを引きますか？（Y/N）"
      response = gets.chomp.upcase
    
      if response == "Y"
        new_card = deck.draw
        player.hit(new_card)
        puts "あなたの引いたカードは#{new_card}です。"
      else
        break
      end
    
      break if player.score > 21  
    end

  puts "あなたの現在の得点は#{player.score}です。"

  if player.score > 21
    puts "あなたの負けです！"
  else
    puts "ディーラーの引いた2枚目のカードは#{dealer.hand[1]}でした。"
    while dealer.score < 17
      new_card = deck.draw
      dealer.hit(new_card)
      puts "ディーラーの引いたカードは#{new_card}です。"
    end

    puts "ディーラーの現在の得点は#{dealer.score}です。"

    if dealer.score > 21 || player.score > dealer.score
      puts "あなたの勝ちです！"
    elsif dealer.score == player.score
      puts "引き分けです。"
    else
      puts "あなたの負けです！"
    end
  end

  puts "ブラックジャックを終了します。"
end

play_blackjack
```
これだけだとわかりにくいのでざっくりと説明します。
```
[プログラムの説明]
このプログラムは、ブラックジャックと呼ばれるトランプゲームをRubyで実装しています。
このプログラムは、3つのクラス（Card(カード)、Deck(デッキ)、Player(プレイヤー)）を定義しています。以下は各クラスの機能の説明です。

・ Cardクラス：トランプのカードを表すクラス。カードのスート(クラブ、ハート、ダイヤ、スペードのマークを指す言葉)と値をインスタンス変数として持ちます。また、カードの得点を計算するメソッドも定義しています。
・ Deckクラス：トランプのデッキを表すクラス。全ての52枚のカードを持ち、カードをシャッフルし、カードを1枚ずつ引くことができます。
・ Playerクラス：プレイヤーを表すクラス。プレイヤーはカードを持ち、現在の得点を計算することができます。また、カードを引くこともできます。

最後に、play_blackjackメソッドは、実際にブラックジャックのゲームを実行します。
デッキと2人のプレイヤー（プレイヤーとディーラー）を作成し、カードを配り、プレイヤーにカードを引かせ、ディーラーがカードを引くループを実行し、勝者を決定します。
勝者は、最高得点が21を超えないようにカードを選択したプレイヤーです。
```

ではこの状態で`rubocop -a`を実行します。

```terminal
% rubocop -a
The following cops were added to RuboCop, but are not configured. Please set Enabled to either `true` or `false` in your `.rubocop.yml` file.

Please also note that you can opt-in to new cops by default by adding this to your config:
  AllCops:
    NewCops: enable
      ・
      ・
      ・
2 files inspected, 66 offenses detected, 59 offenses corrected, 3 more offenses can be corrected with `rubocop -A`
```
結構修正してくれましたね。
残されたエラーを見ていきましょう。
まずは上記の文をお馴染みの[deep](https://www.deepl.com/translator)で翻訳してみます。

`（訳）2つのファイルを検査し、69の違反が検出され、59の違反が修正され、さらに3つの違反が rubocop -A で修正可能です。`

言われた通り実行してみます。
```terminal
% rubocop -A 
The following cops were added to RuboCop, but are not configured. Please set Enabled to either `true` or `false` in your `.rubocop.yml` file.

Please also note that you can opt-in to new cops by default by adding this to your config:
  AllCops:
    NewCops: enable
　　　　　　　　　　・
　　　　　　　　　　・
　　　　　　　　　　・
2 files inspected, 11 offenses detected, 4 offenses corrected
```
`
（訳）2ファイル検査、11件の違反が検出、4件の違反が修正
`
残りエラーが7つあるみたいですね。

あと先ほどから出ている下記のエラー文なんですが、
```
The following cops were added to RuboCop, but are not configured. Please set Enabled to either `true` or `false` in your `.rubocop.yml` file.

Please also note that you can opt-in to new cops by default by adding this to your config:
  AllCops:
    NewCops: enable
```
これはRuboCopが**最近追加されたいくつかのコード検査ルールを検出し、それらがRuboCopの設定ファイルで有効または無効になっていないこと**を示しています。
これらのコード検査ルールは、.rubocop.ymlファイルで明示的に有効または無効にする必要があります。
RuboCopの設定ファイルにEnabledという設定を追加し、それをtrueまたはfalseの値に設定することで、これらのコード検査ルールを有効または無効にできます。また、新しいコード検査ルールをデフォルトで有効にすることもできます。その場合、AllCopsセクションを追加し、NewCopsをenableに設定します。

まあ文章だけだとわかりづらいので.実際にrubocop.ymlファイルを作ってみます。
```yml: .rubocop.yml
AllCops:
  NewCops: enable
```
設定についてさらに噛み砕いて説明していきます。
`NewCops: enable` とは**NewCops: rubocop のバージョンをアップグレードしたときに新しいCopが自動的に有効になる**設定です。
これにより改善点が見つかりやすいので、enableにしておきます。

そしたら実行してみましょう。
```terminal
 % rubocop -a
Inspecting 2 files
.C

Offenses:

blackjack.rb:3:1: C: Style/Documentation: Missing top-level documentation comment for class Card.
class Card
^^^^^^^^^^
blackjack.rb:27:1: C: Style/Documentation: Missing top-level documentation comment for class Deck.
class Deck
^^^^^^^^^^
blackjack.rb:41:1: C: Style/Documentation: Missing top-level documentation comment for class Player.
class Player
^^^^^^^^^^^^　　
　　　　　　　　・
　　　　　　　　・
　　　　　　　　・
2 files inspected, 7 offenses detected
```
先ほど挙げていたエラー分が表示されなくなり、エラーの内容が変わりました。
エラー文に飽きてきたかもしれませんが根気良く頑張るます(𓁹‿𓁹)ﾌｯ・・・

上記のエラーを調べてみると、RuboCopがクラス Card、Deck、Playerの上部にドキュメンテーションコメントがないことを検出したことを示してくれているみたいです。 つまり、クラスの説明が不足しているということです。
どういうことかというと**それぞれのクラスが何をするかわからないから説明を書かないと後で見返した時に困りますよ**ってことを伝えてくれてるってこと。
では説明文を追加してみましょう。
```ruby
# Represents a single playing card in a deck.
class Card
  # ...
end

# Represents a deck of playing cards.
class Deck
  # ...
end

# Represents a player in the game of blackjack.
class Player
  # ...
end
```
それではこの状態で再度`rubocop -a`を実行してみます。
```terminal
% rubocop -a
Inspecting 2 files
.C

Offenses:

blackjack.rb:75:1: C: Metrics/AbcSize: Assignment Branch Condition size for play_blackjack is too high. [<6, 51, 16> 53.79/17]
def play_blackjack ...
^^^^^^^^^^^^^^^^^^
blackjack.rb:75:1: C: Metrics/CyclomaticComplexity: Cyclomatic complexity for play_blackjack is too high. [8/7]
def play_blackjack ...
^^^^^^^^^^^^^^^^^^
blackjack.rb:75:1: C: Metrics/MethodLength: Method has too many lines. [41/10]
def play_blackjack ...
^^^^^^^^^^^^^^^^^^
blackjack.rb:75:1: C: Metrics/PerceivedComplexity: Perceived complexity for play_blackjack is too high. [10/8]
def play_blackjack ...
^^^^^^^^^^^^^^^^^^

2 files inspected, 4 offenses detected
```
エラーが減りましたね。残り4つです。
残りの4つは、よくみて見ると全部play_blackjackメソッドについて言及していますね。
確かにplay_blackjackメソッドは誰がどうみても長いしごちゃごちゃしてるし、文句は言われる気がします。笑

上記のエラー文を訳して調べてみましょうか。
[RuboCop公式](https://docs.rubocop.org/rubocop/cops_metrics.html)によると
* Metrics/AbcSizeは、代入ブランチ条件のサイズが大きすぎるということを示しています。
* Metrics/CyclomaticComplexityは、コードの分岐が複雑すぎるということを示しています。
* Metrics/MethodLengthは、メソッドが長すぎるということを示しています。
* Metrics/PerceivedComplexityは、コードの読み取りやすさが低いことを示しています。

ということみたいです。
案の定めちゃくちゃ怒られてますね。笑

ここも要約すると
**play_blackjackメソッド長すぎてごちゃごちゃしてるからどうにかならない？**
ってことですね。

要望に応えるのが真のエンジニア・・・
play_blackjackメソッドのところを修正していきます。


```ruby
def player_turn(deck, player)
  loop do
    puts "あなたの現在の得点は#{player.score}です。カードを引きますか？（Y/N）"
    answer = gets.chomp.upcase
    if answer == "Y"
      player.hit(deck.draw)
      puts "あなたの引いたカードは#{player.hand.last}です。"
    else
      break
    end
  end
end

def dealer_turn(deck, dealer)
  while dealer.score < 17
    dealer.hit(deck.draw)
  end
end

def determine_winner(player, dealer)
  if player.score > 21
    :dealer
  elsif dealer.score > 21
    :player
  else
    player.score > dealer.score ? :player : :dealer
  end
end

def display_winner(winner)
  if winner == :player
    puts "あなたの勝ちです！"
  else
    puts "ディーラーの勝ちです。"
  end
end

def play_blackjack
  deck = Deck.new
  player = Player.new
  dealer = Player.new

  # Initial draw
  2.times { player.hit(deck.draw) }
  2.times { dealer.hit(deck.draw) }

  puts "ブラックジャックを開始します。"
  puts "あなたの引いたカードは#{player.hand[0]}です。"
  puts "あなたの引いたカードは#{player.hand[1]}です。"
  puts "ディーラーの引いたカードは#{dealer.hand[0]}です。"
  puts "ディーラーの引いた2枚目のカードはわかりません。"

  player_turn(deck, player)
  dealer_turn(deck, dealer)

  winner = determine_winner(player, dealer)
  display_winner(winner)

  puts "ブラックジャックを終了します。"
end

play_blackjack

```
このリファクタリングでは、play_blackjackメソッドをいくつかの小さなメソッドに分割しました。

* player_turn: プレイヤーのターンを処理。
* dealer_turn: ディーラーのターンを処理。
* determine_winner: プレイヤーとディーラーの勝者を決定。
* display_winner: 勝者を表示。

これにより、RuboCopが検出した違反を修正し、コードの可読性が向上するはずです。


再度実行してみると、
```terminal
% rubocop -a
Inspecting 2 files
.C

Offenses:

blackjack.rb:108:1: C: Metrics/AbcSize: Assignment Branch Condition size for play_blackjack is too high. [<4, 25, 0> 25.32/17]
def play_blackjack ...
^^^^^^^^^^^^^^^^^^
blackjack.rb:108:1: C: Metrics/MethodLength: Method has too many lines. [15/10]
def play_blackjack ...
^^^^^^^^^^^^^^^^^^

2 files inspected, 2 offenses detected
asone0420@MacBook-Air submission_quest % 
```

狙い通りエラー文が減りましたね。
`Metrics/AbcSize`と`Metrics/MethodLength`なのでメソッドをより小さくして行数も減らせということなので最後の要望に答えます。

```ruby
def initial_draw(player, dealer, deck)
  2.times { player.hit(deck.draw) }
  2.times { dealer.hit(deck.draw) }
end

def display_initial_cards(player, dealer)
  puts "ブラックジャックを開始します。"
  puts "あなたの引いたカードは#{player.hand[0]}です。"
  puts "あなたの引いたカードは#{player.hand[1]}です。"
  puts "ディーラーの引いたカードは#{dealer.hand[0]}です。"
  puts "ディーラーの引いた2枚目のカードはわかりません。"
end

def play_blackjack
  deck = Deck.new
  player = Player.new
  dealer = Player.new

  initial_draw(player, dealer, deck)
  display_initial_cards(player, dealer)

  player_turn(deck, player)
  dealer_turn(deck, dealer)

  winner = determine_winner(player, dealer)
  display_winner(winner)

  puts "ブラックジャックを終了します。"
end

play_blackjack

```
上記では、play_blackjackメソッドをさらに短くするために、初期カードの配布と表示を別のメソッドに分割しました。
* initial_draw: プレイヤーとディーラーに初期カードを配る。
* display_initial_cards: プレイヤーとディーラーの初期カードを表示。

これにより、play_blackjackメソッドの行数が短くなり、RuboCopの指摘に対処できます。

それでは最後に試してみます。
```terminal
 % rubocop -a
Inspecting 2 files
..

2 files inspected, no offenses detected
```
なんとかクリアできました。
ここまで根気よく付き合わないとエラーが消えないとは・・・
恐るべしRuboCop。

## まとめ
RuboCopは、Rubyプログラムの静的コード解析ツールです。コードの品質をチェックし、一貫性のあるコーディングスタイルを維持することを目的としています。
また具体的には以下のような機能を提供しています。

* **複雑さの低減** ：RuboCopは、メソッドの長さや複雑さをチェックし、コードの可読性と保守性を向上させるための指摘を行う。
* **パフォーマンスの最適化** ：RuboCopは、より効率的な代替手段が存在する場合、コードのパフォーマンスを向上させるための提案を行う。
* **コーディングスタイルの統一** ：RuboCopは、インデントや空白の使用、命名規則など、一貫したコーディングスタイルを維持するためのルールを適用する。
* **セキュリティの向上** ：RuboCopは、セキュリティのリスクがあるコードを検出し、より安全なコーディング方法を提案する。

## 最後に
今回、RuboCopを使って導入から実際にプログラムを修正してみて、いかに自分のコードがいい加減か痛感させられました。
日頃からプログラムの機能的なところだけじゃなくて、読み手のことまで意識してコードを書いていければいいなと深く感じました。

## 参考文献
https://rubocop.readthedocs.io/en/latest/

https://qiita.com/KazuyaInoue/items/54d2f73a52891d7f5637

https://qiita.com/terufumi1122/items/ad55bf8713c0df053f58

https://qiita.com/mtsuge/items/e56015c93ce1ae8afd63