Uncategorized

【Ruby】メソッドを作成するときに考えていること(例のコードもあり)

メソッドを作成する理由

メソッドを作成する理由について、大切なことをまとめます。

複雑さを低減する

メソッドを作成することで複雑なロジックを隠蔽し、詳細について考える必要をなくすことができます。

例えば、以下のようなコントローラーが合った場合、書籍のフィルタリングロジックが index メソッドで行うプロシージャー(手順)を頭の中で追っていくのに邪魔になります。

class BooksController < ApplicationController
  before_action :set_book, only: [:show, :edit, :update, :destroy]

  # GET /books
  # GET /books.json
  def index
    # 別の処理

    # 書籍のフィルタリングロジック
    @books = Book.all
    @books = books.where(genre: params[:genre]) if params[:genre].present?
    @books = books.where(author: params[:author]) if params[:author].present?
    @books = books.where("published_year >= ?", params[:start_year]) if params[:start_year].present?
    @books = books.where("published_year <= ?", params[:end_year]) if params[:end_year].present?
    @books

    # 別の処理
  end

  # 以下のアクションは省略(show, new, edit, create, update, destroy)

  private

  # Use callbacks to share common setup or constraints between actions.
  def set_book
    @book = Book.find(params[:id])
  end
end

コントローラーの責務はリクエストからの指示を解釈して、モデルへの指示を行い、ビューへの描画の指示を行うことです。

そのため、書籍のフィルタリングロジックの詳細について考える必要がないので、これをメソッドにして抽象化することでコントローラーの本来の責務に集中できます。

class BooksController < ApplicationController
  before_action :set_book, only: [:show, :edit, :update, :destroy]

  # GET /books
  # GET /books.json
  def index
    # 別の処理
    @books = filtered_books(params)
    # 別の処理
  end

  # 以下のアクションは省略(show, new, edit, create, update, destroy)

  private

  # 書籍のフィルタリングロジックをカプセル化
  def filtered_books(params)
    books = Book.all
    books = books.where(genre: params[:genre]) if params[:genre].present?
    books = books.where(author: params[:author]) if params[:author].present?
    books = books.where("published_year >= ?", params[:start_year]) if params[:start_year].present?
    books = books.where("published_year <= ?", params[:end_year]) if params[:end_year].present?
    books
  end

  # Use callbacks to share common setup or constraints between actions.
  def set_book
    @book = Book.find(params[:id])
  end
end
  • 評価の詳細がじゃまにならない場所に片付られる
  • 評価の目的がそれを意味する関数名に凝縮される

コードの重複を避ける

メソッドを作成する最も一般的な理由は、コードの重複を避けることであり、これは言わずもがなだと思います。

同じロジックのコードが至るところに散らばっていると変更するときに間違えたり抜け漏れのリスクがありますが、メソッドが作成されていればそこだけを変更すればいいのでミスする可能性が少なくなります。

メソッドの凝集度

凝集度とは、メソッド内の機能がどれだけ一貫性を持って関連しているかを表す概念です。高い凝集度を持つメソッドは、単一の、明確に定義された責任や目的を持っています。

凝集度にはいくつか種類があります。

機能的凝集度

機能的凝集度は、最も理想的な状態でメソッドが単一の具体的な機能やタスクに集中している状態を指します。これは、一つのメソッドが一つのことをうまく行うという原則に基づいています。

例えば、ユーザーの年齢を計算するメソッドやファイルを保存するメソッドは、それぞれ単一の機能に特化しており、機能的凝集度が高いと言えます。

このような設計はコードの再利用性と保守性を向上させます。

def full_name
  first_name + last_name
end

def age_from_birthday
  Time.current.year - birthday_date.year
end

情報的凝集度

情報的凝集度とは、複数の機能が同じデータセットに基づいて動作する場合の凝集度です。

例えば、以下に示す社員の生年月日から年齢と定年までの期間を計算するメソッドは、生年月日という共通のデータを用いて、まず年齢を計算し、次にその年齢を使用して定年までの期間を計算します。このプロセスは決まった順序で実行され、各段階でデータが共有されます。

このような設計ではメソッドが複数の責任を持つため、コードの再利用性や保守性が低下します。これを改善するためには、年齢を計算するメソッドと定年までの期間を計算するメソッドを分けることが望ましいです。

このアプローチでは、それぞれのメソッドが単一の機能に集中するため、機能的凝集度が高まります。

# 情報的凝集度の例
def calculate_age_and_time_until_retirement(birthday_date)
  # 年齢を計算
  age = Time.current.year - birthday_date.year

  # 定年の年齢 - 年齢
  RETIREMENT_AGE - age
end

# 機能的凝集度のメソッド 2 つに分けられる
def age(birthday_date)
  age = Time.current.year - birthday_date.year
end

def time_until_retirement(age)
  RETIREMENT_AGE - age
end

時間的凝集度

他にも同時に実行される複数の処理を同じメソッドにまとめた時間的凝集度があります。

以下の例は、#start メソッドが複数の異なるタスクを直接実行しており、他のメソッドを呼び出していません。これにより、メソッドが複数の責任を持ち、コードの再利用性と保守性が低下しています。

class Application
  def initialize
    # アプリケーションの初期設定
  end

  def start
    # データベース接続の設定
    # ここで直接データベースに接続するコードを記述
    # ...

    # ログファイルの初期化
    # ここでログファイルを開くか初期化するコードを記述
    # ...

    # 設定ファイルの読み込み
    # ここで設定ファイルを読み込むコードを記述
    # ...

    # セキュリティチェック
    # ここでセキュリティチェックを行うコードを記述
    # ...

    # システムの状態をチェック
    # ここでシステムの状態をチェックするコードを記述
    # ...

    # アプリケーションのメイン機能を開始
    # ここでアプリケーションの主要な機能を開始するコードを記述
    # ...
  end
end

これを解消するためには、処理をより意味的に関連する単位に分割し、それぞれの処理を独立したメソッドに分離することが有効です。これにより、各メソッドは特定の機能や責任に集中し、高い凝集度を持つことができます。

class Application
  def initialize
    # アプリケーションの初期設定
  end

  def start
    # データベース接続の設定
    connect_to_database
    # ログファイルの初期化
    initialize_log_file
    # 設定ファイルの読み込み
    load_configuration
    # セキュリティチェック
    perform_security_checks
    # システムの状態をチェック
    check_system_status
    # アプリケーションのメイン機能を開始
    launch_main_features
  end

  # ここに各タスクの実装が続く...
end

メソッド名

メソッドの名前をつける上で大切なことをまとめます。

メソッドが行うことをすべて説明する

メソッド名はそのメソッドが行うことすべてを含めるようにします。例えば、メソッドがレポートの総数を計算して出力ファイルを開くことだとしたら、#compute_report_totals ではなく少し長いですが、#compute_report_totals_and_open_output_file とします。

すべてのメソッドは明確な目的を持ち、その目的を正確に説明する名前をつけるのが最善です。

関数には戻り値を説明する名前をつける

例としては以下になります。

  1. calculate_total
    • 説明: 合計値を計算して返します。
  2. fetch_user_by_id
    • 説明: 特定のIDに基づいてユーザー情報を取得します。
  3. is_valid_email?
    • 説明: メールアドレスが有効かどうかをチェックし、真偽値を返します。
  4. convert_to_json
    • 説明: データをJSON形式に変換します。

プロシージャにはその動作を明確に表す名前をつける

例としては以下になります。

  1. delete_expired_sessions
    • 説明: 期限切れのセッションを削除します。
  2. update_user_profile
    • 説明: ユーザープロファイルを更新します。
  3. send_welcome_email
    • 説明: 新規ユーザーにウェルカムメールを送信します。
  4. process_payment_transaction
    • 説明: 支払いトランザクションを処理します。

このように関数なのかプロシージャなのかを意識するといいメソッド名が付けられるかと思います。

メソッドの引数

メソッドの引数関連で大切なことをまとめます。

引数はだいたい7個までに制限する

心理学によると人は7つ以上のかたまりを一度に覚えられない。なので、引数が多くなりすぎてないかを注意するようにします。

引数が多すぎる場合は、メソッド同士の結合度が強くなりすぎている兆候であるので設計を見直すこと。

メソッドの引数を作業用変数として使用しない

メソッドの引数を作業用変数として利用した場合、最後の行に到達した場合もはや入力値は含まれておらず、他の人がこれを入力値であると思いこんで使用した場合、思わぬバグを引き起こす原因になります。

# 悪い例
def modify_value(input_val)
  input_val += 10
  # 他の処理...
  return input_val
end

# いい例
def modify_value(input_val)
  result = input_val + 10
  # 他の処理...
  return result
end

さいごに

これらを意識することで、良いコードが書けるようになると思います。自分自身まだまだ実践できてないことも多く考えをまとめるいい機会になりました。

こちらの書籍を多いに参考にさせていただきました。プログラマー 2 ~ 3 年目くらいの人が読むのにちょうどいい本だと思います。

ABOUT ME
sakai
東京在住の30歳。元々は車部品メーカーで働いていてましたが、プログラミングに興味を持ちスクールに通ってエンジニアになりました。 そこからベンチャー → メガベンチャー → 個人事業主になりました。 最近は生成 AI 関連の業務を中心にやっています。 ヒカルチャンネル(Youtube)とワンピースが大好きです!