Excelを曖昧検索できるgem「exfuz」をリリースしました

複数のExcelファイルを、fzfやpeco等のfuzzy finderツールを用いて曖昧検索ができるgem「exfuz」をリリースしました!

目次

gemの概要

exfuzは、複数のExcelファイル内のテキスト(現バージョンではセルのテキストのみ)を対象に曖昧検索が出来るTUIツールです。曖昧検索にはfuzzy finderツール*1を利用します。

さらにwsl環境でgemを利用する場合、曖昧検索で選択した箇所へのジャンプが可能です。つまり、選択したセルをアクティブ状態にすることが可能です。

https://user-images.githubusercontent.com/85052152/213947172-8917d9a8-1f88-42a2-9fc1-53c8de25e309.gif

開発した経緯

開発した経緯は、前職で経験したExcelの辛い問題を解決したいためです。

私の前職は、SES企業(SIer業界)のプログラマです。
いくつかのプロジェクトを携わりましたが、要件定義や設計等を管理するドキュメントツールはExcelでした。Excelは便利機能が豊富ですが、ドキュメントを管理する手段としては辛い点が多々ありました。
特に辛いと感じた箇所は以下です。

  • 検索機能が使いづらい
    • 正規表現検索が出来ない
    • 検索文字列の入力から検索までの手順が手間
  • 複数のファイル(Book)を横断して検索ができない
  • ターミナル上で検索できない

ドキュメント数が少ない場合は、上記の不便さに目を瞑ることも出来ました。
しかし、プロジェクト規模が大きい場合、そうは行かなかったです。ドキュメント数が膨大になり、ナレッジの検索に多大な時間を要するため、非効率な時間が増えました。

この問題は私だけでなく、当時のチームメンバーも同様の悩みを抱えてました。(ターミナル上で検索したいというメンバーがいるかは定かではないですが。。。)

このような背景から、Excelを使ったドキュメント管理の苦労を削減できればと思い、TUIツールを作ることにしました。

使い方

ここでは、exfuzの最低限の使い方について解説したいと思います。詳細はREADMEをご覧ください。

まず、起動コマンドは以下となります。

exfuz start

起動後の画面は、初期画面と曖昧検索画面の2つで構成されます。
初期画面では、情報の表示とキー入力によるイベントの受付けを担います。
曖昧検索画面では、Excelファイル内のテキストを曖昧検索します。曖昧検索の対象ファイルは、カレントディレクトの階層内のxlsxファイルとなります。

初期画面

起動をすると、以下の初期画面が表示されます。

「exfuz start」起動時の初期画面

上図の①は、クエリの入力エリアになります。入力した文字列は、曖昧検索の対象(セルのテキスト)をフィルタリングすることが可能です。

②は、xlsxファイルの解析ステータスになります。内訳としては以下となります。

  • 左:現時点で曖昧検索可能なファイル数
  • 右:曖昧検索の対象となる全ファイル数

①について詳細に説明します。例として、2つのxlsxファイルを曖昧検索の対象にexfuzを起動してます。

クエリがブランクの場合は、2つのxlsxファイルの全セルが曖昧検索の対象となります。下図は、クエリがブランクのときの曖昧検索画面になります。

クエリがブランクの場合の曖昧検索画面

次に、クエリに文字列がある場合です。
例えば、クエリエリアに正規表現test.*data を入力します。この場合の曖昧検索画面では、下図のようにクエリにマッチしたセルのみ曖昧検索の対象となります。

クエリに文字列を入力した場合の曖昧検索画面

fuzzy finderツールで選択した後は、初期画面に戻ります。下図の③のように選択行の一覧が表示されます。

曖昧検索で選択した後の初期画面

曖昧検索画面

曖昧検索画面は初期画面から遷移でき、下図のような画面が表示されます。

fzfを使用した場合の曖昧検索画面

曖昧検索画面の構成はfuzzy finderツールに依存します。上図の曖昧検索画面は、fuzzy finderツールであるfzfを使用した場合です。

標準入力の各1行ごとの内容は、以下のようなフォーマットで構成された文字列となります。フォーマットは、各項目ごとに区切り文字を連結した文字列です。

行番号:ファイル名:シート名:セル番地:セルのテキスト

行番号は、標準入力に渡したデータの順序となります。また、各項目ごと区切り文字は:です。

使用技術とシステム構成

使用した技術は以下となります。

次に、exfuzのコア機能のシステム構成について概要を説明します。
下図はシステム構成図となります。

exfuzのコア機能のシステム構成

全体的な処理の流れは、左から右への一方通行となります。

はじめに上図①のパース処理です。 Excelで利用されるxlsxファイルはxmlファイルの圧縮ファイルで構成されています。そのため、普段私達が見慣れた形式(ブック、シート、セル)に変換するためにパース処理を行います。
次に、パースされたデータをキューに格納していきます。データは、「ブック名、シート名、セル番地、セル内の値」から構成されてます。

そして、曖昧検索を起動すると上図②、③の処理を実行します。
キュー内のデータからレコード格納オブジェクトにデータを移します。 レコード格納オブジェクトでは、フィルタリングやグルーピングが出来る振る舞いを用意しました。フィルタリング機能は、クエリによる曖昧検索対象の絞り込みに用います。
最後に、fuzzy finderツールをサブプロセスで起動し、標準入力にデータを流し込みます。

処理の流れは逐次ですが、実際には並行処理が行われています。 上図の2色の矢印(緑色とオレンジ色)でスレッドを分け、スレッド構成は以下となります。

  • 緑色矢印の処理:メインスレッド(プログラム実行時に作成されたスレッド)
  • オレンジ色矢印の処理:ワーカースレッド(メインスレッドとは別のスレッド)

このように処理を別のスレッドに分けることで、パース処理が未完了の状態でも曖昧検索を可能にしました。

技術選定

使用技術とシステム構成 で述べましたが、exfuzはいくつかのgemを利用しています。ここでは、重要な処理を担うgemのXsv を選定した理由について述べます。

exfuzはExcelのファイル、つまりxlsxファイルをパースし、ブック、シート、セル等のExcelの構成要素に変換します。 Xsv はこのパース処理を担います。
Excelをパースするgemは他にもいくつかありますが、その中から Xsv を選定した理由は、パース処理が高速だからです。

xlsxファイルは、xmlファイル群をzip形式で圧縮したものです。テキストファイル同様に曖昧検索を可能にするためには、パース処理が必要になります。Excelの曖昧検索の速度は当然、テキストファイルの曖昧検索と比較すると遅いです。そのため、いかにExcelとテキストファイル間で曖昧検索の処理速度の差を縮められるかが、ユーザー体験を向上させる上で重要だと考えました。

パース処理の速度の調査と検証は、以下のブログを見た上で、軽めのパフォーマンス検証を行いました。その結果、最も高速だったXsvを採用するに至りました。

schembri.me

パフォーマンス以外でも実装コストの懸念は若干ありました。Xsv以外のgemは、概ねExcelの高機能な操作が可能なため、実装コストの削減でメリットがありました。しかし、リリース予定のexfuzは、機能をシンプルにしたかったので、パース後のExcel操作は最低限の自前実装でも何とかなると判断しました。

苦労した点

苦労したことは、設計と実装です。

exfuzの機能はシンプルですが、設計と実装の難易度が、経験上最も高かったです。スクラッチ開発が必要な機能が多く、そのような機能の設計・実装に着手した当初は、方針の目処が全く立たない状況でした。

そんな状況下でも少しずつですが、開発に粘り強く取り組みました。小さく問題を切り分けたり、知識を増やしていくことで自力でリリースすることが出来ました。設計や実装方法の知見は、類似した機能を持つOSSAPIやコード、技術書を参考にして増やしました。

具体的には、以下のOSS、技術書が大変参考になりました。

直近で対応したいこと

リリース速度を優先させたため、改善すべき点や追加したい機能が沢山あります。課題がある中で、直近で対応したいことは以下があります。

  • コードのリファクタリング
    • テストコードも含めて汚いです。モジュール化はある程度しましたが、特に命名が分かりづらいので早期に修正したいです。
  • Windows環境対応
    • Rubyの実行環境がWindowsの場合、exfuzの端末制御にバグがあるため使用できない状況です。Windows環境で利用するユーザー層が多いと思うのでこちらも早期に対応したいです。
  • exfuzのシングルバイナリ化
    • ユーザーの中にはRubyが入ってない環境もあると思われるので、手軽に導入出来るようにしたいです。

最後に

今回の開発では、OSSのコードリーディングをしたことが非常に良い経験となりました。OSSの設計・実装がハイレベルなので、コードを読み解く経験が私の血肉となりました。将来的には、今回参考にしたOSSのような技術力を身につけたいので、自己研鑽に努めてきたいと思います。
そして、OSSに対しての感謝の気持ちが一段と増しました。スクラッチ開発で苦労したこともあり、普段何気なく使っていたOSSへの有難みが身に沁みました。

TwitterExcelに関するツイートを眺めると、「Excelの検索が辛い」と感じるユーザーは多そうでした。開発したgemによって、ユーザーの苦労を少しでも減らせたら嬉しいです。
しかし、リリースを優先したこともあって、exfuzは、機能数も少なく使い勝手が悪い部分も多いです。
より良くするために今後も開発を継続していきたいと思います。

*1:デモではfzfを利用してます。

フィヨルドブートキャンプ内で「1分間スピーチ会」を開いてみた

これは、「フィヨルドブートキャンプ Part 2 Advent Calendar 2022」の23日目の記事です。
昨日の記事は、@uchihiro04さんの「VSCodeのパッケージ不適合によるエラー解決までの道のり(Byebug編)」です。発生したエラーに対して、諦めずに検証を繰り替えした姿勢を見習いたいと思いました!

今年のフィヨルドブートキャンプのアドベントカレンダーは、以下となります。
フィヨルドブートキャンプ Part 1 Advent Calendar 2022 - Adventar

フィヨルドブートキャンプ Part 2 Advent Calendar 2022 - Adventar

はじめに

フィヨルドブートキャンプ(以下FBC)で学習中の@wata00913です。FBCはよくコミュニティと言われ、受講生がイベントを盛んに開催します。
例えば、輪読会、ペアプロ、勝手にペアプロ会、目覚まし会、もくもく会、グループコーチングなどがあります。

受講生が自発的にイベントを開催する様子を見て、自分も何か開いてみたいと思い、6月から「1分間スピーチ会」を開催しました。

この記事では、開いてみた経緯、やってみた感想などをまとめました。1分間スピーチ会がどのようなものか、雰囲気が伝われば幸いです。

目次

どんなことをするのか?

内容は、一般的な1分間スピーチをベースにしました。一般的な1分間スピーチとは、発表者が話したいテーマを1分以内で発表します。
FBC内で開催した1分間スピーチ会の内容は、以下になります。

  • 構成
    • (発表+質疑応答)✕ 人数
      • 質疑応答はコメントも可。
  • 持ち時間
    • 発表時間は2分(1分はバッファ)
    • 質疑応答時間は6分
  • 発表テーマ
    • テーマは自由
    • 思いつかない場合は、「事前テーマ」を発表する
    • 事前テーマは、主に「面接で聞かれそうな質問」
        • エンジニアを目指す理由
        • 仕事で大切にしていること
        • 成長するためにやっていること
  • 開催時刻
    • 月曜の8:00~8:30と13:00~13:30
      • 参加人数は、wata含めて約3名

イベントを開いた背景

イベントを開いた背景は、主に次の3つとなります。

  • 微力でもFBCコミュニティに貢献したい
  • ソフトスキル関連のイベントも増やしたい
  • 受講生の方に開催を宣言して、背中を押してもらった

FBCのコミュニティでは、受講生が活動的です。一方で私は、コミュニティに貢献が出来てないことを痛感してました。
受講生が自然とコミュニティに貢献している姿を見る度に、自分も何か微力ながら貢献したい気持ちが強まりました。

また、当時は「ソフトスキル系のイベントも沢山あればなぁ」と、イベントへの願望がありました。輪読会・ペアプロといった技術系のイベントは多いです。
しかし、ソフトスキル系のイベントは少数なので、私自身で開催し、盛り上げたい気持ちがありました。

2つの動機から「自主開催するぞ!」と意気込んでいましたが、恥ずかしさがあって気後れしました。
しかし、受講生に背中を押して頂いた甲斐もあり、開催する決心が着きました。

イベントの形式を1分間スピーチにした背景

開催する意を決したことで、次は形式決めです。ソフトスキルの中でも、私が苦手とする「コミュニケーション(特に言語化)」が練習できる形式にしようと決めました。

苦手を克服したい目的があったので、それが可能な仕組みを支えるコンセプトを考えました。そのコンセプトとは、

「早い段階で不確実性を減らす形式」

です。具体的には、現時点における言語化能力の課題を早期発見しやすくし、改善を繰り返せるような形式を目指しました。

特に、「早い段階」というのが私の中でポイントです。
コミュニケーション能力は、エンジニアになる上で重要度が高く、また改善に時間を要します。さらに、能力の定量的な評価が難しいため、スキル不足に気づきにくいです。
もし能力不足であることが、カジュアル面談や面接時に気づいた場合、「手遅れ」状態になることを危惧しました。

そういった経緯からコンセプトを考え、そのコンセプトを満たしやすくしたものが、1分間スピーチ会でした。

工夫したこと

工夫したことは、「相手の気持ちを考える」ことです。

これは、今回の1分間スピーチに限らず、今年1年を通して意識するようにしてました。
具体的に1分間スピーチの場合は、「参加者の負担を減らす」ことを心がけ、次のような取り組みをしました。

  • 開催時間を短くする(大体30分程度)
  • 質問は最初にwataが必ず行う
    • 質問の敷居を下げるため
    • フィードバックが得られるように、発表に対する質問数が1つ以上の状態にする
  • 事前テーマを面接の質問にする
    • 就活希望者が多いため、就活の事前準備に最適と考えた
    • 面接の質問は、課題解決や学習方法等を内省する問として最適と考えた

得られたこと

実施してみた個人的な結果は、コンセプトをそこそこ反映出来たかなと自己満足してます。
私が実施した中で、コンセプトである「早期発見した課題」と、「想定外に良かった」ことを紹介します。

  • 早期発見した課題
    • 聞き手目線を意識して話の構成を考える
    • 聞き手の興味あることは探るには、聞き手の質問内容、質問量から把握する
    • 聞き手が話を理解しやすくするために、コンテキストをなるべく合わせる
    • 1分(最大2分)は思った以上に短い。簡潔に伝えるために話したいことを絞る
    • 印象が下がるような口癖(えっと、えー)を頻繁に言わないようにする
    • うまく話せない、中身が薄い話は、そのテーマについて深く考えてない or 言語化が出来てない
  • 想定外に良かったこと
    • 発表者の話し方や学習・仕事の進め方、興味のある技術等を知る機会が増えた
    • 発表したテーマについてのディスカッションが増えた

但し、1分間スピーチも銀の弾丸ではなく、デメリットも見受けられました。

  • 話し手を主体に置いた形式のため、聞き手のメリットが薄くなりがち
    • 聞き手の興味を惹かないこと内容も多い
      • 事前テーマの面接の質問
      • 学習内容
  • 言語化の練習としては質が低い部分もある
    • 発表、質疑応答の時間が短すぎるので、詳細な話が出来ない

やってみた感想

初の試みなので、参加してもらえるのか?、続くのか?等の不安に駆られました。しかし、現在まで何とか存続できてます。
これもひとえに参加者のおかげです。来てくださった方に本当に感謝してます。

また、参加者の方が、日報やブログで書いた感想を目に見かけます。何かしらの気づきを得たり、有意義な時間を提供出来たことを知ると純粋に嬉しいです。ユーザーに喜ばれるプロダクトづくりをした感覚を味わえました。

最後に

@lef237さんのお声掛けに後押しされて、「新春 輪読会EXPO 2023」という、輪読会や勉強会を紹介するイベントで「1分間スピーチ会」を告知する予定です。人見知りなので緊張しますが、告知を頑張りたいと思います💪

コミュニケーション(言語化)が苦手な方、就活の面接練習をしたい方は、是非遊びに来てください〜!

明日の記事は、@saeyama_pgさんになります!

RailsのPathヘルパーを誤用してdeviseにハマった

はじめに

タイトル通りです。 deviseを使って認証機能を実装する際に、Pathヘルパーを誤用していたため、devise周りの機能がバグってしまいました。

この記事では、deviseでバグが発生した原因とその修正方法をまとめました。 また、何故deviseで期待した挙動にならないのかを理解するために、deviseのコードを追ってみました。

目次

バグの内容

サインアウトのリンクをクリック後に以下のバグが発生しました。

  • リダイレクトしない。
  • サインアウト後に通知メッセージが表示されない。
    • 英語の場合、en.devise.sessions.signed_outキーの訳文が該当メッセージとなる。

再現方法

今回発生した現象を再現するために、devise周りの設定方法について記載しました。

まず、バージョン情報は以下になります。

次にルーティングの設定です。config/routes.rbにて以下のように設定をします。

Rails.application.routes.draw do
  devise_for :users
  # その他ルーティングの設定は省略。
end

サインアウトのprefix、メソッド、URIパターンは以下のようになります。

prefix メソッド URIパターン アクション
destroy_user_session DELETE /users/sign_out(.:format) devise/sessions#destroy

Controllerの設定は不要です。
最後にViewの設定になります。任意のViewファイルで以下のようにPathヘルパーを使ってサインアウトのリンクを作ります。

<%= link_to 'サインアウト', destroy_user_session_path(current_user), method: :delete %>

current_userはdeviseが用意したヘルパーメソッドです。サインイン中のユーザーのモデルオブジェクトを返します。

バグ発生の原因と対処方法

バグ発生の原因は、タイトルにある通り、Pathヘルパーの使い方に誤りがあるためです。 該当箇所は再現方法で説明した「サインアウトのリンクを作成するコード」です。 Pathヘルパーのdestroy_user_session_pathのパラメータにcurrent_userを指定したことが誤りになります。

destroy_user_sessionURIパターン/user/sign_out(.:format):formatは、パラメータとなります。丸括弧()は必須ではないパラメータを指します。パラメータ名がformatなので、ここに入れるべき値は、htmlxmlなどのMimeTypeのサブタイプが当てはまります。

しかし今回のケースでは、Pathヘルパーの引数にcurrent_userを指定してます。 実際のURLは/user/sign_out.2のように、:format箇所が ログインユーザーのidとなります。フォーマットがdeviseの想定しない値であるため、今回のバグが発生したのです。

バグの対処方法は簡単です。 Pathヘルパーのパラメータに不適切な値を指定したので、これを修正することでバグは解消できます。URIパターンの丸括弧のパラメータは必須ではないため、下記のように、Pathヘルパーの引数を削除します。(引数なしでもリクエストでのフォーマットのMimeTypeのサブタイプはhtmlになります。)

<%= link_to 'サインアウト', destroy_user_session_path, method: :delete %>

サインアウト後にリダイレクトされない原因

根本的な原因は上述しましたが、deviseで期待した挙動にならない原因をコードリーディングで調査したいと思います。

サインアウト後のアクションは、devise/sessions#destroyになるので、app/controllers/devise/sessions_controller.rbdestroyメソッドを見ます。(deviseのファイルのパスは、bundle info deviseで確認が出来ます。)
最終行にrespond_to_on_destroyメソッドを呼び出しているので、このメソッドがリダイレクト処理を実行しています。

  def respond_to_on_destroy
    # We actually need to hardcode this as Rails default responder doesn't
    # support returning empty response on GET request
    respond_to do |format|
      format.all { head :no_content }
      format.any(*navigational_formats) { redirect_to after_sign_out_path_for(resource_name) }
    end
  end

respond_toメソッドでは、クライアントの要求するフォーマットごとに処理を記述しています。 ブロック処理にはformat.all(引数なし)とformat.any(*navigational_formats)があります。format.allformat.anyは異なる機能のメソッドに見えますが、これらはエイリアスメソッドになります。 ですので、2つの処理が異なる点は引数の有無です。 引数の有無による挙動の違いについては、Rails APIActionController::MimeResponds#respond_toで以下のように説明があります。

respond_to | Rails API

  • 引数有りの場合

    respond_to also allows you to specify a common block for different formats by using any:

  • 引数無しの場合

    any can also be used with no arguments, in which case it will be used for any format requested by the user:

つまり、引数有りの場合は、指定したフォーマットを対象に共通のブロック処理を実行します。また、引数がない場合は、任意のフォーマットを対象に共通のブロック処理を実行します。

respond_to_on_destroyでは、navigational_formatsに該当しないフォーマットの場合は、リクエスト時のページから遷移しない204 No Contentをレスポンスステータスとしています。 このことから、サインアウト後にリダイレクトしない原因は、destroy_user_session_path(current_user)のフォーマットがnavigational_formatsに該当しないことだと予測できます。

では次に、navigational_formatsがどのフォーマットであるのか調べます。 app/controllers/devise_controller.rbに該当するメソッドがあります。

  def navigational_formats
    @navigational_formats ||= Devise.navigational_formats.select { |format| Mime::EXTENSION_LOOKUP[format.to_s] }
  end

配列のクラス変数で繰り返し処理を行っています。Mime::EXTENSION_LOOKUPに含まれているフォーマットを抽出した要素がnavigational_formatsになるようです。

Mime::EXTENSION_LOOKUPは、Railsで扱うフォーマットになります。 クラス変数のDevise.navigational_formatsは、lib/devise.rbで定義しています。

  # Which formats should be treated as navigational.
  mattr_accessor :navigational_formats
  @@navigational_formats = ["*/*", :html]

navigational_formatsは、Railsプロジェクトで設定可能な項目になります。 つまりlib/devise.rbでは、初期設定としてワイルドカードhtmlnavigational_formatsとして定義されています。

以上のことから、調査内容をまとめます。

  1. destroy_user_session_path(current_user)のリクエストフォーマットはログイン中のユーザーidである。
  2. リクエストのフォーマットがnavigational_formatsに該当する場合は、リダイレクトする。
  3. 2.以外の場合は、204 No Contentをレスポンスステータスを返す。つまり、画面遷移をしない。
  4. navigational_formatsは、今回のdeviseの設定では*/*html
  5. 今回のケースでリダイレクトしない原因は、リクエストフォーマットがnavigational_formatsに含まれず、2.が実行されるため。

サインアウト後に通知メッセージが表示されない原因

こちらの原因もコードリーディングで調査したいと思います。

destroyメソッドには、set_flash_message!を呼び出しています。これが、通知メッセージを出力する処理となります。

  # Sets flash message if is_flashing_format? equals true
  def set_flash_message!(key, kind, options = {})
    if is_flashing_format?
      set_flash_message(key, kind, options)
    end
  end

デバッグすると、is_flashing_format?の値が偽となり処理が終了します。
is_flashing_format?を見てみます。

# Check if flash messages should be emitted. Default is to do it on
# navigational formats
def is_flashing_format?
  request.respond_to?(:flash) && is_navigational_format?
end

最初の式の値は真となりましたが、2つ目の式の値は偽となりました。 ここでもnavigational_formatが出ました。is_navigational_format?は、リクエストのフォーマットがDevise.navigatinal_formatsに含まれているかを判定します。

def is_navigational_format?
  Devise.navigational_formats.include?(request_format)
end

Devise.navigational_formatsはリダイレクトの原因調査の際に確認しました。 つまり、通知メッセージが表示されない原因は、リダイレクトしない原因と同様にリクエストフォーマットがnavigational_formatsに含まれないためです。

まとめ

RailsのPathヘルパーを使用する際に、フォーマットのパラメータにdeviseが意図しない値を指定したことが原因でdevise周りの機能でバグが発生しました。
今回のバグの調査をしたことで、提供されるAPIメソッドの使い方と挙動を正確に理解することが重要であることを学びました。

「getter,setter (attr)」と 「インスタンス変数の直接アクセス」を比較してみた(書き方編)

はじめに

こんにちは、Webエンジニアを目指してフィヨルドブートキャンプ(FBC)で学習をしてますwataです。

ここ最近は、FBCのカリキュラム以外にも、リファクタリング*1 *2を学習中です。

リファクタリング本には、対立関係の項目が目立ちます。例えば、「引数の追加」と「引数の削除」です。 この対立関係において筆者は、片方の方法が絶対的な最適解ではない、つまり銀の弾丸ではないことを主張してます。

その一方で、私がコーディングをする際に使用しない項目も散見されます。本を読むだけだと、「本当にこの項目は、リファクタリングをするほどの価値があるのかな🤔」と思うことがあります。

そこで私が気になった、対立関係にある項目を、自分なりに比較してみることにしました。 対象は、タイトルにある「getter、setter」と「インスタンス変数の直接アクセス」を扱います。

本記事では前段として、Rubyでの「getter、setter」の書き方を紹介したいと思います。

インスタンス変数の直接アクセス

インスタンス変数を直接操作する方法は、ローカル変数とさほど変わりません。 インスタンス変数の先頭に@を付けるだけです。簡単ですね!

サンプルコードにNameクラスを用います。名前(first_str)と名字(last_str)を引数に取ります。

class Name
  def initialize(first_str, last_str)
    # インスタンス変数へ直接代入する
    @first = first_str
    @last = last_str
  end

  def to_s
    # インスタンス変数を直接参照する
    "#{@last} #{@first}"
  end
end

getter、setter

前提として、今回取り上げるgetter、setterは、クラス内部に限った話をします。 何故ならRubyは、「インスタンス変数の直接アクセス」がクラス外部から出来ないためです。

getter、setterとは、インスタンス変数をメソッド経由でアクセスする方法です。 皆さんお馴染みのアクセスメソッド(attr_readerattr_writeattr_accessor)もgetter、setterになります。

注意点として、getter、setterはインスタンス変数を加工するようなロジックは含みません。あくまで値を取り出す、値を代入するだけです。

  # getter
  def first
    @first
  end
  # getterでない
  def uppercased_first
     @first.upcase
  end

リファクタリング本では、インスタンス変数自身をメソッドでカプセル化することから、フィールドの自己カプセル化自己カプセル化フィールドなどと呼称してます。

定義方法

getter、setterの定義方法について説明します。定義方法は「自前実装」と「アクセスメソッド」の2パターンがあります。

自前実装

後述するアクセスメソッドを用いずに自前で実装する方法です。とは言ったものの、コード量は少なく実装方法は簡単です。

class Name
  def initialize(first_str, last_str)
    @first = first_str
    @last = last_str
  end

  # getter
  def first
    @first
  end

  # setter
  def first=(first_str)
    @first = first_str
  end
end

# @lastについては省略

アクセスメソッド

アクセスメソッドの「attr_readerattr_writerattr_accessor」を使用します。自前での実装と比較して、シンプルに定義ができます。
3つのメソッドは、アクセスする用途ごとに使い分けます。

以下のコードは、自前実装で作成したgetter、setterをattr_accessorに置き換えたものになります。 attr_accessorメソッドを呼び出すだけです。

class Name
  attr_accessor :first, :last

  def initialize(first_str, last_str)
    @first = first_str
    @last = last_str
  end
end

呼び出し方

呼び出し方は、自前実装とアクセスメソッドで共通です。アクセスメソッドが自前実装を定義しているためです。 getterメソッドの呼び出し方法は、通常の関数の呼び出し方法と同様です。

class Name
  attr_accessor :first, :last

  def initialize(first_str, last_str)
    @first = first_str
    @last = last_str
  end

  def uppercased_first
    # getterでの呼び出し
    first.upcase
  end
end

name = Name.new('wata', 'fjord')
# 大文字に変換される
name.uppercased_first #=> WATA

ここで注意すべきなのが、setterです!
インスタンス変数の初期化を、以下のようなsetterメソッドの呼び出し方法で置き換えてみます。

class Name
  attr_accessor :first, :last

  def initialize(first_str, last_str)
    first = first_str # setterのつもりで呼び出している
    @last = last_str
  end

  def uppercased_first
    first.uppcase
  end
end

name = Name.new('wata', 'fjord')
# 大文字に変換されない!
name.uppercased_first #=> undefined method `uppcase' for nil:NilClass (NoMethodError)

newメソッドの第1引数(first_str)に文字列'wata'を与えたのに、uppercased_firstでは例外が発生しました。。。
実は、初期化で呼び出したはずのfirstはsetterメソッドではないんです。firstローカル変数として解釈されます。 ローカル変数について、るりまの説明では以下のように書かれています。

変数と定数 (Ruby 3.1 リファレンスマニュアル)

小文字で始まる識別子への最初の代入はそのスコープに属するローカル変数の宣言になります。

そのためinitializeメソッド内では、ローカル変数firstの値は'wata'ですが、インスタンス変数@firstの値はnilです。

class Name
  def initialize(first_str, last_str)
    first = first_str #=> "wata"
    @last = last_str
    p @first #=> nil
  end
end
name = Name.new('wata', 'fjord')

name.uppercased_firstメソッドを呼び出したのに例外が発生した理由は、 @firstに文字列"wata"が代入されず、値がnilの状態でuppcaseメソッドを呼び出したためです。

この問題を解消するために、クラス内部でsetterメソッドを呼び出すためにselfを用います。selfは実行中のメソッドのレシーバーになるオブジェクトを指します。
オブジェクト.メソッド名と記述することで明示的にメソッドであることを伝えます。

class Name
  attr_accessor :first, :last

  def initialize(first_str, last_str)
    self.first = first_str # setterを利用
    @last = last_str
  end

  def uppercased_first
    first.upcase
  end
end

name = Name.new('wata', 'fjord')
name.uppercased_first #=> 'WATA'

getter、setterのアクセスをprivateにする方法

インスタンス変数への操作方法にgetter、setterを紹介しました。

しかし、上述で紹介した方法は、クラス外部からのアクセスが可能になってしまいます。アクセスするスコープをクラス内部限定にしたい場合もあると思います。
その場合は、privateメソッドを呼び出します。 privateメソッド以降に定義したメソッドは、クラス内のみのアクセスになります。

class Name

  def initialize(first_val, last_val)
    @first = first_val
    @last = last_val
  end

  def lowercased_first
    # クラス内部からのアクセス
    first.downcase
  end

  private   
  attr_accessor :first, :last
end

name = Name.new('wata', 'fjord')
name.lowercased_first #=> 'WATA'
# クラス外部からのアクセスは不可能
name.first #=> private method `first' called for #<Name:0x00007fe7d70aae30 @first="wata", @last="fjord"> (NoMethodError)

1つ注意点として、privateメソッドはJavaC#privateメソッドとは異なる仕様があります。この点は、伊藤さんの記事が詳しいのでこちらを参照して下さい。

blog.jnito.com

また、チェリー本にはprivateメソッドの細かい使い方が解説していあります。

まとめ

今回はリファクタリング本の対立関係「getter、setter」と「インスタンス変数の直接アクセス」を比較する前段として、書き方を紹介しました。

書き方だけを比較すると、「インスタンス変数の直接アクセス」の方が簡易的なので、これ一択のようにも思えます。 しかし、リファクタリングのし易さや可読性の観点から見ると、一概にベストな選択とは言い切れなさそうです。

次回は、書き方以外の観点で比較してます!

2022年の抱負

はじめに

こんにちは! フィヨルドブートキャンプで学習中のwataです。

最初のブログを投稿してから日が経ちすぎたので、ブログを書こうかなと思い立ったのですが、ネタが中々思いつかないこの頃です。😓

私事ですが、今年の3月末に新卒で入社したSES企業を退社しました。
現在は、転職に向けてフィヨルドブートキャンプ(以下、FBC)でフルコミット中です。
3月に退職したこともあり、4、5月は自分にとって気持ちの切り替わりのタイミングとなり、新年を迎えたような気分になっています。

そこで、2022年の抱負を今回の記事にしようと思います。

目次

目標

私は、Web系企業への転職を本気で目指すために、退職して勉強一本に絞ることにしました。

在職中も、転職に向けた勉強をしていましたが、なかなか仕事と勉強の両立は難しかったです。 というのも、案件の納期が迫ると残業時間が増えるため、勉強しようという気が起きず… 勉強が疎かになっていました。

しかし、現在は無職です。悠長なことは言ってられません! 転職活動は12月までと期限を設けました。

そこで、12月までに転職を成功させるために、目標を設けました。 自分が立てた目標は多いので、5つ紹介します。

真面目に就職活動すること

当たり前かもしれませんが。😅 私が新卒で就職活動をしたときは、この基本ができてなかったように思います。

新卒のときも、Web系企業への就職を目標としていました。 しかし、漠然とした憧れだけで、自己分析や面接対策等を怠った結果、就職活動は大失敗に終わりました。 とにかく本気度が足りなかったです。

転職活動は、この反省を踏まえ、「ダラダラと取り組まない」「一つ一つ真剣に取り組む」よう心がけます。

プロジェクトの一貫として取り組むこと

「プロジェクトの一環として取り組む」とは、「計画、実行、振り返り」を短期的に繰り返すことです。

以前から就職活動のために、直向きに学習をしてましたが、行き当たりばったりの行動は不味いなと、考えを改めました。
就職までの道のりでは、思いの外やることが多いです。例えば、私の場合だと以下のようなことが挙げられます。

  • FBCの学習
    • 技術書の通読、課題の提出
    • チーム開発
    • 自作サービス開発
  • FBC以外の学習
    • 技術書の通読
  • 輪読会の参加
  • 自己分析
  • 面接対策
  • 書類作成
  • アウトプット(ブログ)
  • etc...

上記を1時間単位のタスクに分解すると、相当な時間が算出されると思います。 そもそも、作業時間を正確に見積もることは難しいですよね。 さらに、タスク分解をしても、「取り返しのつかない漏れ」があるかもしれないです。

つまり、就職活動では不確実な要素が多いと言えそうです。
何とか策を練らないと新卒時と同じ二の舞を踏みそうです...
そうならないように、就職活動を「プロジェクト」として意識しながら進めていきます。

アウトプットの頻度を上げること

ここでの「アウトプット」はブログを投稿することとします。

約1年前にアウトプット用のブログを開設しましたが、投稿数は1本だけです...
「はじめに」でも述べましたが、何を書くべきかが悩ましいです。

技術のアウトプットに関しては、FBCのメンターである伊藤さんのこちらの記事が参考になりました。 blog.jnito.com

スキルアップに役立つアウトプットは、「自分の経験がベースになっていて、なおかつ、自分でもよくわかっていない点を詳しく調べた上で書いたアウトプット」です。

コードを書いてたり、学習をしてると日頃疑問に思うことは多そうです。
そこで、自分の興味・関心事を中心にネタを探してみようかなと思います。
投稿頻度の目標は月1本です。

コミュニケーション(横の繋がりを増やすこと)

コミュニケーションも細かく見れば色々ありますが、特に人間関係です。 人間関係は、根っからの内向的な性格が起因して大きな課題の1つになってます。

FBCはよく「スクールというよりコミュニティ」と呼ばれてますが、1年入会してもそこまで馴染めてないです。 大勢の輪に入るのが苦手で、コミュニティの方達と会話したことほぼ無いです。

現状の改善策は、場数を踏む、つまり人との関わりを多く持つしかないかなぁと考えてます。 ただ、いきなり大勢の中で会話をして、交流を深めるのはハードルがかなり高いです。 なので、TDD(テスト駆動開発)みたくスモールステップで着実に前進するのが得策と思ってます。

例えば、ミートアップのラジオ参加や輪読会に参加し、スタンプやテキストでアクションすることです。 最近では、朝に「今日やること報告+雑談」を軽く行う「目覚まし会」にも参加してます。

まずは、少しずつ馴染めるようにしたいです。

強みを伸ばすこと

エンジニアチームの一員として活躍するために、今のうちから強みを磨きたいです。 私の中で、自分の強みとは「自分の興味・関心のあること」と定義しています。

具体的には下記の項目になります。

企業が求めていることと、強みがマッチしているかは正直まだ分からないです。
とりあえずやってみる精神で行こうと思います。

まとめ

2022年の抱負を簡単に述べました。
具体的な目標達成へのプロセスは別途ブログに投稿しようと思います。

目標達成に向けて、やらないといけないこと、やりたいこと、やるべきことが多いです。 悔いの残らないように、1つ1つを丁寧に、精一杯取り組んでいきたいと思います。💪

リファクタリングの奨め

はじめに

こんにちは〜! フィヨルドブートキャンプ現役生のwataと申します。

これは、「フィヨルドブートキャンプ Advent Calendar 2021」の18日目の記事です。

フィヨルドブートキャンプ Part 1 Advent Calendar 2021 - Adventar

フィヨルドブートキャンプ Part 2 Advent Calendar 2021 - Adventar

軽い自己紹介ですが、私は現在、新卒で入社したSESの企業でプログラマーとして働いており、入社3年目になります。

フィヨルドブートキャンプに入会して約半年、全くブログを公開してませんでした(汗)。Advent Calendarをきっかけに重い腰を上げてエントリーしてみました!

初投稿なので、温かい目で見守ってください笑🙇‍♂️

本記事では、リファクタリングを知らない人、リファクタリングに抵抗を感じる人を対象に、自分が日頃実践しているマーティン・ファウラー著のリファクタリング本の手法を紹介しつつ、リファクタリングの魅力を伝えられればと思います!

※以降の章では、「リファクタリング本」のことを「リファクタ本」と省略することにします。

※本記事で説明するコードは、フィヨルドブートキャンプのプラクティスであるFizzBuzz問題カレンダープログラムを取り扱います。

目次

リファクタリングとは

まずはリファクタリングの定義の説明をします。リファクタリング本で次のように述べています。

外部から見たときの振る舞いを保ちつつ、 理解や修正が簡単になるように、 ソフトウェアの内部構造を変化させること。

MartinFowler. リファクタリング 既存のコードを安全に改善する(第2版) (p.45). Kindle 版.

外部の振る舞いとは、アプリケーションの動きのことです。内部機能とは、外部の振る舞いを満たすためのロジック、処理の詳細になります。

リファクタリングの肝は改善することにあります。例としてFizzBuzz問題を取り扱います。最初に、下記のような内部機能となるコードを書いて、FizzBuzz問題の振る舞いを実現したとします。

def FizzBuzzProblem(number)
  if (number % 15).zero?
     puts 'FizzBuzz'
  elseif (number % 3).zero?
     puts 'Fizz'
  elseif (number % 5).zero?
     puts 'Buzz'
  else
     puts number
  end
end

次に、リファクタリングによって改善したコードは、下記のようになります。

def FizzBuzzProblem(number)
  if FizzBuzz?(number)
     puts 'FizzBuzz'
  elseif Fizz?(number)
     puts 'Fizz'
  elseif Buzz?(number)
     puts 'Buzz'
  else
     puts number
  end
end

def FizzBuzz?(number)
  Fizz?(number) && Buzz?(number)
end

def Fizz?(number)
  (number % 3).zero?
end

def Buzz?(number)
  (number % 5).zero?
end

改善前後を比較してみます。改善後のコードは、メソッドを増やしたため全体のコード量がふえました。しかし、条件のロジックをメソッドで切り分けることで、条件に名前を付けることができました。 名前を付けることで、処理の詳細を見なくても概要を把握が容易になります。

また、FizzBuzz問題の要件が変更されても、すぐに該当箇所を把握できる、変更内容が他の処理に影響しない構造になっています。

しかし、改善後のコードに振る舞いの変化はありません。

理解しづらい、変更しづらいコードは色々と辛い

FizzBuzz問題では、リファクタリングの旨味を薄く感じるかも知れないです。少し長めのコードを例にしたいと思います。リファクタリングすべきコードは以下のパターンがあります。

  • メソッドの中身が膨大
  • 変数名、関数名とコードの表現が不一致

メソッドの中身が膨大である特徴の1つとして、「1つのメソッドの中に、別のメソッドを呼び出さず、そのまま処理を書く」ことがあります。

カレンダープラクティスのコードを例にします。2021年の12月のカレンダーを表示したプログラムで、外部の振る舞いは下記の3つです。

  • コンソールの1行目に年と月を表示する
  • コンソールの2行目に曜日を表示する
  • コンソールの3行目以降に各週ごとの日を表示する
def cal
  year = 2021
  month = 12
  # 12月の週を配列の要素とする。
  # さらに各週ごとの日を要素とする。曜日に紐づく日がない場合はnil。
  days = [
    [nil, nil, nil, 1, 2, 3, 4],
    [5,  6, 7, 8, 9, 10, 11],
    [12, 13, 14, 15, 16, 17, 18],
    [19, 20, 21, 22, 23, 24, 25],
    [26, 27, 28, 29, 30, 31, nil]
  ]

  # 外部の振る舞い1。カレンダーを描画
  puts "#{month}#{year}".center(20)
  # 外部の振る舞い2。曜日を描画
  displayed_day_of_week = %w[日 月 火 水 木 金 土]
  sep = ' '
  puts displayed_day_of_week.join(sep)

  # 外部の振る舞い3。各週の日を描画
  days.each do |week|
    displayed_week = week.map do |day|
      # 曜日列に日付がない場合は空白表示
      next '  ' if day.nil?

      # 日付の文字数が1文字の場合は空白を追加して幅を調整
      padding_width = day < 10 ? 2 : 0
      day.to_s.rjust(padding_width)
    end
    sep = ' '
    puts displayed_week.join(sep)
  end
end

cal

各外部の振る舞いの開始位置に、振る舞いのコメントを書いてます。コメントがあるため、上から読めば、各外部の振る舞いが理解可能です。しかし、コードの理解、機能の追加、不具合の修正を行う度に、以下のことを意識する必要があります。

  • 各処理の範囲がどこまでか。
    • 明確なくくりがないので、視覚的にわかりづらい。
  • 各処理の振る舞い が何を表すのか。
    • 概要を表現したコメントがない場合、コードの詳細を追わなければならない。
    • 経験的に、コメントはメンテされにくいので、概要を記載したコメントと処理の意味に乖離が起きやすい。
  • 各処理で使用した変数が、他の処理で変更されてないか。
    • 変数のスコープが1つの処理の前後まで及ぼすので、影響している可能性がある。

これらの労力は、1つのメソッドの中にあるコード量に比例します。カレンダーのコードは、先程のFizzBuzz問題のコードと比較すると、注意する労力をより一層要します。

また、2つ目のパターンの変数名、メソッド名の名前が不一致だと、せっかく付けた名前も無駄になります。結局、処理の詳細を見ないと、誤解なく正確に理解することが難しいからです。

私が今まで職場で見たコード(私自身含めて)も、このパターンは多かったです。バグの調査、コードの修正や、コードで仕様把握等の作業過程で、コードを理解するのに時間がかかり、苦労した思い出があります😭

リファクタリングの紹介

カレンダープログラムのサンプルコードを例にして、リファクタ本にある2つの手法を紹介していきたいと思います。

テストコードを書く

リファクタリングをする準備として、テストコードを書きます。 テストコードは、メソッドの振る舞いが期待通りに動くか確認するための用途になります。 リファクタリングをする上で、テストコードを書くメリットは以下が挙げられます。

  • 修正中にバグを素早く検知することができる
  • 少しずつリファクタリングを進めることができる

テストコードがない場合、プログラムが正常に動作する検証方法は、プログラムの実行が必要です。プログラムの実行は全体を通した確認ができても、部分的に正しいかを詳細に見ることはできません。

私が経験した現場では、テストコードがなかったので、修正してもデグレが多発しました。そのため、今までテストした項目も最初からテストすることがありました...

片やテストコードは、修正したコードが原因で不具合が発生すれば、ピンポイントでかつ素早く通知します。安心してプログラムの修正に取り掛かることが可能です。

先程のカレンダープログラムを構成する外部の振る舞い3を対象に、テストコードを書いてみます。

テストコードの対象を見つける手順は、メソッドの抽出で説明します。

各週ごとの日付を表示する処理がありますが、この処理の入力と出力を考えます。

  • 入力:1週間の日を要素とした1次元配列。
    • その週の日がない場合、要素はnil
  • 出力:1週間の日を空白区切りの文字列で出力。
    • 日付表示の最大幅は2。
    • 日付が1文字の場合は右詰め。
    • 要素がnilの場合は空白表示。

上記の入力と出力を元に、以下のようなテストコードを書きます。 1例として入力は、最初の週にします。

テストコード

require 'minitest/autorun'
require_relative './cal'

class CalTest < Minitest::Test
  def test_get_week
    # 入力
    input = [nil, nil, nil, 1, 2, 3, 4]
    # 期待する出力
    expected = '          1  2  3  4'
    
    # 検証。
    assert expected, get_week(input)
  end
end

テストコードにMinitestを使用しています。テストコードのディレクトリは、cal.rbと同じです。

リファクタリング最低限の準備はこれだけです。

次はリファクタリングに入ります。

メソッドの抽出

長い処理の中には、細分化できる塊が多く存在します。この塊には、それぞれの役割があり独立できる可能性があります。意味のある塊をコメントで表現するのではなく、メソッドとして抽出することは以下のメリットがあります。

  • 塊の範囲が視覚的にわかりやすい。
  • リファクタリングが容易にできる。
    • 関数名の変更、内部機能の変更など。
  • 処理を閉じることで、他の関数からの影響を受けにくい。

メソッドの抽出は、大まかに以下の方法で実現できます。リファクタ本では、グローバルなパラメータを参照している場合など、複雑な状況に対応した手順が紹介されています。

  1. 意味のある塊(処理)を探す。
  2. 抽出用のメソッドを用意する。
  3. 手順1で探した処理をコピーし、抽出用のメソッドにペーストする。
  4. コピー元の処理を削除して抽出メソッドを呼び出す。

上記の手順を見るとそこまで複雑なことはしてないです。

外部の振る舞い3を対象にメソッドの抽出を行います。
まずは手順1の「意味のある塊」を探します。

この振る舞いを実現するために、内部機能では、1週間ごとの日を表示する繰り返し処理を行っています。1週間ごとの日を表示する処理は意味のある塊なので、抽出対象となります。

該当箇所は下記になります。

    # 日付をカレンダーのフォーマットに変更
    displayed_week = week.map do |day|
      # 曜日列に日付がない場合は空白表示
      next '  ' if day.nil?

      # 日付の文字数が1文字の場合は空白を追加して幅を調整
      width = 2
      day.to_s.rjust(width)
    end
    sep = ' '
    puts displayed_week.join(sep)

putsは、引数を標準出力に出力するメソッドなので、これを塊の範囲内にすると、返り値がないメソッドになってしまいます。テストコードでは返り値を期待結果と比較するので、putsは除外します。

次に手順2です。
抽出用のメソッドを用意します。メソッド名の検討は後ほど考えて、暫定的な名前を付けます。

def get_week
end

手順3です。 コピーした処理を抽出用メソッドにコピーします。putsは除外します。

def get_week
    # 日付をカレンダーのフォーマットに変更
    displayed_week = week.map do |day|
      # 曜日列に日付がない場合は空白表示
      next '  ' if day.nil?

      # 日付の文字数が1文字の場合は空白を追加して幅を調整
      width = 2
      day.to_s.rjust(width)
    end
    sep = ' '
    displayed_week.join(sep)
end

この状態でテストコードを実行してみると失敗します。メソッドの引数がないからです。 コピー元のコードでは、変数weekをパラメータとして使用しているので、weekを引数として 追加します。

def get_week(week)
    # 日付をカレンダーのフォーマットに変更
    displayed_week = week.map do |day|
      # 曜日列に日付がない場合は空白表示
      next '  ' if day.nil?

      # 日付の文字数が1文字の場合は空白を追加して幅を調整
      width = 2
      day.to_s.rjust(width)
    end
    sep = ' '
    displayed_week.join(sep)
end

最後にコピー元のメソッドを削除してメソッドを呼び出します。メソッドの引数があることに注意します。

def cal
  # ... 省略
  # 各週の日にちを描画
  days.each do |week|
    displayed_week = get_week(week)
    puts displayed_week
  end
end

メソッドの抽出が完了しました。
リファクタリング前後で比べていかがでしょうか。リファクタリング後のコードは、処理の概要を把握しやすくなったと思います。

このコードを見るとさらに塊があります。繰り返し処理をしている処理です。これもメソッドの抽出ができます。
※テストコードは省略します。

def cal
  # ... 省略
  print_days(days)
end

def print_days(days)
  days.each do |week|
    displayed_week = get_week(week)
    puts displayed_week
  end
end

次に、抽出したメソッド名をリファクタリングしていきます。

メソッド名の修正

メソッドの抽出で作られたメソッドにより、長い処理を意味のある塊として切り分けをしました。しかし、意味のある塊を表現する名前が不適切な場合もあります。名前と処理の表現に乖離があると以下のデメリットがあります。

  • 処理の内容を誤解してしまう
  • 理解するのにコストがかかる

名前が不適切だと、結局は処理の詳細を追わなければなりません。

名前の重要性についての詳細と、良い名前の付け方は、WEB+DB PRESS Vol.110名前付け大全記事が参考になります。

名前を修正するリファクタリング手順について、リファクタ本では移行的手順が紹介されています。 移行的手順は主に以下のステップで行います。

  1. リネームの対象となるメソッドの処理全体に、メソッドの抽出を施す。
  2. 古いメソッド名で呼び出している箇所を、新しいメソッド名にリネームする。

メソッドの抽出リファクタリングした外部の振る舞い3を対象に、メソッド名の修正を行います。
この塊の処理は、表示するカレンダー用に1週間の日付を文字列で返しています。
そこで、修正後の名前をdisplayed_weekとします。Rubyでは命名getsetをつけない慣習なので、getは省略します。

手順1です。
メソッドの抽出を行います。抽出する範囲は、メソッドの処理全体になります。

def get_week(week)
  displayed_week(week)
end

def displayed_week(week)
    # 日付をカレンダーのフォーマットに変更
    displayed_week = week.map do |day|
      # 曜日列に日付がない場合は空白表示
      next '  ' if day.nil?

      # 日付の文字数が1文字の場合は空白を追加して幅を調整
      width = 2
      day.to_s.rjust(width)
    end
    sep = ' '
    displayed_week.join(sep)
end

次に手順2です。
テストコード、呼び出し元のコードを1つずつ新しいメソッドで置き換えます。

def cal
  # ... 省略
  print_days(days)
end

def print_days(days)
  days.each do |week|    
    puts displayed_week(week)
  end
end

この方法だと、単純にメソッド名を一括置換する方法と比較して、安全に変更することができます。(IDEのリネーム機能は例外かもしれません)。 途中でテストコードを実行して、正常動作するか確認ができます。 また、途中でリネームの作業を止めてもコードは動くので安心です。

print_daysのメソッド名も不適切なので、同様にリファクタリングしていきます。 print_daysは各週の日付を1行ごとにputsメソッドでコンソールに表示しているので display_weeksとします。

リファクタリングの紹介まとめ

テストコードを作成し、メソッドの抽出とメソッド名のリファクタリングを適用しました。
他の箇所で上記の方法を適用できたり、変数名の修正ができるので、最終的に修正したコードが下記になります。

def cal
  year = 2021
  month = 12
  # 12月の週を配列の要素とする。
  # さらに各週ごとの日を要素とする。曜日に紐づく日がない場合はnil。
  # 曜日のはじめは日曜。
  weeks_of_one_month = [
    [nil, nil, nil, 1, 2, 3, 4],
    [5,  6, 7, 8, 9, 10, 11],
    [12, 13, 14, 15, 16, 17, 18],
    [19, 20, 21, 22, 23, 24, 25],
    [26, 27, 28, 29, 30, 31, nil]
  ]

  # 外部の振る舞い1。
  puts displayed_year_and_month(year, month)

  # 外部の振る舞い2。
  puts displayed_day_of_week(%w[日 月 火 水 木 金 土])

  # 外部の振る舞い3。
  display_weeks(weeks_of_one_month)
end

def displayed_year_and_month(year, month)
  "#{month}#{year}".center(20)
end

def displayed_day_of_week(day_of_week)
  day_of_week = %w[日 月 火 水 木 金 土]
  sep = ' '
  day_of_week.join(sep)
end

def display_weeks(weeks)
  weeks.each do |week|
    puts displayed_week(week)
  end
end

def displayed_week(week)
  # 日付をカレンダーのフォーマットに変更
  displayed_days = week.map do |day|
    # 曜日列に日付がない場合は空白表示
    next '  ' if day.nil?

    # 日付の文字数が1文字の場合は空白を追加して幅を調整
    width = 2
    day.to_s.rjust(width)
  end
  sep = ' '
  displayed_days.join(sep)
end

cal

今回紹介した方法は、意外にも簡単にできると思います。実際、私がリファクタ本を実践する前は難しいものだと感じてましたが、プラクティスでもすぐに実践できました。

冒頭のリファクタリングとはで、改善することがリファクタリングの肝だと言いましたが、もう1つ肝があります。 リファクタ本では、リファクタリングについて以下のことも述べています。

リファクタリングは振る舞いを保ちつつ小さなステップを適用していくことであり、ステップを積み重ねていくことで大きな変化をもたらしていくものなのです。ここのリファクタリングでは非常に小さいステップ、またはそれらの組み合わせでできています。

MartinFowler. リファクタリング 既存のコードを安全に改善する(第2版) (p.46). Kindle 版.

つまり、リファクタリングは、いきなり大規模にコードを修正することではなく、小さな範囲から始めることになります。リファクタリングは、熟練者だけができる方法論ではななく、誰でも簡単に始められるものです。

ラクティスで実践してみての所感

私は、lsコマンドを作るまでリファクタ本を実践してきました。 実践を通して気づいたことは、「リファクタリングは良い循環を与えてくれる」ことです。 具体例として、下記のような、1度改善をするとさらに改善できそうなポイントを自然と見つけられる機会が多くなりました。

  • メソッドを抽出したら、他にもメソッドを抽出できないかを探る。
  • メソッドを作るのでメソッド名を考えるようになる。
  • 名前がしっくり来なければ、切り出す範囲を変えてメソッドの抽出を再度行う。
  • 名前を考える際に正しい表現なのか調べる。
    • ドメイン内ではどのように使われているか。
    • 意味の似た単語の違いは何か調べる。
  • 複数の変数名に同じ形容詞がつくと、データのまとまりにできないか考える。その際に、まとまりを表現する名前を考える

いきなり良い設計が思い浮かばずとも、リファクタリングを繰り返すことで、良い設計にすることも可能なのかなと感じてます。まさに、小さなステップで大きな変化を体感できるようになりつつあります。

リファクタ本では、今回紹介した方法以外にも多くの手法が紹介されています。 また、手法だけでなく、なぜその改善をすべきなのか、改善していくと得すること等も説明があるので、良いコードと悪いコードの特徴を知ることもできます。

まとめ

本記事では、リファクタリングのメリットの説明と、リファクタ本にある手軽に始められる手法の紹介をしました。

私は全くの駆け出しですが、今回の記事を通して、「ちょっとリファクタリングしてみようかな」と思って貰えると幸いです。

かなり長いかつ拙い文章ですが、最後まで読んでいただきありがとうございました。🙏

参考資料