nikkie-ftnextの日記

イベントレポートや読書メモを発信

え、待って!リファクタリングって1つ1つのステップそんなに小さく実施するの!? (『The Art of Agile Development』読書録)

はじめに

こんなクソデカ感情見たことない♪ nikkieです!

『The Art of Agile Development Second edition』を読んでいます1
その中でリファクタリングの章を読んだときに、「私がリファクタリングだと思っていたやり方は全然違っていた」と打ちのめされました。

この衝撃とそこからの学びをアウトプットしてみます。

目次

題材となるプログラム

アルファベットの1文字について、13文字入れ替えた文字に変換するプログラムです。

  • a -> n
  • N -> A

上のリストからは下の対応する位置の文字に、下のリストからは上の対応する位置の文字に変換します(大文字も同様です)

  • abcdefghijklm
  • nopqrstuvwxyz

NIKKIEはAVXXVRとなりますし、
satomi1231ならfngbzv1231となりますね。
AからZ以外の文字は変換されません(先の例では1231がそのままですし、🤗は🤗のままです)

>>> transform("NIKKIE")
'AVXXVR'
>>> transform("satomi1231")
'fngbzv1231'
>>> transform("🤗")
'🤗'

『The Art of Agile Development Second edition』ではJavaScriptで書かれています2が、これをPythonで書き直して、リファクタリングを写経しました。
テストコード込みで写経して、コードを変更するたびにテストが通るかを見ています。

リファクタリング!!劇的ビフォーアフター

ビフォー

def transform(input_: str) -> str:
    if not isinstance(input_, str):
        raise TypeError("Expected string parameter")

    result = ""
    for character in input_:
        char_code = ord(character)
        result += transform_letter(char_code)
    return result


def transform_letter(char_code: int) -> str:
    if is_between(char_code, "a", "m") or is_between(char_code, "A", "M"):
        char_code += 13
    elif is_between(char_code, "n", "z") or is_between(char_code, "N", "Z"):
        char_code -= 13
    return chr(char_code)


def is_between(char_code: int, first_letter: str, last_letter: str) -> bool:
    return char_code >= code_for(first_letter) and char_code <= code_for(
        last_letter
    )


def code_for(letter: str) -> int:
    return ord(letter)

アフター

def transform(input_: str) -> str:
    if not isinstance(input_, str):
        raise TypeError("Expected string parameter")

    result = ""
    for character in input_:
        result += transform_letter(character)
    return result


def transform_letter(letter: str) -> str:
    char_code = ord(letter)
    if ("a" <= letter <= "m") or ("A" <= letter <= "M"):
        char_code += 13
    elif ("n" <= letter <= "z") or ("N" <= letter <= "Z"):
        char_code -= 13
    return chr(char_code)

達人のリファクタリングの進め方

イデア文字コードにしなくても文字自体で比較できる!

「ビフォー」のコードのis_between関数は(文字がaからmの範囲にあるかといった判定に使われるのですが、)文字コードで比較しています。
JavaScriptでもPythonでも)文字コードに変換せずに、文字がaからmの範囲にあるかのような判定ができますね。

>>> ord("a") <= ord("c") <= ord("m")
True
>>> "a" <= "c" <= "m"
True

これが今回のリファクタリングのアイデアです。

初手:transform_letter関数に文字を渡す(引数追加)

transform_letter関数の呼び出しで文字を渡すだけの変更をします(第1引数を追加)。

def transform(input_: str) -> str:
    # 変更がないところは省略
        result += transform_letter(character, char_code)
    # 変更がないところは省略

def transform_letter(character: str, char_code: int) -> str:
    # 変更がないところは省略

仮引数・実引数とも追加するだけです。
中の処理はまだ変えません。
テストが通るので、引数の追加で間違えていないことが確認できます。

渡した引数を使うように処理を変えていませんが、これでリファクタリングの1ステップというのが衝撃でした。
こんなにステップが小さいのか!!

※この後も各ステップで変更するたびにテストを流してリファクタリングを誤っていないか確認していきますが、この記事ではテスト実行についての記載は以降では省略します。

is_between関数にも文字を渡す(引数追加)

transform_letter関数から呼び出すis_between関数についても文字を渡せるように変更します(第1引数を追加)。

def transform_letter(letter: str, char_code: int) -> str:
    # 変更がないところは省略
    if is_between(letter, char_code, "a", "m") or is_between(
        letter, char_code, "A", "M"
    ):
        # 変更がないところは省略
    elif is_between(letter, char_code, "n", "z") or is_between(
        letter, char_code, "N", "Z"
    ):
        # 変更がないところは省略

def is_between(
    letter: str, char_code: int, first_letter: str, last_letter: str
) -> bool:
    # 変更がないところは省略

文字コードでなく文字を比較するようにis_between関数を変更

いよいよリファクタリングのアイデアを実行します!

def is_between(
    letter: str, char_code: int, first_letter: str, last_letter: str
) -> bool:
    return first_letter <= letter <= last_letter

合わせてPythonicな比較演算子の使い方に直しました(x < y < zのように書けます!)

使わなくなったcode_for関数を削除

is_between関数は文字コードでなく文字の比較になりました。
文字コードを取得するためのcode_for関数は使われません。削除しましょう!
(書籍にありますが、リファクタリング中に細かくコミットしておくと細かい単位で戻せるので、消しやすいと思います)

is_between関数のインライン化

リファクタリングのテクニックの1つ、Inline functionでis_between関数の処理を呼び出し箇所に直接書きis_between関数はなくします。

def transform_letter(letter: str, char_code: int) -> str:
    # 変更がないところは省略
    if ("a" <= letter <= "m") or ("A" <= letter <= "M"):
        # 変更がないところは省略
    elif ("n" <= letter <= "z") or ("N" <= letter <= "Z"):
        # 変更がないところは省略

文字の比較にしたことで関数の本体が関数名と同じくらい分かりやすくなり3、インライン化できるようになったと理解しました。
実際「なんで最初からインライン化しなかったんだろう」と思うくらい読みやすさは変わらない印象です。

今回使ったエディタはVSCodeなんですが、Inline functionはサポートされていないんですかね?
リファクタリングの頻出テクニックと思われるので、拡張でできないか調べてみないと(宿題)

transform_letter関数の中で文字コードを取得する

最後にtransform_letter関数のchar_code引数を削除していきます。
小さなステップとしてやることはletter引数を使ってchar_codeを取得することです。

def transform_letter(letter: str, char_code: int) -> str:
    char_code = ord(letter)
    # 変更がないところは省略

transform_letter関数のchar_code引数を削除

ついにtransform_letter関数のchar_code引数を消します。

def transform(input_: str) -> str:
    # 変更がないところは省略
        result += transform_letter(character)
    # 変更がないところは省略

def transform_letter(character: str) -> str:
    # 変更がないところは省略

こうして文字自体で比較というアイデアに基づいたリファクタリングが一段落しました。

書籍ではもう少し続きます

ビフォーアフターのコードを比べるとだいぶスッキリした印象がありますが、実は書籍ではさらにスッキリします(めっちゃ簡潔になります!)。

興味を持った方は(普通の技術書より高いのですが)書籍をご覧くださいー!(翻訳も待たれますね)

リファクタリングの章を読んでの学び

達人のリファクタリングのステップが非常に小さいことを知り、「私の知っていたリファクタリングは全然違っていた…」と衝撃を受けました。

  • 変数の導入、引数の追加も立派なリファクタリングの1ステップ
  • 各ステップでは、テストを流して安全に変更できていることを確認
    • TDDで実装していくのに通じるものがありますね
    • クソデカだとテストが落ちっぱなし(長時間RED)になることがありますが、この方法だとそうなりにくいと思われます
  • リファクタリングのテクニックについて知ることは、IDEの操作の可能性を知るということかも
    • こんなリファクタリングテクニックあるんだ → IDEの操作で簡単にできる!(使えるようになっていく)

非常に小さいステップ × 何回も何回も適用、これでコードが良くなっていくんだなあと仮説を持ちました。
この章を読んで、リファクタリングは1回で終わらせるものではなく、時間が経過していく中で、小さくかつ何回も実施された結果、効いてくるものなんだととらえています。

終わりに

『The Art of Agile Development Second edition』のリファクタリングの章が衝撃的すぎて、この記事にアウトプットしました。
私、リファクタリングのこと、何も分かってなかったんだ…(今は完全に理解した)
小さいステップでのリファクタリング、普段書くコードで練習あるのみですね!💪

実は7/28(木)にリファクタリングLTというLT会があります(ラクスさんありがとうございます!)。
登壇するのでみんな来て!(登壇者も募集中)

今回のアウトプットはLT会の前の事前アウトプットという側面もあります。

One more thing!
Pythonに書き換えたコードはこちらで公開しています。